[chirp_devel] TS480 & 590 Patches

Rick DeWitt AA0RD
Sun Nov 10 10:16:08 PST 2019


Oops... sorry about attaching the driver instead of the patch.
Here they are now. the manifest includes them both.
My first attempt was to combine both drivers, but the IF statements got 
to be huge and very confusing. There are enough differences in CAT 
commands and settings such that it made more sense to split them.

-- 
Rick DeWitt
AA0RD
Sequim, Washington, USA 98382
(360) 681-3494

-------------- next part --------------
# HG changeset patch
# User Rick DeWitt <aa0rd at yahoo.com>
# Date 1573409029 28800
#      Sun Nov 10 10:03:49 2019 -0800
# Node ID ca3b13b5bfbc5209e7e9563a3fd49bb66d9b3552
# Parent  067b69c9b7d04d49462f390d99bc7f80c4b1b5d2
[ts590] New Kenwood TS-590 Clone Mode driver. Issue #7077

New Clone Mode driver for #7077

diff -r 067b69c9b7d0 -r ca3b13b5bfbc chirp/drivers/ts590.py
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/chirp/drivers/ts590.py	Sun Nov 10 10:03:49 2019 -0800
@@ -0,0 +1,1677 @@
+# Copyright 2019 Rick DeWitt <aa0rd at yahoo.com>
+# Version 2.0: No Live Mode library links. Implementing mem as Clone Mode
+#              Having fun with Dictionaries
+# 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/>.
+
+import time
+import struct
+import logging
+import re
+import math
+import threading
+from chirp import chirp_common, directory, memmap
+from chirp import bitwise, errors, util
+from chirp.settings import RadioSettingGroup, RadioSetting, \
+    RadioSettingValueBoolean, RadioSettingValueList, \
+    RadioSettingValueString, RadioSettingValueInteger, \
+    RadioSettingValueFloat, RadioSettings, InvalidValueError
+from textwrap import dedent
+
+LOG = logging.getLogger(__name__)
+
+MEM_FORMAT = """
+#seekto 0x0000;
+struct {            // 20 bytes per chan
+  u32  rxfreq;
+  u32  txfreq;
+  u8   xmode:4,     // param stored as CAT value
+       data:2,
+       tmode:2;
+  u8   rtone;
+  u8   ctone;
+  u8   filter:1,
+       fmnrw:1,
+       skip:1,
+       nu:5;
+  char name[8];
+} ch_mem[120];   // 100 normal + 10 P-type + 10 EXT
+
+struct {        // 5 bytes each
+  u32 asfreq;
+  u8  asmode:4,     // param stored as CAT value
+      asdata:2,
+      asnu:2;
+} asf[32];
+
+struct {        // 10 x 5 4-byte frequencies
+  u32  ssfreq;
+} ssf[50];
+
+struct {        // 16 bytes
+  u8  txeq;
+  u8  rxeq;
+} eqx[8];
+
+struct {        // Common to S and SG models
+  u8   ag;
+  u8   an1:2,
+       an2:2,
+       an3:2,
+       anu:2;
+  u32  fa;
+  u32  fb;
+  char fv[4];
+  u8   mf;
+  u8   mg;
+  u8   pc;
+  u8   rg;
+  u8   tp;
+} settings;
+
+struct {            // Menu A/B settings by TS-590SG names
+  char ex001[8];    // 590S values get put in SG equiv
+  u8   ex002;       // These params stored as nibbles
+  u8   ex003;
+  u8   ex005;
+  u8   ex006;
+  u8   ex007;
+  u8   ex008;
+  u8   ex009;
+  u8   ex010;
+  u8   ex011;
+  u8   ex012;
+  u8   ex013;
+  u8   ex016;
+  u8   ex017;
+  u8   ex018;
+  u8   ex019;
+  u8   ex021;
+  u8   ex022;
+  u8   ex023;
+  u8   ex024;
+  u8   ex025;
+  u8   ex026;
+  u8   ex054;
+  u8   ex055;
+  u8   ex076;
+  u8   ex077;
+  u8   ex087;
+  u8   ex088;
+  u8   ex089;
+  u8   ex090;
+  u8   ex091;
+  u8   ex092;
+  u8   ex093;
+  u8   ex094;
+  u8   ex095;
+  u8   ex096;
+  u8   ex097;
+  u8   ex098;
+  u8   ex099;
+} exset[2];
+
+  char mdl_name[9];     // appended model name, first 9 chars
+
+"""
+
+STIMEOUT = 2
+LOCK = threading.Lock()
+BAUD = 0    # Initial baud rate
+MEMSEL = 0  # Default Menu A
+BEEPVOL = 4     # Default beep volume
+W8S = 0.01      # short wait, secs
+W8L = 0.05      # long wait
+
+TS590_DUPLEX = ["", "-", "+"]
+TS590_SKIP = ["", "S"]
+
+# start at 0:LSB
+TS590_MODES = ["LSB", "USB", "CW", "FM", "AM", "FSK", "CW-R",
+               "FSK-R", "Data+LSB", "Data+USB", "Data+FM"]
+EX_MODES = ["FSK-R", "CW-R", "Data+LSB", "Data+USB", "Data+FM"]
+for ix in EX_MODES:
+    if ix not in chirp_common.MODES:
+        chirp_common.MODES.append(ix)
+
+TS590_TONES = list(chirp_common.TONES)
+TS590_TONES.append(1750.0)
+
+RADIO_IDS = {   # From kenwood_live.py; used to report wrong radio
+    "ID019;": "TS-2000",
+    "ID009;": "TS-850",
+    "ID020;": "TS-480",
+    "ID021;": "TS-590S",
+    "ID023;": "TS-590SG"
+}
+
+
+def command(ser, cmd, rsplen, w8t=0.01, exts=""):
+    """Send @cmd to radio via @ser"""
+    # cmd is output string without ; terminator
+    # rsplen is expected response char count, including terminator
+    #       If rsplen = 0 then do not read after write
+
+    start = time.time()
+    #   LOCK.acquire()
+    stx = cmd       # preserve cmd for response check
+    stx = stx + exts + ";"    # append arguments
+    ser.write(stx)
+    LOG.debug("PC->RADIO [%s]" % stx)
+    ts = time.time()        # implement the wait after command
+    while (time.time() - ts) < w8t:
+        ix = 0      # NOP
+    result = ""
+    if rsplen > 0:  # read response
+        result = ser.read(rsplen)
+        LOG.debug("RADIO->PC [%s]" % result)
+        result = result[:-1]        # remove terminator
+    #   LOCK.release()
+    return result.strip()
+
+
+def _connect_radio(radio):
+    """Determine baud rate and verify radio on-line"""
+    global BAUD        # Allows modification
+    bauds = [115200, 57600, 38400, 19200, 9600, 4800]
+    if BAUD > 0:
+        bauds.insert(0, BAUD)       # Make the detected one first
+    # Flush the input buffer
+    radio.pipe.timeout = 0.005
+    radio.pipe.baudrate = 9600
+    junk = radio.pipe.read(256)
+    radio.pipe.timeout = STIMEOUT
+
+    for bd in bauds:
+        radio.pipe.baudrate = bd
+        BAUD = bd
+        radio.pipe.write(";")
+        radio.pipe.write(";")
+        resp = radio.pipe.read(4)
+        radio.pipe.write("ID;")
+        resp = radio.pipe.read(6)
+
+        if resp == radio.ID:           # Good comms
+            resp = command(radio.pipe, "AI0", 0, W8L)
+            return
+        elif resp in RADIO_IDS.keys():
+            msg = "Radio reported as model %s, not %s!" % \
+                (RADIO_IDS[resp], radio.MODEL)
+            raise errors.RadioError(msg)
+    raise errors.RadioError("No response from radio")
+    return
+
+
+def read_str(radio, trm=";"):
+    """ Read chars until terminator """
+    stq = ""
+    ctq = ""
+    while ctq != trm:
+        ctq = radio.pipe.read(1)
+        stq += ctq
+    LOG.debug("   + [%s]" % stq)
+    return stq[:-1]     # Return without trm
+
+
+def _read_mem(radio):
+    """Get the memory map"""
+    global BEEPVOL
+    # UI progress
+    status = chirp_common.Status()
+    status.cur = 0
+    status.max = radio._upper + 20  # 10 P chans and 10 EXT
+    status.msg = "Reading Channel Memory..."
+    radio.status_fn(status)
+
+    result0 = command(radio.pipe, "EX0050000", 9, W8S)
+    result0 += read_str(radio)
+    BEEPVOL = int(result0[6:])
+    result0 = command(radio.pipe, "EX005000000", 0, W8L)   # Silence beeps
+
+    data = ""
+    mrlen = 41      # Expected fixed return string length
+    for chn in range(0, (radio._upper + 21)):   # Loop stops at +20
+        # Request this mem chn
+        r0ch = 999
+        r1ch = r0ch
+        # return results can come back out of order
+        while (r0ch != chn):
+            # simplex
+            if chn < 100:
+                result0 = command(radio.pipe, "MR0 %02i" % chn,
+                                  mrlen, W8S)
+                result0 += read_str(radio)
+            else:
+                result0 = command(radio.pipe, "MR0%03i" % chn,
+                                  mrlen, W8S)
+                result0 += read_str(radio)
+            r0ch = int(result0[3:6])
+        while (r1ch != chn):
+            # split
+            if chn < 100:
+                result1 = command(radio.pipe, "MR1 %02i" % chn,
+                                  mrlen, W8S)
+                result1 += read_str(radio)
+            else:
+                result1 = command(radio.pipe, "MR1%03i" % chn,
+                                  mrlen, W8S)
+                result1 += read_str(radio)
+            r1ch = int(result1[3:6])
+        data += radio._parse_mem_spec(result0, result1)
+        # UI Update
+        status.cur = chn
+        status.msg = "Reading Channel Memory..."
+        radio.status_fn(status)
+
+    if len(data) == 0:       # To satisfy run_tests
+        raise errors.RadioError('No data received.')
+    return data
+
+
+def _make_dat(sx, nb):
+    """ Split the string sx into nb binary bytes """
+    vx = int(sx)
+    dx = ""
+    if nb > 3:
+        dx += chr((vx >> 24) & 0xFF)
+    if nb > 2:
+        dx += chr((vx >> 16) & 0xFF)
+    if nb > 1:
+        dx += chr((vx >> 8) & 0xFF)
+    dx += chr(vx & 0xFF)
+    return dx
+
+
+def _sets_val(stx, nv=3, nb=2):
+    """ Split string stx into nv nb-bit values in 1 byte """
+    # Right now: hardcoded for nv:3 values of nb:2 bits each
+    v1 = int(stx[0]) << 6
+    v1 = v1 | (int(stx[1]) << 4)
+    v1 = v1 | (int(stx[2]) << 2)
+    return chr(v1)
+
+
+def _sets_asf(stx):
+    """ Process AS0 auto-mode setting """
+    asm = _make_dat(stx[0:11], 4)   # 11-bit freq
+    a1 = int(stx[11])               # 4-bit mode
+    a2 = int(stx[12])               # 2-bit data
+    asm += chr((a1 << 4) | (a2 << 2))
+    return asm
+
+
+def my_val_list(setting, opts, obj, atrb, fix=0, ndx=-1):
+    """Callback:from ValueList. Set the integer index."""
+    # This function is here to be available to get_mem and get_set
+    # fix is optional additive offset to the list index
+    # ndx is optional obj[ndx] array index
+    value = opts.index(str(setting.value))
+    value += fix
+    if ndx >= 0:    # indexed obj
+        setattr(obj[ndx], atrb, value)
+    else:
+        setattr(obj, atrb, value)
+    return
+
+
+def _read_settings(radio):
+    """ Continue filling memory map"""
+    global MEMSEL
+    # setc: the list of CAT commands for downloaded settings
+    # Block paramters first. In the exact order of MEM_FORMAT
+    setc = radio.SETC
+    setc.extend(radio.EX)  # Menu A EX params
+    setc.extend(radio.EX)  # Menu B
+    status = chirp_common.Status()
+    status.cur = 0
+    status.max = 32 + 50 + 8 + 11 + 39 + 39
+    status.msg = "Reading Settings..."
+    radio.status_fn(status)
+
+    setts = ""
+    nc = 0
+    for cmc in setc:
+        skipme = False
+        argx = ""           # Extended arguments
+        if cmc == "AS0":
+            skipme = True   # flag to disable further processing
+            for ix in range(32):        # 32 AS params
+                result0 = command(radio.pipe, cmc, 19, W8S,
+                                  "%02i" % ix)
+                xc = len(cmc) + 2
+                result0 = result0[xc:]
+                setts += _sets_asf(result0)
+                nc += 1
+                status.cur = nc
+                radio.status_fn(status)
+        elif cmc == "SS":
+            skipme = True
+            for ix in range(10):     # 10 chans
+                for nx in range(5):     # 5 spots
+                    result0 = command(radio.pipe, cmc, 16, W8S,
+                                      "%1i%1i" % (ix, nx))
+                    setts += _make_dat(result0[4:], 4)
+                    nc += 1
+                    status.cur = nc
+                    radio.status_fn(status)
+        elif cmc == "EQ":
+            skipme = True
+            for ix in range(8):
+                result0 = command(radio.pipe, cmc, 6, W8S, "0%1i"
+                                  % ix)   # Tx eq
+                setts += chr(int(result0[4:]))    # 'EQ13x", want the x
+                result0 = command(radio.pipe, cmc, 6, W8S, "1%1i"
+                                  % ix)   # Rx eq
+                setts += chr(int(result0[4:]))
+                nc += 1
+                status.cur = nc
+                radio.status_fn(status)
+        elif ((not radio.SG) and (cmc == "EX087")) \
+                or (radio.SG and (cmc == "EX001")):
+            result0 = command(radio.pipe, cmc, 9, W8S, "0000")
+            result0 += read_str(radio)    # Read pwron message
+            result0 = result0[8:]
+            nx = len(result0)
+            for ix in range(8):
+                if ix < nx:
+                    sx = result0[ix]    # may need to test valid char
+                    setts += sx
+                else:
+                    setts += chr(0)
+            skipme = True
+            nc += 1
+            status.cur = nc
+            radio.status_fn(status)
+        elif (cmc == "MF0") or (cmc == "MF1"):
+            result0 = command(radio.pipe, cmc, 0, W8S)
+            skipme = True   # cmd only, no response
+        else:   # issue the cmc cmd as-is with argx
+            if str(cmc).startswith("EX"):
+                argx = "0000"
+            result0 = command(radio.pipe, cmc, 0, W8S, argx)
+            result0 = read_str(radio)    # various length responses
+            # strip the cmd echo
+            xc = len(cmc)
+            result0 = result0[xc:]
+        # Cmd has been sent, process the result
+        if cmc == "FV":      # all chars
+            skipme = True
+            setts += result0
+        elif cmc == "AN":    # Antenna selection has 3 values
+            skipme = True
+            setts += _sets_val(result0, 3, 2)   # store as 2-bits each
+        elif (cmc == "FA") or (cmc == "FB"):    # Response is 11-bit frq
+            skipme = True
+            setts += _make_dat(result0, 4)   # 11-bit freq
+        elif (cmc == "MF0") or (cmc == "MF1"):  # No stored response
+            skipme = True
+        # Generic single byte processing
+        if not skipme:
+            setts += chr(int(result0))
+        if cmc == "MF":     # Save the initial Menu selection
+            MEMSEL = int(result0)
+        nc += 1
+        status.cur = nc
+        radio.status_fn(status)
+    setts += radio.MODEL.ljust(9)
+    # Now set the inidial menu selection back
+    result0 = command(radio.pipe, "MF", 0, W8L, "%1i" % MEMSEL)
+    # And the original Beep Volume
+    result0 = command(radio.pipe, "EX0050000%2i" % BEEPVOL, 0, W8L)
+    return setts
+
+
+def _write_mem(radio):
+    """ Send MW commands for each channel """
+    global BEEPVOL
+    # UI progress
+    status = chirp_common.Status()
+    status.cur = 0
+    status.max = radio._upper + 20  # 10 P chans and 10 EXT
+    status.msg = "Writing Channel Memory"
+    radio.status_fn(status)
+
+    result0 = command(radio.pipe, "EX0050000", 9, W8S)
+    result0 += read_str(radio)
+    BEEPVOL = int(result0[6:])
+    result0 = command(radio.pipe, "EX005000000", 0, W8L)   # Silence beeps
+
+    for chn in range(0, (radio._upper + 21)):   # Loop stops at +20
+        _mem = radio._memobj.ch_mem[chn]
+        cmx = "MW0 %02i" % chn
+        if chn > 99:
+            cmx = "MW0%03i" % chn
+        stm = cmx + radio._make_base_spec(_mem, _mem.rxfreq)
+        result0 = command(radio.pipe, stm, 0, W8L)     # No response
+        cmx = "MW1 %02i" % chn
+        if chn > 99:
+            cmx = "MW1%03i" % chn
+        stm = cmx + radio._make_base_spec(_mem, _mem.txfreq)
+        if _mem.rxfreq > 0:         # Dont write MW1 if empty
+            result0 = command(radio.pipe, stm, 0, W8L)
+        # UI Update
+        status.cur = chn
+        radio.status_fn(status)
+    return
+
+
+def _write_sets(radio):
+    """ Send settings and Menu a/b """
+    status = chirp_common.Status()
+    status.cur = 0
+    status.max = 187   # Total to send
+    status.msg = "Writing Settings"
+    radio.status_fn(status)
+    # Define mem struct shortcuts
+    _sets = radio._memobj.settings
+    _asf = radio._memobj.asf
+    _ssf = radio._memobj.ssf
+    _eqx = radio._memobj.eqx
+    _mex = radio._memobj.exset
+    snx = 0     # Settings status counter
+    stlen = 0   # No response count
+    # Send 32 AS
+    for ix in range(32):
+        scm = "AS0%02i%011i%1i%1i" % (ix, _asf[ix].asfreq,
+                                      _asf[ix].asmode, _asf[ix].asdata)
+        result0 = command(radio.pipe, scm, stlen, W8S)
+        snx += 1
+        status.cur = snx
+        radio.status_fn(status)
+    # Send 50 SS
+    for ix in range(10):
+        for kx in range(5):
+            nx = ix * 5 + kx
+            scm = "SS%1i%1i%011i" % (ix, kx, _ssf[nx].ssfreq)
+            result0 = command(radio.pipe, scm, stlen, W8S)
+            snx += 1
+            status.cur = snx
+            radio.status_fn(status)
+    # Send 16 EQ
+    for ix in range(8):
+        scm = "EQ0%1i%1i" % (ix, _eqx[ix].txeq)
+        result0 = command(radio.pipe, scm, stlen, W8S)
+        scm = "EQ1%1i%1i" % (ix, _eqx[ix].rxeq)
+        result0 = command(radio.pipe, scm, stlen, W8S)
+        snx += 2
+        status.cur = snx
+        radio.status_fn(status)
+    # Send 11 thingies
+    scm = "AG0%03i" % _sets.ag
+    result0 = command(radio.pipe, scm, stlen, W8S)
+    scm = "AN%1i%1i%1i" % (_sets.an1, _sets.an2, _sets.an3)
+    result0 = command(radio.pipe, scm, stlen, W8S)
+    scm = "FA%011i" % _sets.fa
+    result0 = command(radio.pipe, scm, stlen, W8S)
+    scm = "FB%011i" % _sets.fb
+    result0 = command(radio.pipe, scm, stlen, W8S)
+    scm = "MG%03i" % _sets.mg
+    result0 = command(radio.pipe, scm, stlen, W8S)
+    scm = "PC%03i" % _sets.pc
+    result0 = command(radio.pipe, scm, stlen, W8S)
+    scm = "RG%03i" % _sets.rg
+    result0 = command(radio.pipe, scm, stlen, W8S)
+    scm = "TP%03i" % _sets.tp
+    result0 = command(radio.pipe, scm, stlen, W8S)
+    scm = "MF0"   # Select menu A/B
+    result0 = command(radio.pipe, scm, stlen, W8S)
+    snx += 11
+    status.cur = snx
+    radio.status_fn(status)
+    # Send 38 Menu A EX
+    setc = radio.EX     # list of EX cmds
+    for ix in range(2):
+        for cmx in setc:
+            if str(cmx)[0:2] == "MF":
+                scm = cmx
+            else:       # The EX cmds
+                # Test for the power-on string
+                if (radio.SG and cmx == "EX001") or \
+                        ((not radio.SG) and cmx == "EX087"):
+                    scm = cmx + "0000"
+                    for chx in _mex[ix].ex001:  # Both get string here
+                        scm += chr(chx)
+                    scm = scm.strip()
+                    scm = scm.strip(chr(0))     # in case any got thru
+                # Now for the other EX cmds
+                else:
+                    if radio.SG:
+                        scm = "%s0000%i" % (cmx, getattr(_mex[ix],
+                                            cmx.lower()))
+                    else:   # Gotta use the cross reference dict for cmd
+                        scm = "%s0000%i" % (cmx, getattr(_mex[ix],
+                                            radio.EX_X[cmx].lower()))
+            result0 = command(radio.pipe, scm, stlen, W8S)
+            snx += 1
+            status.cur = snx
+            radio.status_fn(status)
+    # Now set the inidial menu selection back
+    result0 = command(radio.pipe, "MF", 0, W8L, "%1i" % _sets.mf)
+    # And the original Beep Volume
+    result0 = command(radio.pipe, "EX0050000%2i" % BEEPVOL, 0, W8L)
+    return
+
+
+ at directory.register
+class TS590Radio(chirp_common.CloneModeRadio):
+    """Kenwood TS-590"""
+    VENDOR = "Kenwood"
+    MODEL = "TS-590SG_CloneMode"
+    ID = "ID023;"
+    SG = True
+    # Settings read/write cmd sequence list
+    SETC = ["AS0", "SS", "EQ", "AG0", "AN", "FA", "FB",
+            "FV", "MF", "MG", "PC", "RG", "TP", "MF0"]
+    # This is the TS-590SG MENU A/B read_settings paramter tuple list
+    # The order is mandatory; to match the Mem_Format sequence
+    EX = ["EX001", "EX002", "EX003", "EX005", "EX006", "EX007",
+          "EX008", "EX009", "EX010", "EX011", "EX012", "EX013", "EX016",
+          "EX017", "EX018", "EX019", "EX021", "EX022", "EX023", "EX024",
+          "EX025", "EX026", "EX054", "EX055", "EX076", "EX077", "EX087",
+          "EX088", "EX089", "EX090", "EX091", "EX092", "EX093", "EX094",
+          "EX095", "EX096", "EX097", "EX098", "EX099", "MF1"]
+    # EX menu settings label dictionary. Key is the EX number
+    EX_LBL = {2: " Display brightness",
+              1: " Power-On message",
+              3: " Backlight color",
+              5: " Beep volume",
+              6: " Sidetone volume",
+              7: " Message playback volume",
+              8: " Voice guide volume",
+              9: " Voice guide speed",
+              10: " Voice guide language",
+              11: " Auto Announcement",
+              12: " MHz step",
+              13: " Tuning control adj rate (Hz)",
+              16: " SSB tune step (KHz)",
+              17: " CW/FSK tune step (KHz)",
+              18: " AM tune step (KHz)",
+              19: " FM tune step (KHz)",
+              21: " Max number of Quick Mem chans",
+              22: " Temporary MR Chan freq allowed",
+              23: " Program Scan slowdown",
+              24: " Program Scan slowdown range (Hz)",
+              25: " Program Scan hold",
+              26: " Scan Resume method",
+              54: " TX Power fine adjust",
+              55: " Timeout timer (Secs)",
+              76: " Data VOX",
+              77: " Data VOX delay (x30 mSecs)",
+              87: " Panel PF-A function",
+              88: " Panel PF-B function",
+              89: " RIT key function",
+              90: " XIT key function",
+              91: " CL key function",
+              92: " Front panel MULTI/CH key (non-CW mode)",
+              93: " Front panel MULTI/CH key (CW mode)",
+              94: " MIC PF1 function",
+              95: " MIC PF2 function",
+              96: " MIC PF3 function",
+              97: " MIC PF4 function",
+              98: " MIC PF (DWN) function",
+              99: " MIC PF (UP) function"}
+
+    BAUD_RATE = 115200
+    _upper = 99
+
+    # Special Channels Declaration
+    # WARNING Indecis are hard wired in get/set_memory code !!!
+    # Channels print in + increasing index order
+    SPECIAL_MEMORIES = {"EXT 0": 110,
+                        "EXT 1": 111,
+                        "EXT 2": 112,
+                        "EXT 3": 113,
+                        "EXT 4": 114,
+                        "EXT 5": 115,
+                        "EXT 6": 116,
+                        "EXT 7": 117,
+                        "EXT 8": 118,
+                        "EXT 9": 119}
+
+    def get_features(self):
+        rf = chirp_common.RadioFeatures()
+
+        rf.can_odd_split = False
+        rf.has_bank = False
+        rf.has_ctone = True
+        rf.has_dtcs = False
+        rf.has_dtcs_polarity = False
+        rf.has_name = True
+        rf.has_settings = True
+        rf.has_offset = True
+        rf.has_mode = True
+        rf.has_tuning_step = False  # Not in mem chan
+        rf.has_nostep_tuning = True     # Radio accepts any entered freq
+        rf.has_cross = True
+        rf.has_comment = False
+        rf.memory_bounds = (0, self._upper)
+        rf.valid_bands = [(30000, 24999999), (25000000, 59999999)]
+        rf.valid_characters = chirp_common.CHARSET_UPPER_NUMERIC + "*+-/"
+        rf.valid_duplexes = TS590_DUPLEX
+        rf.valid_modes = TS590_MODES
+        rf.valid_skips = TS590_SKIP
+        rf.valid_tuning_steps = [0.5, 1.0, 2.5, 5.0, 6.25, 10.0, 12.5,
+                                 15.0, 20.0, 25.0, 30.0, 50.0, 100.0]
+        rf.valid_tmodes = ["", "Tone", "TSQL", "Cross"]
+        rf.valid_cross_modes = ["Tone->Tone"]
+        rf.valid_name_length = 8    # 8 character channel names
+        rf.valid_special_chans = sorted(self.SPECIAL_MEMORIES.keys())
+
+        return rf
+
+    @classmethod
+    def get_prompts(cls):
+        rp = chirp_common.RadioPrompts()
+        rp.info = _(dedent("""\
+            Click on the "Special Channels" toggle-button of the memory
+            editor to see/set the EXT channels. P-VFO channels 100-109
+            are considered Settings.\n
+            Only a subset of the over 200 available radio settings
+            are supported in this release.\n
+            Ignore the beeps from the radio on upload and download.
+            """))
+        rp.pre_download = _(dedent("""\
+            Follow these instructions to download the radio memory:
+            1 - Connect your interface cable
+            2 - Radio > Download from radio: DO NOT mess with the radio
+            during download!
+            3 - Disconnect your interface cable
+            """))
+        rp.pre_upload = _(dedent("""\
+            Follow these instructions to upload the radio memory:
+            1 - Connect your interface cable
+            2 - Radio > Upload to radio: DO NOT mess with the radio
+            during upload!
+            3 - Disconnect your interface cable
+            """))
+        return rp
+
+    def sync_in(self):
+        """Download from radio"""
+        try:
+            _connect_radio(self)
+            data = _read_mem(self)
+            data += _read_settings(self)
+        except errors.RadioError:
+            # Pass through any real errors we raise
+            raise
+        except Exception:
+            # If anything unexpected happens, make sure we raise
+            # a RadioError and log the problem
+            LOG.exception('Unexpected error during download')
+            raise errors.RadioError('Unexpected error communicating '
+                                    'with the radio')
+
+        self._mmap = memmap.MemoryMap(data)
+        self.process_mmap()
+        return
+
+    def sync_out(self):
+        """Upload to radio"""
+        try:
+            _connect_radio(self)
+            _write_mem(self)
+            _write_sets(self)
+        except Exception:
+            # If anything unexpected happens, make sure we raise
+            # a RadioError and log the problem
+            LOG.exception('Unexpected error during upload')
+            raise errors.RadioError('Unexpected error communicating '
+                                    'with the radio')
+        return
+
+    def process_mmap(self):
+        """Process the mem map into the mem object"""
+        self._memobj = bitwise.parse(MEM_FORMAT, self._mmap)
+        return
+
+    def get_memory(self, number):
+        """Convert raw channel data (_mem) into UI columns (mem)"""
+        mem = chirp_common.Memory()
+        mem.extra = RadioSettingGroup("extra", "Extra")
+        if isinstance(number, str):
+            mem.name = number   # Spcl chns 1st var
+            mem.number = self.SPECIAL_MEMORIES[number]
+            _mem = self._memobj.ch_mem[mem.number]
+        else:       # Normal mem chans and VFO edges
+            if number > 99 and number < 110:
+                return          # Don't show VFO edges as mem chans
+            _mem = self._memobj.ch_mem[number]
+            mem.number = number
+            mnx = ""
+            for char in _mem.name:
+                mnx += chr(char)
+            mem.name = mnx.strip()
+            mem.name = mem.name.upper()
+        # From here on is common to both types
+        if _mem.rxfreq == 0:
+            _mem.txfreq = 0
+            mem.empty = True
+            mem.freq = 0
+            mem.mode = "LSB"
+            mem.offset = 0
+            return mem
+        mem.empty = False
+        mem.freq = int(_mem.rxfreq)
+        mem.duplex = TS590_DUPLEX[0]    # None by default
+        mem.offset = 0
+        if _mem.rxfreq < _mem.txfreq:   # + shift
+            mem.duplex = TS590_DUPLEX[2]
+            mem.offset = _mem.txfreq - _mem.rxfreq
+        if _mem.rxfreq > _mem.txfreq:   # - shift
+            mem.duplex = TS590_DUPLEX[1]
+            mem.offset = _mem.rxfreq - _mem.txfreq
+        if _mem.txfreq == 0:
+            # leave offset alone, or run_tests will bomb
+            mem.duplex = TS590_DUPLEX[0]
+        mx = _mem.xmode - 1     # CAT modes start at 1
+        if _mem.xmode == 9:     # except CAT FSK-R is 9, there is no 8
+            mx = 7
+        if _mem.data:       # LSB+Data= 8, USB+Data= 9, FM+Data= 10
+            if _mem.xmode == 1:     # CAT LSB
+                mx = 8
+            elif _mem.xmode == 2:   # CAT USB
+                mx = 9
+            elif _mem.xmode == 4:   # CAT FM
+                mx = 10
+        mem.mode = TS590_MODES[mx]
+        mem.tmode = ""
+        mem.cross_mode = "Tone->Tone"
+        mem.ctone = TS590_TONES[_mem.ctone]
+        mem.rtone = TS590_TONES[_mem.rtone]
+        if _mem.tmode == 1:
+            mem.tmode = "Tone"
+        elif _mem.tmode == 2:
+            mem.tmode = "TSQL"
+        elif _mem.tmode == 3:
+            mem.tmode = "Cross"
+        mem.skip = TS590_SKIP[_mem.skip]
+
+        # Channel Extra settings: Only Boolean & List methods, no call-backs
+        options = ["Wide", "Narrow"]
+        rx = RadioSettingValueList(options, options[_mem.fmnrw])
+        # NOTE: first param of RadioSetting is the object attribute name
+        rset = RadioSetting("fmnrw", "FM mode", rx)
+        rset.set_apply_callback(my_val_list, options, _mem, "fmnrw")
+        mem.extra.append(rset)
+
+        options = ["Filter A", "Filter B"]
+        rx = RadioSettingValueList(options, options[_mem.filter])
+        rset = RadioSetting("filter", "Filter A/B", rx)
+        rset.set_apply_callback(my_val_list, options, _mem, "filter")
+        mem.extra.append(rset)
+
+        return mem
+
+    def set_memory(self, mem):
+        """Convert UI column data (mem) into MEM_FORMAT memory (_mem)"""
+        _mem = self._memobj.ch_mem[mem.number]
+        if mem.empty:
+            _mem.rxfreq = 0
+            _mem.txfreq = 0
+            _mem.xmode = 0
+            _mem.data = 0
+            _mem.tmode = 0
+            _mem.rtone = 0
+            _mem.ctone = 0
+            _mem.filter = 0
+            _mem.skip = 0
+            _mem.fmnrw = 0
+            _mem.name = "        "
+            return
+
+        if mem.number > self._upper:    # Specials: No Name changes
+            ix = 0
+            # LOG.warning("Special Chan set_mem @ %i" % mem.number)
+        else:
+            nx = len(mem.name)
+            for ix in range(8):
+                if ix < nx:
+                    _mem.name[ix] = mem.name[ix].upper()
+                else:
+                    _mem.name[ix] = " "    # assignment needs 8 chrs
+        _mem.rxfreq = mem.freq
+        _mem.txfreq = 0
+        if mem.duplex == "+":
+            _mem.txfreq = mem.freq + mem.offset
+        if mem.duplex == "-":
+            _mem.txfreq = mem.freq - mem.offset
+        ix = TS590_MODES.index(mem.mode)
+        _mem.data = 0
+        _mem.xmode = ix + 1     # stored as CAT values, LSB= 1
+        if ix == 7:     # FSK-R
+            _mem.xmode = 9      # There is no CAT 8
+        if ix > 7:      # a Data mode
+            _mem.data = 1
+            if ix == 8:
+                _mem.xmode = 1      # LSB
+            elif ix == 9:
+                _mem.xmode = 2      # USB
+            elif ix == 10:
+                _mem.xmode = 4      # FM
+        _mem.tmode = 0
+        _mem.rtone = TS590_TONES.index(mem.rtone)
+        _mem.ctone = TS590_TONES.index(mem.ctone)
+        if mem.tmode == "Tone":
+            _mem.tmode = 1
+        if mem.tmode == "TSQL":
+            _mem.tmode = 2
+        if mem.tmode == "Cross" or mem.tmode == "Tone->Tone":
+            _mem.tmode = 3
+        _mem.skip = 0
+        if mem.skip == "S":
+            _mem.skip = 1
+        for setting in mem.extra:
+            setattr(_mem, setting.get_name(), setting.value)
+        return
+
+    def _parse_mem_spec(self, spec0, spec1):
+        """ Extract ascii memory paramters; build data string """
+        # spec0 is simplex result, spec1 is split
+        # pad string so indexes match Kenwood docs
+        spec0 = "x" + spec0  # match CAT document 1-based description
+        ix = len(spec0)
+        # _pxx variables are STRINGS
+        _p1 = spec0[3]       # P1    Split Specification
+        _p3 = spec0[4:7]     # P3    Memory Channel
+        _p4 = spec0[7:18]    # P4    Frequency
+        _p5 = spec0[18]      # P5    Mode
+        _p6 = spec0[19]      # P6    Data Mode
+        _p7 = spec0[20]      # P7    Tone Mode
+        _p8 = spec0[21:23]   # P8    Tone Frequency Index
+        if _p8 == "00":      # Can't be 0 at upload
+            _p8 = "08"
+        _p9 = spec0[23:25]   # P9    CTCSS Frequency Index
+        if _p9 == "00":
+            _p9 = "08"
+        _p10 = spec0[25:28]  # P10   Always 0
+        _p11 = spec0[28]     # P11   Filter A/B
+        _p12 = spec0[29]     # P12   Always 0
+        _p13 = spec0[30:38]  # P13   Always 0
+        _p14 = spec0[38:40]  # P14   FM Mode
+        _p15 = spec0[40]     # P15   Chan Lockout (Skip)
+        _p16 = spec0[41:49]  # P16   Max 8-Char Name if assigned
+
+        spec1 = "x" + spec1
+        _p4s = int(spec1[7:18])  # P4: Offset freq
+
+        datm = ""   # Fill in MEM_FORMAT sequence
+        datm += _make_dat(_p4, 4)   # rxreq: u32, 4 bytes/chars
+        datm += _make_dat(_p4s, 4)  # tx freq
+        v1 = int(_p5) << 4          # xMode: 0-9, upper 4 bits
+        v1 = v1 | (int(_p6) << 2)   # Data: 0/1
+        v1 = v1 | int(_p7)          # Tmode: 0-3
+        datm += chr(v1)
+        datm += chr(int(_p8))       # rtone: 00-42
+        datm += chr(int(_p9))       # ctone
+        v1 = int(_p11) << 7         # Filter A/B 1 bit msb
+        v1 = v1 | (int(_p14) << 6)  # fmwide: 1 bit
+        v1 = v1 | (int(_p15) << 5)  # skip: 1 bit
+        datm += chr(v1)
+        v1 = len(_p16)
+        for ix in range(8):
+            if ix < v1:
+                datm += _p16[ix]
+            else:
+                datm += " "
+
+        return datm
+
+    def _make_base_spec(self, mem, freq):
+        """ Generate memory channel parameter string """
+        spec = "%011i%1i%1i%1i%02i%02i000%1i0000000000%02i%1i%s" \
+            % (freq, mem.xmode, mem.data, mem.tmode, mem.rtone,
+                mem.ctone, mem.filter, mem.fmnrw, mem.skip, mem.name)
+
+        return spec.strip()
+
+    def get_settings(self):
+        """Translate the MEM_FORMAT structs into settings in the UI"""
+        # Define mem struct write-back shortcuts
+        _sets = self._memobj.settings
+        _asf = self._memobj.asf
+        _ssf = self._memobj.ssf
+        _eqx = self._memobj.eqx
+        _mex = self._memobj.exset
+        _chm = self._memobj.ch_mem
+        basic = RadioSettingGroup("basic", "Basic Settings")
+        pvfo = RadioSettingGroup("pvfo", "VFO Band Edges")
+        mena = RadioSettingGroup("mena", "Menu A")
+        menb = RadioSettingGroup("menb", "Menu B")
+        equ = RadioSettingGroup("equ", "Equalizers")
+        amode = RadioSettingGroup("amode", "Auto Mode")
+        ssc = RadioSettingGroup("ssc", "Slow Scan")
+        group = RadioSettings(basic, pvfo, mena, menb, equ, amode, ssc)
+
+        mhz1 = 1000000.
+        nsg = not self.SG
+        if nsg:     # Make reverse EX_X dictionary
+            x_ex = dict(zip(self.EX_X.values(), self.EX_X.keys()))
+
+        # Callback functions
+        def _my_readonly(setting, obj, atrb):
+            """NOP callback, prevents writing the setting"""
+            vx = 0
+            return
+
+        def my_adjraw(setting, obj, atrb, fix=0, ndx=-1):
+            """Callback for Integer add or subtract fix from value."""
+            vx = int(str(setting.value))
+            value = vx + int(fix)
+            if value < 0:
+                value = 0
+            if ndx < 0:
+                setattr(obj, atrb, value)
+            else:
+                setattr(obj[ndx], atrb, value)
+            return
+
+        def my_mhz_val(setting, obj, atrb, ndx=-1):
+            """ Callback to set freq back to Htz"""
+            vx = float(str(setting.value))
+            vx = int(vx * mhz1)
+            if ndx < 0:
+                setattr(obj, atrb, vx)
+            else:
+                setattr(obj[ndx], atrb, vx)
+            return
+
+        def my_bool(setting, obj, atrb, ndx=-1):
+            """ Callback to properly set boolean """
+            # set_settings is not setting [indexed] booleans???
+            vx = 0
+            if str(setting.value) == "True":
+                vx = 1
+            if ndx < 0:
+                setattr(obj, atrb, vx)
+            else:
+                setattr(obj[ndx], atrb, vx)
+            return
+
+        def my_asf_mode(setting, obj, nx=0):
+            """ Callback to extract mode and create asmode, asdata """
+            v1 = TS590_MODES.index(str(setting.value))
+            v2 = 0      # asdata
+            vx = v1 + 1     # stored as CAT values, same as xmode
+            if v1 == 7:
+                vx = 9
+            if v1 > 7:      # a Data mode
+                v2 = 1
+                if v1 == 8:
+                    vx = 1      # LSB
+                elif v1 == 9:
+                    vx = 2      # USB
+                elif v1 == 10:
+                    vx = 4      # FM
+            setattr(obj[nx], "asdata", v2)
+            setattr(obj[nx], "asmode", vx)
+            return
+
+        def my_fnctns(setting, obj, ndx, atrb):
+            """ Filter only valid key function assignments """
+            vx = int(str(setting.value))
+            if self.SG:
+                vmx = 210
+                if (vx > 99 and vx < 120) or (vx > 170 and vx < 200):
+                    raise errors.RadioError(" %i Change Ignored for %s."
+                                            % (vx, atrb))
+                    return  # not valid, ignored
+            else:
+                vmx = 208
+                if (vx > 87 and vx < 100) or (vx > 134 and vx < 200):
+                    raise errors.RadioError(" %i Change Ignored for %s."
+                                            % (vx, atrb))
+                    return
+            if vx > vmx:
+                vx = 255       # Off
+            setattr(obj[ndx], atrb, vx)
+            return
+
+        def my_labels(kx):      # nsg and x_ex defined above
+            lbl = "%03i:" % kx      # SG EX number
+            if nsg:
+                lbl = x_ex["EX%03i" % kx][2:] + ":"    # S-model EX num
+            lbl += self.EX_LBL[kx]      # and the label to match
+            return lbl
+
+        # ===== BASIC GROUP =====
+        sx = ""
+        for i in range(4):
+            sx += chr(_sets.fv[i])
+        rx = RadioSettingValueString(0, 4, sx)
+        rset = RadioSetting("settings.fv", "FirmwareVersion", rx)
+        rset.set_apply_callback(_my_readonly, _sets, "fv")
+        basic.append(rset)
+
+        rx = RadioSettingValueInteger(0, 255, _sets.ag + 1)
+        rset = RadioSetting("settings.ag", "AF Gain", rx)
+        rset.set_apply_callback(my_adjraw, _sets, "ag", -1)
+        basic.append(rset)
+
+        rx = RadioSettingValueInteger(0, 255, _sets.rg + 1)
+        rset = RadioSetting("settings.rg", "RF Gain", rx)
+        rset.set_apply_callback(my_adjraw, _sets, "rg", -1)
+        basic.append(rset)
+
+        options = ["ANT1", "ANT2"]
+        # CAUTION: an1 has value of 1 or 2
+        rx = RadioSettingValueList(options, options[_sets.an1 - 1])
+        rset = RadioSetting("settings.an1", "Antenna Selected", rx)
+        # Add 1 to the changed value. S/b 1/2
+        rset.set_apply_callback(my_val_list, options, _sets, "an1", 1)
+        basic.append(rset)
+
+        rx = RadioSettingValueBoolean(bool(_sets.an2))
+        rset = RadioSetting("settings.an2", "Recv Antenna is used", rx)
+        basic.append(rset)
+
+        rx = RadioSettingValueBoolean(bool(_sets.an3))
+        rset = RadioSetting("settings.an3", "Drive Out On", rx)
+        basic.append(rset)
+
+        rx = RadioSettingValueInteger(0, 100, _sets.mg)
+        rset = RadioSetting("settings.mg", "Microphone gain", rx)
+        basic.append(rset)
+
+        nx = 5      # Coarse step
+        if bool(_mex[0].ex054):   # Power Fine enabled in menu A
+            nx = 1
+        vx = _sets.pc       # Trap invalid values from run_tests.py
+        if vx < 5:
+            vx = 5
+        rx = RadioSettingValueInteger(5, 100, vx, nx)
+        sx = "TX Output power (Watts)"
+        rset = RadioSetting("settings.pc", sx, rx)
+        basic.append(rset)
+
+        vx = _sets.tp
+        rx = RadioSettingValueInteger(5, 100, vx, nx)
+        sx = "TX Tuning power (Watts)"
+        rset = RadioSetting("settings.tp", sx, rx)
+        basic.append(rset)
+
+        val = _sets.fa / mhz1       # Allow Rx freq range
+        rx = RadioSettingValueFloat(0.3, 60.0, val, 0.001, 3)
+        sx = "VFO-A Frequency (MHz)"
+        rset = RadioSetting("settings.fa", sx, rx)
+        rset.set_apply_callback(my_mhz_val, _sets, "fa")
+        basic.append(rset)
+
+        val = _sets.fb / mhz1
+        rx = RadioSettingValueFloat(0.3, 60.0, val, 0.001, 3)
+        sx = "VFO-B Frequency (MHz)"
+        rset = RadioSetting("settings.fb", sx, rx)
+        rset.set_apply_callback(my_mhz_val, _sets, "fb")
+        basic.append(rset)
+
+        options = ["Menu A", "Menu B"]
+        rx = RadioSettingValueList(options, options[_sets.mf])
+        sx = "Menu Selected"
+        rset = RadioSetting("settings.mf", sx, rx)
+        rset.set_apply_callback(my_val_list, options, _sets, "mf")
+        basic.append(rset)
+
+        # ==== VFO Edges Group ================
+
+        for mx in range(100, 110):
+            val = _chm[mx].rxfreq / mhz1
+            if val < 1.8:       # Many operators never use this
+                val = 1.8       # So default is 0.0
+            rx = RadioSettingValueFloat(1.8, 54.0, val, 0.001, 3)
+            sx = "VFO-Band %i lower limit (MHz)" % (mx - 100)
+            rset = RadioSetting("ch_mem.rxfreq/%d" % mx, sx, rx)
+            rset.set_apply_callback(my_mhz_val, _chm, "rxfreq", mx)
+            pvfo.append(rset)
+
+            val = _chm[mx].txfreq / mhz1
+            if val < 1.8:
+                val = 54.0
+            rx = RadioSettingValueFloat(1.8, 54.0, val, 0.001, 3)
+            sx = "    VFO-Band %i upper limit (MHz)" % (mx - 100)
+            rset = RadioSetting("ch_mem.txfreq/%d" % mx, sx, rx)
+            rset.set_apply_callback(my_mhz_val, _chm, "txfreq", mx)
+            pvfo.append(rset)
+
+            kx = _chm[mx].xmode
+            options = ["None", "LSB", "USB", "CW", "FM", "AM", "FSK",
+                       "CW-R", "N/A", "FSK-R"]
+            rx = RadioSettingValueList(options, options[kx])
+            sx = "    VFO-Band %i Tx/Rx Mode" % (mx - 100)
+            rset = RadioSetting("ch_mem.xmode/%d" % mx, sx, rx)
+            rset.set_apply_callback(my_val_list, options, _chm,
+                                    "xmode", 0, mx)
+            pvfo.append(rset)
+
+        # ==== Menu A/B Group =================
+
+        for mx in range(2):      # A/B index
+            sx = ""
+            for i in range(8):
+                if int(_mex[mx].ex001[i]) != 0:
+                    sx += chr(_mex[mx].ex001[i])
+            sx = sx.strip()
+            rx = RadioSettingValueString(0, 8, sx)
+            sx = my_labels(1)     # Proper label for EX001
+            rset = RadioSetting("exset.ex001/%d" % mx, sx, rx)
+            if mx == 0:
+                mena.append(rset)
+            else:
+                menb.append(rset)
+
+            sx = my_labels(2)
+            rx = RadioSettingValueInteger(0, 6, _mex[mx].ex002)
+            rset = RadioSetting("exset.ex002", sx, rx)
+            if mx == 0:
+                mena.append(rset)
+            else:
+                menb.append(rset)
+
+            nx = 2
+            if self.SG:
+                nx = 10
+            vx = _mex[mx].ex003 + 1     # radio rtns 0-9
+            rx = RadioSettingValueInteger(1, nx, vx)
+            sx = my_labels(3)
+            rset = RadioSetting("exset.ex003", sx, rx)
+            rset.set_apply_callback(my_adjraw, _mex, "ex003", -1, mx)
+            if mx == 0:
+                mena.append(rset)
+            else:
+                menb.append(rset)
+
+            nx = 9
+            if self.SG:
+                nx = 20
+            rx = RadioSettingValueInteger(0, nx, _mex[mx].ex005)
+            sx = my_labels(5)
+            rset = RadioSetting("exset.ex005", sx, rx)
+            if mx == 0:
+                mena.append(rset)
+            else:
+                menb.append(rset)
+
+            sx = my_labels(6)
+            rx = RadioSettingValueInteger(0, nx, _mex[mx].ex006)
+            rset = RadioSetting("exset.ex006", sx, rx)
+            if mx == 0:
+                mena.append(rset)
+            else:
+                menb.append(rset)
+
+            sx = my_labels(7)
+            rx = RadioSettingValueInteger(0, nx, _mex[mx].ex007)
+            rset = RadioSetting("exset.ex007", sx, rx)
+            if mx == 0:
+                mena.append(rset)
+            else:
+                menb.append(rset)
+
+            nx = 7
+            if self.SG:
+                nx = 20
+            sx = my_labels(8)
+            rx = RadioSettingValueInteger(0, nx, _mex[mx].ex008)
+            rset = RadioSetting("exset.ex008", sx, rx)
+            if mx == 0:
+                mena.append(rset)
+            else:
+                menb.append(rset)
+
+            sx = my_labels(9)
+            rx = RadioSettingValueInteger(0, 4, _mex[mx].ex009)
+            rset = RadioSetting("exset.ex009", sx, rx)
+            if mx == 0:
+                mena.append(rset)
+            else:
+                menb.append(rset)
+
+            options = ["English", "Japanese"]
+            rx = RadioSettingValueList(options, options[_mex[mx].ex010])
+            sx = my_labels(10)
+            rset = RadioSetting("exset.ex010/%d" % mx, sx, rx)
+            rset.set_apply_callback(my_val_list, options, _mex,
+                                    "ex010", 0, mx)
+            if mx == 0:
+                mena.append(rset)
+            else:
+                menb.append(rset)
+
+            options = ["Off", "1", "2"]
+            rx = RadioSettingValueList(options, options[_mex[mx].ex011])
+            sx = my_labels(11)
+            rset = RadioSetting("exset.ex011/%d" % mx, sx, rx)
+            rset.set_apply_callback(my_val_list, options, _mex,
+                                    "ex011", 0, mx)
+            if mx == 0:
+                mena.append(rset)
+            else:
+                menb.append(rset)
+
+            options = ["0.1", "0.5", "1.0"]
+            rx = RadioSettingValueList(options, options[_mex[mx].ex012])
+            sx = my_labels(12)
+            rset = RadioSetting("exset.ex012", sx, rx)
+            rset.set_apply_callback(my_val_list, options, _mex,
+                                    "ex012", 0, mx)
+            if mx == 0:
+                mena.append(rset)
+            else:
+                menb.append(rset)
+
+            options = ["250", "500", "1000"]
+            rx = RadioSettingValueList(options, options[_mex[mx].ex013])
+            sx = my_labels(13)
+            rset = RadioSetting("exset.ex013/%d" % mx, sx, rx)
+            rset.set_apply_callback(my_val_list, options, _mex,
+                                    "ex013", 0, mx)
+            if mx == 0:
+                mena.append(rset)
+            else:
+                menb.append(rset)
+
+            # S and SG have different ranges for control steps
+            options = ["0.5", "1.0", "2.5", "5.0", "10.0"]
+            if self.SG:
+                options = ["Off", "0.5", "0.5", "1.0", "2.5",
+                           "5.0", "10.0"]
+            rx = RadioSettingValueList(options, options[_mex[mx].ex016])
+            sx = my_labels(16)
+            if nsg:
+                sx = "014: Tuning step for SSB/CW/FSK (KHz)"
+            rset = RadioSetting("exset.ex016/%d" % mx, sx, rx)
+            rset.set_apply_callback(my_val_list, options, _mex,
+                                    "ex016", 0, mx)
+            if mx == 0:
+                mena.append(rset)
+            else:
+                menb.append(rset)
+
+            if self.SG:       # this setting only for SG
+                rx = RadioSettingValueList(options,
+                                           options[_mex[mx].ex017])
+
+                sx = my_labels(17)
+                rset = RadioSetting("exset.ex017/%d" % mx, sx, rx)
+                rset.set_apply_callback(my_val_list, options, _mex,
+                                        "ex017", 0, mx)
+                if mx == 0:
+                    mena.append(rset)
+                else:
+                    menb.append(rset)
+
+            options = ["Off", "5.0", "6.25", "10.0", "12.5", "15.0",
+                       "20.0", "25.0", "30.0", "50.0", "100.0"]
+            if self.SG:
+                options.remove("Off")
+            rx = RadioSettingValueList(options, options[_mex[mx].ex018])
+            sx = my_labels(18)
+            rset = RadioSetting("exset.ex018/%d" % mx, sx, rx)
+            rset.set_apply_callback(my_val_list, options, _mex,
+                                    "ex018", 0, mx)
+            if mx == 0:
+                mena.append(rset)
+            else:
+                menb.append(rset)
+
+            rx = RadioSettingValueList(options, options[_mex[mx].ex019])
+            sx = my_labels(19)
+            rset = RadioSetting("exset.ex019/%d" % mx, sx, rx)
+            rset.set_apply_callback(my_val_list, options, _mex,
+                                    "ex019", 0, mx)
+            if mx == 0:
+                mena.append(rset)
+            else:
+                menb.append(rset)
+
+            options = ["3", "5", "10"]
+            rx = RadioSettingValueList(options, options[_mex[mx].ex021])
+            sx = my_labels(21)
+            rset = RadioSetting("exset.ex021/%d" % mx, sx, rx)
+            rset.set_apply_callback(my_val_list, options, _mex,
+                                    "ex021", 0, mx)
+            if mx == 0:
+                mena.append(rset)
+            else:
+                menb.append(rset)
+
+            rx = RadioSettingValueBoolean(bool(_mex[mx].ex022))
+            sx = my_labels(22)
+            rset = RadioSetting("exset.ex022/%d" % mx, sx, rx)
+            rset.set_apply_callback(my_bool, _mex, "ex022", mx)
+            if mx == 0:
+                mena.append(rset)
+            else:
+                menb.append(rset)
+
+            rx = RadioSettingValueBoolean(bool(_mex[mx].ex023))
+            sx = my_labels(23)
+            rset = RadioSetting("exset.ex023/%d" % mx, sx, rx)
+            rset.set_apply_callback(my_bool, _mex, "ex023", mx)
+            if mx == 0:
+                mena.append(rset)
+            else:
+                menb.append(rset)
+
+            options = ["100", "200", "300", "400", "500"]
+            rx = RadioSettingValueList(options, options[_mex[mx].ex024])
+            sx = my_labels(24)
+            rset = RadioSetting("exset.ex024/%d" % mx, sx, rx)
+            rset.set_apply_callback(my_val_list, options, _mex,
+                                    "ex024", 0, mx)
+            if mx == 0:
+                mena.append(rset)
+            else:
+                menb.append(rset)
+
+            rx = RadioSettingValueBoolean(bool(_mex[mx].ex025))
+            sx = my_labels(25)
+            rset = RadioSetting("exset.ex025/%d" % mx, sx, rx)
+            rset.set_apply_callback(my_bool, _mex, "ex025", mx)
+            if mx == 0:
+                mena.append(rset)
+            else:
+                menb.append(rset)
+
+            options = ["TO", "CO"]
+            rx = RadioSettingValueList(options, options[_mex[mx].ex026])
+            sx = my_labels(26)
+            rset = RadioSetting("exset.ex026/%d" % mx, sx, rx)
+            rset.set_apply_callback(my_val_list, options, _mex,
+                                    "ex026", 0, mx)
+            if mx == 0:
+                mena.append(rset)
+            else:
+                menb.append(rset)
+
+            rx = RadioSettingValueBoolean(bool(_mex[mx].ex054))
+            sx = my_labels(54)
+            rset = RadioSetting("exset.ex054/%d" % mx, sx, rx)
+            rset.set_apply_callback(my_bool, _mex, "ex054", mx)
+            if mx == 0:
+                mena.append(rset)
+            else:
+                menb.append(rset)
+
+            options = ["Off", "3", "5", "10", "20", "30"]
+            rx = RadioSettingValueList(options, options[_mex[mx].ex055])
+            sx = my_labels(55)
+            rset = RadioSetting("exset.ex055/%d" % mx, sx, rx)
+            rset.set_apply_callback(my_val_list, options, _mex,
+                                    "ex055", 0, mx)
+            if mx == 0:
+                mena.append(rset)
+            else:
+                menb.append(rset)
+
+            rx = RadioSettingValueBoolean(bool(_mex[mx].ex076))
+            sx = my_labels(76)
+            rset = RadioSetting("exset.ex076/%d" % mx, sx, rx)
+            rset.set_apply_callback(my_bool, _mex, "ex076", mx)
+            if mx == 0:
+                mena.append(rset)
+            else:
+                menb.append(rset)
+
+            rx = RadioSettingValueInteger(0, 100, _mex[mx].ex077, 5)
+            sx = my_labels(77)
+            rset = RadioSetting("exset.ex077/%d" % mx, sx, rx)
+            if mx == 0:
+                mena.append(rset)
+            else:
+                menb.append(rset)
+
+            rx = RadioSettingValueInteger(0, 256, _mex[mx].ex087)
+            sx = my_labels(87)
+            rset = RadioSetting("exset.ex087/%d" % mx, sx, rx)
+            rset.set_apply_callback(my_fnctns, _mex, mx, "ex087")
+            if mx == 0:
+                mena.append(rset)
+            else:
+                menb.append(rset)
+
+            rx = RadioSettingValueInteger(0, 256, _mex[mx].ex088)
+            sx = my_labels(88)
+            rset = RadioSetting("exset.ex088/%d" % mx, sx, rx)
+            rset.set_apply_callback(my_fnctns, _mex, mx, "ex088")
+            if mx == 0:
+                mena.append(rset)
+            else:
+                menb.append(rset)
+
+            if self.SG:       # Next 5 settings not supported in 590S
+                rx = RadioSettingValueInteger(0, 256, _mex[mx].ex089)
+                sx = my_labels(89)
+                rset = RadioSetting("exset.ex089/%d" % mx, sx, rx)
+                rset.set_apply_callback(my_fnctns, _mex, mx, "ex089")
+                if mx == 0:
+                    mena.append(rset)
+                else:
+                    menb.append(rset)
+
+                rx = RadioSettingValueInteger(0, 256, _mex[mx].ex090)
+                sx = my_labels(90)
+                rset = RadioSetting("exset.ex090/%d" % mx, sx, rx)
+                rset.set_apply_callback(my_fnctns, _mex, mx, "ex090")
+                if mx == 0:
+                    mena.append(rset)
+                else:
+                    menb.append(rset)
+
+                rx = RadioSettingValueInteger(0, 256, _mex[mx].ex091)
+                sx = my_labels(91)
+                rset = RadioSetting("exset.ex091/%d" % mx, sx, rx)
+                rset.set_apply_callback(my_fnctns, _mex, mx, "ex091")
+                if mx == 0:
+                    mena.append(rset)
+                else:
+                    menb.append(rset)
+
+                rx = RadioSettingValueInteger(0, 256, _mex[mx].ex092)
+                sx = my_labels(92)
+                rset = RadioSetting("exset.ex092/%d" % mx, sx, rx)
+                rset.set_apply_callback(my_fnctns, _mex, mx, "ex092")
+                if mx == 0:
+                    mena.append(rset)
+                else:
+                    menb.append(rset)
+
+                rx = RadioSettingValueInteger(0, 256, _mex[mx].ex093)
+                sx = my_labels(93)
+                rset = RadioSetting("exset.ex093/%d" % mx, sx, rx)
+                rset.set_apply_callback(my_fnctns, _mex, mx, "ex093")
+                if mx == 0:
+                    mena.append(rset)
+                else:
+                    menb.append(rset)
+
+            # Now both S and SG models
+            rx = RadioSettingValueInteger(0, 256, _mex[mx].ex094)
+            sx = my_labels(94)
+            rset = RadioSetting("exset.ex094/%d" % mx, sx, rx)
+            rset.set_apply_callback(my_fnctns, _mex, mx, "ex094")
+            if mx == 0:
+                mena.append(rset)
+            else:
+                menb.append(rset)
+
+            rx = RadioSettingValueInteger(0, 256, _mex[mx].ex095)
+            sx = my_labels(95)
+            rset = RadioSetting("exset.ex095/%d" % mx, sx, rx)
+            rset.set_apply_callback(my_fnctns, _mex, mx, "ex095")
+            if mx == 0:
+                mena.append(rset)
+            else:
+                menb.append(rset)
+
+            rx = RadioSettingValueInteger(0, 256, _mex[mx].ex096)
+            sx = my_labels(96)
+            rset = RadioSetting("exset.ex096/%d" % mx, sx, rx)
+            rset.set_apply_callback(my_fnctns, _mex, mx, "ex096")
+            if mx == 0:
+                mena.append(rset)
+            else:
+                menb.append(rset)
+
+            rx = RadioSettingValueInteger(0, 256, _mex[mx].ex097)
+            sx = my_labels(97)
+            rset = RadioSetting("exset.ex097/%d" % mx, sx, rx)
+            rset.set_apply_callback(my_fnctns, _mex, mx, "ex097")
+            if mx == 0:
+                mena.append(rset)
+            else:
+                menb.append(rset)
+
+            rx = RadioSettingValueInteger(0, 256, _mex[mx].ex098)
+            sx = my_labels(98)
+            rset = RadioSetting("exset.ex098/%d" % mx, sx, rx)
+            rset.set_apply_callback(my_fnctns, _mex, mx, "ex098")
+            if mx == 0:
+                mena.append(rset)
+            else:
+                menb.append(rset)
+
+            rx = RadioSettingValueInteger(0, 256, _mex[mx].ex099)
+            sx = my_labels(99)
+            rset = RadioSetting("exset.ex099/%d" % mx, sx, rx)
+            rset.set_apply_callback(my_fnctns, _mex, mx, "ex099")
+            if mx == 0:
+                mena.append(rset)
+            else:
+                menb.append(rset)
+        # End of for mx loop
+
+        # ==== Auto Scan Params (amode) ==============
+        for ix in range(32):
+            val = _asf[ix].asfreq / mhz1
+            rx = RadioSettingValueFloat(0.03, 60.0, val, 0.001, 3)
+            rset = RadioSetting("asf.asfreq/%d" % ix,
+                                "Scan %02i Freq (MHz)" % ix, rx)
+            rset.set_apply_callback(my_mhz_val, _asf, "asfreq", ix)
+            amode.append(rset)
+
+            mx = _asf[ix].asmode - 1     # Same logic as xmode
+            if _asf[ix].asmode == 9:
+                mx = 7
+            if _asf[ix].asdata:
+                if _asf[ix].asmode == 1:
+                    mx = 8
+                elif _asf[ix].asmode == 2:
+                    mx = 9
+                elif _asf[ix].asmode == 4:
+                    mx = 10
+            rx = RadioSettingValueList(TS590_MODES, TS590_MODES[mx])
+            rset = RadioSetting("asf.asmode/%d" % ix, "   Mode", rx)
+            rset.set_apply_callback(my_asf_mode, _asf, ix)
+            amode.append(rset)
+
+        # ==== Slow Scan Settings ===
+        for ix in range(10):        # Chans
+            for nx in range(5):     # spots
+                px = ((ix * 5) + nx)
+                val = _ssf[px].ssfreq / mhz1
+                stx = "      -   -   -    Slot %02i Freq (MHz)" % nx
+                if nx == 0:
+                    stx = "Slow Scan %02i, Slot 0 Freq (MHz" % ix
+                rx = RadioSettingValueFloat(0, 54.0, val, 0.001, 3)
+                rset = RadioSetting("ssf.ssfreq/%d" % px, stx, rx)
+                rset.set_apply_callback(my_mhz_val, _ssf, "ssfreq", px)
+                ssc.append(rset)
+
+        # ==== Equalizer subgroup =====
+        mohd = ["SSB", "SSB-DATA", "CW/CW-R", "FM", "FM-DATA", "AM",
+                "AM-DATA", "FSK/FSK-R"]
+        tcurves = ["Off", "HB1", "HB2", "FP", "BB1", "BB2",
+                   "C", "U"]
+        rcurves = ["Off", "HB1", "HB2", "FP", "BB1", "BB2",
+                   "FLAT", "U"]
+        for ix in range(8):
+            rx = RadioSettingValueList(tcurves, tcurves[_eqx[ix].txeq])
+            rset = RadioSetting("eqx.txeq/%d" % ix, "TX %s Equalizer"
+                                % mohd[ix], rx)
+            rset.set_apply_callback(my_val_list, tcurves, _eqx,
+                                    "txeq", 0, ix)
+            equ.append(rset)
+
+            rx = RadioSettingValueList(rcurves, rcurves[_eqx[ix].rxeq])
+            rset = RadioSetting("eqx.rxeq/%d" % ix, "RX %s Equalizer"
+                                % mohd[ix], rx)
+            rset.set_apply_callback(my_val_list, rcurves, _eqx,
+                                    "rxeq", 0, ix)
+            equ.append(rset)
+
+        return group       # END get_settings()
+
+    def set_settings(self, settings):
+        _settings = self._memobj.settings
+        _mem = self._memobj
+        for element in settings:
+            if not isinstance(element, RadioSetting):
+                self.set_settings(element)
+                continue
+            else:
+                try:
+                    name = element.get_name()
+                    if "." in name:
+                        bits = name.split(".")
+                        obj = self._memobj
+                        for bit in bits[:-1]:
+                            if "/" in bit:
+                                bit, index = bit.split("/", 1)
+                                index = int(index)
+                                obj = getattr(obj, bit)[index]
+                            else:
+                                obj = getattr(obj, bit)
+                        setting = bits[-1]
+                    else:
+                        obj = _settings
+                        setting = element.get_name()
+
+                    if element.has_apply_callback():
+                        LOG.debug("Using apply callback")
+                        element.run_apply_callback()
+                    elif element.value.get_mutable():
+                        LOG.debug("Setting %s = %s" % (setting, element.value))
+                        setattr(obj, setting, element.value)
+                except Exception, e:
+                    LOG.debug(element.get_name())
+                    raise
+
+
+ at directory.register
+class TS590SRadio(TS590Radio):
+    """ Kenwood TS-590S variant of the TS590 """
+    VENDOR = "Kenwood"
+    MODEL = "TS-590S_CloneMode"
+    ID = "ID021;"
+    SG = False
+    # This is the equivalent Menu A/B list for the TS-590S
+    # The equivalnt S param is stored in the SG Mem_Format slot
+    EX = ["EX087", "EX000", "EX001", "EX003", "EX004", "EX005",
+          "EX006", "EX007", "EX008", "EX009", "EX010", "EX011", "EX014",
+          "EX014", "EX015", "EX016", "EX017", "EX018", "EX019", "EX020",
+          "EX021", "EX022", "EX048", "EX049", "EX069", "EX070", "EX079",
+          "EX080", "EX080", "EX080", "EX080", "EX080", "EX080", "EX081",
+          "EX082", "EX083", "EX084", "EX085", "EX086", "MF1"]
+    # EX cross reference dictionary- key is S param, value is the SG
+    EX_X = {"EX087": "EX001", "EX000": "EX002", "EX001": "EX003",
+            "EX003": "EX005", "EX004": "EX006", "EX005": "EX007",
+            "EX006": "EX008", "EX007": "EX009", "EX008": "EX010",
+            "EX009": "EX011", "EX010": "EX012", "EX011": "EX013",
+            "EX014": "EX016", "EX015": "EX018", "EX081": "EX094",
+            "EX016": "EX019", "EX017": "EX021", "EX018": "EX022",
+            "EX019": "EX023",
+            "EX020": "EX024", "EX021": "EX025", "EX022": "EX026",
+            "EX048": "EX054", "EX049": "EX055", "EX069": "EX076",
+            "EX070": "EX077", "EX079": "EX087", "EX080": "EX088",
+            "EX082": "EX095", "EX083": "EX096", "EX084": "EX097",
+            "EX085": "EX098", "EX086": "EX099"}
-------------- next part --------------
# HG changeset patch
# User Rick DeWitt <aa0rd at yahoo.com>
# Date 1573406881 28800
#      Sun Nov 10 09:28:01 2019 -0800
# Node ID 6e9d8a59f376e086de5ce6e706edfafbf3c29a02
# Parent  067b69c9b7d04d49462f390d99bc7f80c4b1b5d2
[ts480] New Clone Mode driver for Kenwood TS480 models, Issue #7079



New driver fixes issue #7079

diff -r 067b69c9b7d0 -r 6e9d8a59f376 chirp/drivers/ts480.py
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/chirp/drivers/ts480.py	Sun Nov 10 09:28:01 2019 -0800
@@ -0,0 +1,1132 @@
+# Copyright 2019 Rick DeWitt <aa0rd at yahoo.com>
+# Implementing Kenwood TS-480 as Clone Mode
+# 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/>.
+
+import time
+import struct
+import logging
+import re
+import math
+from chirp import chirp_common, directory, memmap
+from chirp import bitwise, errors, util
+from chirp.settings import RadioSettingGroup, RadioSetting, \
+    RadioSettingValueBoolean, RadioSettingValueList, \
+    RadioSettingValueString, RadioSettingValueInteger, \
+    RadioSettingValueFloat, RadioSettings, InvalidValueError
+from textwrap import dedent
+
+LOG = logging.getLogger(__name__)
+
+MEM_FORMAT = """
+#seekto 0x0000;
+struct {            // 20 bytes per chan
+  u32  rxfreq;
+  u32  txfreq;
+  u8   xmode;     // param stored as CAT value
+  u8   tmode;
+  u8   rtone;
+  u8   ctone;
+  u8   skip;
+  u8   step;
+  char name[8];
+} ch_mem[110];   // 100 normal + 10 P-type
+
+struct {        // 5 bytes each
+  u32 asfreq;
+  u8  asmode:4,     // param stored as CAT value (0 -9)
+      asdata:2,
+      asnu:2;
+} asf[32];
+
+struct {        // 10 x 5 4-byte frequencies
+  u32  ssfreq;
+} ssf[50];
+
+struct {
+  u8   ag;
+  u8   an;
+  u32  fa;
+  u32  fb;
+  u8   mf;
+  u8   mg;
+  u8   pc;
+  u8   rg;
+  u8   ty;
+} settings;
+
+struct {            // Menu A/B settings
+  char ex000;
+  u8   ex003;       // These params stored as nibbles
+  u8   ex007;
+  u8   ex008;
+  u8   ex009;
+  u8   ex010;
+  u8   ex011;
+  u8   ex012;
+  u8   ex013;
+  u8   ex014;
+  u8   ex021;
+  u8   ex022;
+  u8   ex048;
+  u8   ex049;
+  u8   ex050;
+  u8   ex051;
+  u8   ex052;
+} exset[2];
+
+  char mdl_name[9];     // appended model name, first 9 chars
+
+"""
+
+STIMEOUT = 0.6
+BAUD = 0    # Initial baud rate
+MEMSEL = 0  # Default Menu A
+BEEPVOL = 5     # Default beep level
+W8S = 0.01      # short wait, secs
+W8L = 0.05      # long wait
+
+TS480_DUPLEX = ["", "-", "+"]
+TS480_SKIP = ["", "S"]
+
+# start at 0:LSB
+TS480_MODES = ["LSB", "USB", "CW", "FM", "AM", "FSK", "CW-R", "FSK-R"]
+EX_MODES = ["FSK-R", "CW-R"]
+for ix in EX_MODES:
+    if ix not in chirp_common.MODES:
+        chirp_common.MODES.append(ix)
+
+TS480_TONES = list(chirp_common.TONES)
+TS480_TONES.append(1750.0)
+
+TS480_BANDS = [(50000, 24999999),  # VFO Rx range. TX has lockouts
+               (25000000, 59999999)]
+
+TS480_TUNE_STEPS = [0.5, 1.0, 2.5, 5.0, 6.25, 10.0, 12.5,
+                    15.0, 20.0, 25.0, 30.0, 50.0, 100.0]
+
+RADIO_IDS = {   # From kenwood_live.py; used to report wrong radio
+    "ID019;": "TS-2000",
+    "ID009;": "TS-850",
+    "ID020:": "TS-480",
+    "ID021;": "TS-590S",
+    "ID023;": "TS-590SG"
+}
+
+
+def command(ser, cmd, rsplen, w8t=0.01, exts=""):
+    """Send @cmd to radio via @ser"""
+    # cmd is output string without ; terminator
+    # rsplen is expected response char count, including terminator
+    #       If rsplen = 0 then do not read after write
+
+    start = time.time()
+    stx = cmd       # preserve cmd for response check
+    stx = stx + exts + ";"    # append arguments
+    ser.write(stx)
+    LOG.debug("PC->RADIO [%s]" % stx)
+    ts = time.time()        # implement the wait after command
+    while (time.time() - ts) < w8t:
+        ix = 0      # NOP
+    result = ""
+    if rsplen > 0:  # read response
+        result = ser.read(rsplen)
+        LOG.debug("RADIO->PC [%s]" % result)
+        result = result[:-1]        # remove terminator
+    return result.strip()
+
+
+def _connect_radio(radio):
+    """Determine baud rate and verify radio on-line"""
+    global BAUD        # Allows modification
+    bauds = [9600, 115200, 57600, 38400, 19200, 4800]
+    if BAUD > 0:
+        bauds.insert(0, BAUD)       # Make the detected one first
+    # Flush the input buffer
+    radio.pipe.timeout = 0.005
+    junk = radio.pipe.read(256)
+    radio.pipe.timeout = STIMEOUT
+
+    for bd in bauds:
+        radio.pipe.baudrate = bd
+        BAUD = bd
+        radio.pipe.write(";")
+        radio.pipe.write(";")
+        resp = radio.pipe.read(4)
+        radio.pipe.write("ID;")
+        resp = radio.pipe.read(6)
+        if resp == radio.ID:           # Good comms
+            resp = command(radio.pipe, "AI0", 0, W8L)
+            return
+        elif resp in RADIO_IDS.keys():
+            msg = "Radio reported as model %s, not %s!" % \
+                (RADIO_IDS[resp], radio.MODEL)
+            raise errors.RadioError(msg)
+    raise errors.RadioError("No response from radio")
+    return
+
+
+def read_str(radio, trm=";"):
+    """ Read chars until terminator """
+    stq = ""
+    ctq = ""
+    while ctq != trm:
+        ctq = radio.pipe.read(1)
+        stq += ctq
+    LOG.debug("   + [%s]" % stq)
+    return stq[:-1]     # Return without trm
+
+
+def _read_mem(radio):
+    """Get the memory map"""
+    global BEEPVOL
+    # UI progress
+    status = chirp_common.Status()
+    status.cur = 0
+    status.max = radio._upper + 10  # 10 P chans
+    status.msg = "Reading Channel Memory..."
+    radio.status_fn(status)
+
+    result0 = command(radio.pipe, "EX0120000", 12, W8S)
+    BEEPVOL = int(result0[6:12])
+    result0 = command(radio.pipe, "EX01200000", 0, W8L)   # Silence beeps
+    data = ""
+    mrlen = 41      # Expected fixed return string length
+    for chn in range(0, (radio._upper + 11)):   # Loop stops at +10
+        # Request this mem chn
+        r0ch = 999
+        r1ch = r0ch
+        # return results can come back out of order
+        while (r0ch != chn):
+            # simplex
+            result0 = command(radio.pipe, "MR0%03i" % chn,
+                              mrlen, W8S)
+            result0 += read_str(radio)
+            r0ch = int(result0[3:6])
+        while (r1ch != chn):
+            # split
+            result1 = command(radio.pipe, "MR1%03i" % chn,
+                              mrlen, W8S)
+            result1 += read_str(radio)
+            r1ch = int(result1[3:6])
+        data += radio._parse_mem_spec(result0, result1)
+        # UI Update
+        status.cur = chn
+        status.msg = "Reading Channel Memory..."
+        radio.status_fn(status)
+
+    if len(data) == 0:       # To satisfy run_tests
+        raise errors.RadioError('No data received.')
+    return data
+
+
+def _make_dat(sx, nb):
+    """ Split the string sx into nb binary bytes """
+    vx = int(sx)
+    dx = ""
+    if nb > 3:
+        dx += chr((vx >> 24) & 0xFF)
+    if nb > 2:
+        dx += chr((vx >> 16) & 0xFF)
+    if nb > 1:
+        dx += chr((vx >> 8) & 0xFF)
+    dx += chr(vx & 0xFF)
+    return dx
+
+
+def _sets_asf(stx):
+    """ Process AS0 auto-mode setting """
+    asm = _make_dat(stx[0:11], 4)   # 11-bit freq
+    a1 = int(stx[11])               # 4-bit mode
+    a2 = 0                          # not used in TS-480
+    asm += chr((a1 << 4) | (a2 << 2))
+    return asm
+
+
+def my_val_list(setting, opts, obj, atrb, fix=0, ndx=-1):
+    """Callback:from ValueList. Set the integer index."""
+    # This function is here to be available to get_mem and get_set
+    # fix is optional additive offset to the list index
+    # ndx is optional obj[ndx] array index
+    value = opts.index(str(setting.value))
+    value += fix
+    if ndx >= 0:    # indexed obj
+        setattr(obj[ndx], atrb, value)
+    else:
+        setattr(obj, atrb, value)
+    return
+
+
+def _read_settings(radio):
+    """ Continue filling memory map"""
+    global MEMSEL
+    # setc: the list of CAT commands for downloaded settings
+    # Block paramters first. In the exact order of MEM_FORMAT
+    setc = radio.SETC
+    setc.extend(radio.EX)  # Menu A EX params
+    setc.extend(radio.EX)  # Menu B
+    status = chirp_common.Status()
+    status.cur = 0
+    status.max = 32 + 50 + 8 + 17 + 17
+    status.msg = "Reading Settings..."
+    radio.status_fn(status)
+
+    setts = ""
+    nc = 0
+    for cmc in setc:
+        skipme = False
+        argx = ""           # Extended arguments
+        if cmc == "AS0":
+            skipme = True   # flag to disable further processing
+            for ix in range(32):        # 32 AS params
+                result0 = command(radio.pipe, cmc, 19, W8S,
+                                  "%02i" % ix)
+                xc = len(cmc) + 2
+                result0 = result0[xc:]
+                setts += _sets_asf(result0)
+                nc += 1
+                status.cur = nc
+                radio.status_fn(status)
+        elif cmc == "SS":
+            skipme = True
+            for ix in range(10):     # 10 chans
+                for nx in range(5):     # 5 spots
+                    result0 = command(radio.pipe, cmc, 16, W8S,
+                                      "%1i%1i" % (ix, nx))
+                    setts += _make_dat(result0[4:], 4)
+                    nc += 1
+                    status.cur = nc
+                    radio.status_fn(status)
+        elif (cmc == "MF0") or (cmc == "MF1"):
+            result0 = command(radio.pipe, cmc, 0, W8S)
+            skipme = True   # cmd only, no response
+        else:   # issue the cmc cmd as-is with argx
+            if str(cmc).startswith("EX"):
+                argx = "0000"
+            result0 = command(radio.pipe, cmc, 0, W8S, argx)
+            result0 = read_str(radio)    # various length responses
+            # strip the cmd echo
+            xc = len(cmc)
+            result0 = result0[xc:]
+        # Cmd has been sent, process the result
+        if (cmc == "FA") or (cmc == "FB"):    # Response is 11-bit frq
+            skipme = True
+            setts += _make_dat(result0, 4)   # 11-bit freq
+        elif (cmc == "MF0") or (cmc == "MF1"):  # No stored response
+            skipme = True
+        # Generic single byte processing
+        if not skipme:
+            setts += chr(int(result0))
+        if cmc == "MF":     # Save the initial Menu selection
+            MEMSEL = int(result0)
+        nc += 1
+        status.cur = nc
+        radio.status_fn(status)
+    setts += radio.MODEL.ljust(9)
+    # Now set the initial menu selection back
+    result0 = command(radio.pipe, "MF", 0, W8L, "%1i" % MEMSEL)
+    # And the original Beep Volume
+    result0 = command(radio.pipe, "EX0120000%i" % BEEPVOL, 0, W8L)
+    return setts
+
+
+def _write_mem(radio):
+    """ Send MW commands for each channel """
+    global BEEPVOL
+    # UI progress
+    status = chirp_common.Status()
+    status.cur = 0
+    status.max = radio._upper + 10  # 10 P chans
+    status.msg = "Writing Channel Memory"
+    radio.status_fn(status)
+
+    result0 = command(radio.pipe, "EX0120000", 12, W8S)
+    BEEPVOL = int(result0[6:12])
+    result0 = command(radio.pipe, "EX01200000", 0, W8L)   # Silence beeps
+
+    for chn in range(0, (radio._upper + 11)):   # Loop stops at +20
+        _mem = radio._memobj.ch_mem[chn]
+        cmx = "MW0%03i" % chn
+        stm = cmx + radio._make_base_spec(_mem, _mem.rxfreq)
+        result0 = command(radio.pipe, stm, 0, W8L)     # No response
+        if _mem.txfreq > 0:            # Don't write MW1 if empty/deleted
+            cmx = "MW1%03i" % chn
+            stm = cmx + radio._make_base_spec(_mem, _mem.txfreq)
+            result0 = command(radio.pipe, stm, 0, W8L)
+        status.cur = chn
+        radio.status_fn(status)
+    return
+
+
+def _write_sets(radio):
+    """ Send settings and Menu a/b """
+    status = chirp_common.Status()
+    status.cur = 0
+    status.max = 124   # Total to send
+    status.msg = "Writing Settings"
+    radio.status_fn(status)
+    # Define mem struct shortcuts
+    _sets = radio._memobj.settings
+    _asf = radio._memobj.asf
+    _ssf = radio._memobj.ssf
+    _mex = radio._memobj.exset
+    snx = 0     # Settings status counter
+    stlen = 0   # No response count
+    # Send 32 AS
+    for ix in range(32):
+        scm = "AS0%02i%011i%1i%1i" % (ix, _asf[ix].asfreq,
+                                      _asf[ix].asmode, _asf[ix].asdata)
+        result0 = command(radio.pipe, scm, stlen, W8S)
+        snx += 1
+        status.cur = snx
+        radio.status_fn(status)
+    # Send 50 SS
+    for ix in range(10):
+        for kx in range(5):
+            nx = ix * 5 + kx
+            scm = "SS%1i%1i%011i" % (ix, kx, _ssf[nx].ssfreq)
+            result0 = command(radio.pipe, scm, stlen, W8S)
+            snx += 1
+            status.cur = snx
+            radio.status_fn(status)
+    # Send 8 thingies
+    scm = "AG0%03i" % _sets.ag
+    result0 = command(radio.pipe, scm, stlen, W8S)
+    scm = "AN%1i" % _sets.an
+    result0 = command(radio.pipe, scm, stlen, W8S)
+    scm = "FA%011i" % _sets.fa
+    result0 = command(radio.pipe, scm, stlen, W8S)
+    scm = "FB%011i" % _sets.fb
+    result0 = command(radio.pipe, scm, stlen, W8S)
+    scm = "MG%03i" % _sets.mg
+    result0 = command(radio.pipe, scm, stlen, W8S)
+    scm = "PC%03i" % _sets.pc
+    result0 = command(radio.pipe, scm, stlen, W8S)
+    scm = "RG%03i" % _sets.rg
+    result0 = command(radio.pipe, scm, stlen, W8S)
+    # TY cmd is firmware read-only
+    scm = "MF0"   # Select menu A/B
+    result0 = command(radio.pipe, scm, stlen, W8S)
+    snx += 8
+    status.cur = snx
+    radio.status_fn(status)
+    # Send 17 Menu A EX
+    setc = radio.EX     # list of EX cmds
+    for ix in range(2):
+        for cmx in setc:
+            if str(cmx)[0:2] == "MF":
+                scm = cmx
+            else:       # The EX cmds
+                scm = "%s0000%i" % (cmx, getattr(_mex[ix],
+                                    cmx.lower()))
+            result0 = command(radio.pipe, scm, stlen, W8S)
+            snx += 1
+            status.cur = snx
+            radio.status_fn(status)
+    # Now set the initial menu selection back
+    result0 = command(radio.pipe, "MF", 0, W8L, "%1i" % _sets.mf)
+    # And the original Beep Volume
+    result0 = command(radio.pipe, "EX0120000%i" % BEEPVOL, 0, W8L)
+    return
+
+
+ at directory.register
+class TS480Radio(chirp_common.CloneModeRadio):
+    """Kenwood TS-480"""
+    VENDOR = "Kenwood"
+    MODEL = "TS-480_CloneMode"
+    ID = "ID020;"
+    # Settings read/write cmd sequence list
+    SETC = ["AS0", "SS", "AG0", "AN", "FA", "FB",
+            "MF", "MG", "PC", "RG", "TY", "MF0"]
+    # This is the TS-590SG MENU A/B read_settings paramter tuple list
+    # The order is mandatory; to match the Mem_Format sequence
+    EX = ["EX000", "EX003", "EX007", "EX008", "EX009", "EX010", "EX011",
+          "EX012", "EX013", "EX014", "EX021", "EX022", "EX048", "EX049",
+          "EX050", "EX051", "EX052", "MF1"]
+    # EX menu settings label dictionary. Key is the EX number
+    EX_LBL = {0: " Display brightness",
+              3: "  Tuning control adj rate (Hz)",
+              12: " Beep volume",
+              13: " Sidetone volume",
+              14: " Message playback volume",
+              7: " Temporary MR Chan freq allowed",
+              8: " Program Scan slowdown",
+              9: " Program Scan slowdown range (Hz)",
+              10: " Program Scan hold",
+              11: " Scan Resume method",
+              21: " TX Power fine adjust",
+              22: " Timeout timer (Secs)",
+              48: " Panel PF-A function",
+              49: " MIC PF1 function",
+              50: " MIC PF2 function",
+              51: " MIC PF3 function",
+              52: " MIC PF4 function"}
+
+    _upper = 99
+
+    def get_features(self):
+        rf = chirp_common.RadioFeatures()
+
+        rf.can_odd_split = False
+        rf.has_bank = False
+        rf.has_ctone = True
+        rf.has_dtcs = False
+        rf.has_dtcs_polarity = False
+        rf.has_name = True
+        rf.has_settings = True
+        rf.has_offset = True
+        rf.has_mode = True
+        rf.has_tuning_step = True
+        rf.has_nostep_tuning = True     # Radio accepts any entered freq
+        rf.has_cross = False
+        rf.has_comment = False
+        rf.memory_bounds = (0, self._upper)
+        rf.valid_bands = TS480_BANDS
+        rf.valid_characters = chirp_common.CHARSET_UPPER_NUMERIC + "*+-/"
+        rf.valid_duplexes = TS480_DUPLEX
+        rf.valid_modes = TS480_MODES
+        rf.valid_skips = TS480_SKIP
+        rf.valid_tuning_steps = TS480_TUNE_STEPS
+        rf.valid_tmodes = ["", "Tone", "TSQL"]
+        rf.valid_name_length = 8    # 8 character channel names
+
+        return rf
+
+    @classmethod
+    def get_prompts(cls):
+        rp = chirp_common.RadioPrompts()
+        rp.info = _(dedent("""\
+            P-VFO channels 100-109 are considered Settings.\n
+            Only a subset of the over 130 available radio settings
+            are supported in this release.\n
+            """))
+        rp.pre_download = _(dedent("""\
+            Follow these instructions to download the radio memory:
+            1 - Connect your interface cable
+            2 - Radio > Download from radio: Don't adjust any settings
+            on the radio head!
+            3 - Disconnect your interface cable
+            """))
+        rp.pre_upload = _(dedent("""\
+            Follow these instructions to upload the radio memory:
+            1 - Connect your interface cable
+            2 - Radio > Upload to radio: Don't adjust any settings
+            on the radio head!
+            3 - Disconnect your interface cable
+            """))
+        return rp
+
+    def sync_in(self):
+        """Download from radio"""
+        try:
+            _connect_radio(self)
+            data = _read_mem(self)
+            data += _read_settings(self)
+        except errors.RadioError:
+            # Pass through any real errors we raise
+            raise
+        except Exception:
+            # If anything unexpected happens, make sure we raise
+            # a RadioError and log the problem
+            LOG.exception('Unexpected error during download')
+            raise errors.RadioError('Unexpected error communicating '
+                                    'with the radio')
+
+        self._mmap = memmap.MemoryMap(data)
+        self.process_mmap()
+        return
+
+    def sync_out(self):
+        """Upload to radio"""
+        try:
+            _connect_radio(self)
+            _write_mem(self)
+            _write_sets(self)
+        except Exception:
+            # If anything unexpected happens, make sure we raise
+            # a RadioError and log the problem
+            LOG.exception('Unexpected error during upload')
+            raise errors.RadioError('Unexpected error communicating '
+                                    'with the radio')
+        return
+
+    def process_mmap(self):
+        """Process the mem map into the mem object"""
+        self._memobj = bitwise.parse(MEM_FORMAT, self._mmap)
+        return
+
+    def get_memory(self, number):
+        """Convert raw channel data (_mem) into UI columns (mem)"""
+        mem = chirp_common.Memory()
+        if number > 99 and number < 110:
+            return          # Don't show VFO edges as mem chans
+        _mem = self._memobj.ch_mem[number]
+        mem.number = number
+        mnx = ""
+        for char in _mem.name:
+            mnx += chr(char)
+        mem.name = mnx.strip()
+        mem.name = mem.name.upper()
+        if _mem.rxfreq == 0:
+            mem.empty = True
+            return mem
+        mem.empty = False
+        mem.freq = int(_mem.rxfreq)
+        mem.duplex = TS480_DUPLEX[0]    # None by default
+        mem.offset = 0
+        if _mem.rxfreq < _mem.txfreq:   # + shift
+            mem.duplex = TS480_DUPLEX[2]
+            mem.offset = _mem.txfreq - _mem.rxfreq
+        if _mem.rxfreq > _mem.txfreq:   # - shift
+            mem.duplex = TS480_DUPLEX[1]
+            mem.offset = _mem.rxfreq - _mem.txfreq
+        if _mem.txfreq == 0:
+            # leave offset alone, or run_tests will bomb
+            mem.duplex = TS480_DUPLEX[0]
+        mx = _mem.xmode - 1     # CAT modes start at 1
+        if _mem.xmode == 9:     # except there is no xmode 9
+            mx = 7
+        mem.mode = TS480_MODES[mx]
+        mem.tmode = ""
+        mem.cross_mode = "Tone->Tone"
+        mem.ctone = TS480_TONES[_mem.ctone]
+        mem.rtone = TS480_TONES[_mem.rtone]
+        if _mem.tmode == 1:
+            mem.tmode = "Tone"
+        elif _mem.tmode == 2:
+            mem.tmode = "TSQL"
+        elif _mem.tmode == 3:
+            mem.tmode = "Cross"
+        mem.skip = TS480_SKIP[_mem.skip]
+        # Tuning step depends on mode
+        options = [0.5, 1.0, 2.5, 5.0, 10.0]    # SSB/CS/FSK
+        if _mem.xmode == 4 or _mem.xmode == 5:   # AM/FM
+            options = TS480_TUNE_STEPS[3:]
+        mem.tuning_step = options[_mem.step]
+        return mem
+
+    def set_memory(self, mem):
+        """Convert UI column data (mem) into MEM_FORMAT memory (_mem)"""
+        _mem = self._memobj.ch_mem[mem.number]
+        if mem.empty:
+            _mem.rxfreq = 0
+            _mem.txfreq = 0
+            _mem.xmode = 0
+            _mem.step = 0
+            _mem.tmode = 0
+            _mem.rtone = 0
+            _mem.ctone = 0
+            _mem.skip = 0
+            _mem.name = "        "
+            return
+
+        if mem.number > self._upper:    # Specials: No Name changes
+            ix = 0
+            # LOG.warning("Special Chan set_mem @ %i" % mem.number)
+        else:
+            nx = len(mem.name)
+            for ix in range(8):
+                if ix < nx:
+                    _mem.name[ix] = mem.name[ix].upper()
+                else:
+                    _mem.name[ix] = " "    # assignment needs 8 chrs
+        _mem.rxfreq = mem.freq
+        _mem.txfreq = 0
+        if mem.duplex == "+":
+            _mem.txfreq = mem.freq + mem.offset
+        if mem.duplex == "-":
+            _mem.txfreq = mem.freq - mem.offset
+        ix = TS480_MODES.index(mem.mode)
+        _mem.xmode = ix + 1     # stored as CAT values, LSB= 1
+        if ix == 7:     # FSK-R
+            _mem.xmode = 9      # There is no CAT 8
+        _mem.tmode = 0
+        _mem.rtone = TS480_TONES.index(mem.rtone)
+        _mem.ctone = TS480_TONES.index(mem.ctone)
+        if mem.tmode == "Tone":
+            _mem.tmode = 1
+        if mem.tmode == "TSQL":
+            _mem.tmode = 2
+        _mem.skip = 0
+        if mem.skip == "S":
+            _mem.skip = 1
+        options = [0.5, 1.0, 2.5, 5.0, 10.0]    # SSB/CS/FSK steps
+        if _mem.xmode == 4 or _mem.xmode == 5:   # AM/FM
+            options = TS480_TUNE_STEPS[3:]
+        _mem.step = options.index(mem.tuning_step)
+        return
+
+    def _parse_mem_spec(self, spec0, spec1):
+        """ Extract ascii memory paramters; build data string """
+        # spec0 is simplex result, spec1 is split
+        # pad string so indexes match Kenwood docs
+        spec0 = "x" + spec0  # match CAT document 1-based description
+        ix = len(spec0)
+        # _pxx variables are STRINGS
+        _p1 = spec0[3]       # P1    Split Specification
+        _p3 = spec0[5:7]     # P3    Memory Channel
+        _p4 = spec0[7:18]    # P4    Frequency
+        _p5 = spec0[18]      # P5    Mode
+        _p6 = spec0[19]      # P6    Chan Lockout (Skip)
+        _p7 = spec0[20]      # P7    Tone Mode
+        _p8 = spec0[21:23]   # P8    Tone Frequency Index
+        if _p8 == "00":
+            _p8 = "08"
+        _p9 = spec0[23:25]   # P9    CTCSS Frequency Index
+        if _p9 == "00":
+            _p9 = "08"
+        _p14 = spec0[39:41]  # P14   Step Size
+        _p16 = spec0[41:50]  # P16   Max 8-Char Name if assigned
+
+        spec1 = "x" + spec1
+        _p4s = int(spec1[7:18])  # P4: Offset freq
+
+        datm = ""   # Fill in MEM_FORMAT sequence
+        datm += _make_dat(_p4, 4)   # rxreq: u32, 4 bytes/chars
+        datm += _make_dat(_p4s, 4)  # tx freq
+        datm += chr(int(_p5))       # xmode: 0-9
+        datm += chr(int(_p7))       # Tmode: 0-3
+        datm += chr(int(_p8))       # rtone: 00-41
+        datm += chr(int(_p9))       # ctone: 00-41
+        datm += chr(int(_p6))       # skip: 0/1
+        datm += chr(int(_p14))      # step: 0-9
+        v1 = len(_p16)
+        for ix in range(8):
+            if ix < v1:
+                datm += _p16[ix]
+            else:
+                datm += " "
+        return datm
+
+    def _make_base_spec(self, mem, freq):
+        """ Generate memory channel parameter string """
+        spec = "%011i%1i%1i%1i%02i%02i00000000000000%02i0%s" \
+            % (freq, mem.xmode, mem.skip, mem.tmode, mem.rtone,
+                mem.ctone, mem.step, mem.name)
+
+        return spec.strip()
+
+    def get_settings(self):
+        """Translate the MEM_FORMAT structs into settings in the UI"""
+        # Define mem struct write-back shortcuts
+        _sets = self._memobj.settings
+        _asf = self._memobj.asf
+        _ssf = self._memobj.ssf
+        _mex = self._memobj.exset
+        _chm = self._memobj.ch_mem
+        basic = RadioSettingGroup("basic", "Basic Settings")
+        pvfo = RadioSettingGroup("pvfo", "VFO Band Edges")
+        mena = RadioSettingGroup("mena", "Menu A")
+        menb = RadioSettingGroup("menb", "Menu B")
+        amode = RadioSettingGroup("amode", "Auto Mode")
+        ssc = RadioSettingGroup("ssc", "Slow Scan")
+        group = RadioSettings(basic, pvfo, mena, menb, amode, ssc)
+
+        mhz1 = 1000000.
+
+        # Callback functions
+        def _my_readonly(setting, obj, atrb):
+            """NOP callback, prevents writing the setting"""
+            vx = 0
+            return
+
+        def my_adjraw(setting, obj, atrb, fix=0, ndx=-1):
+            """Callback for Integer add or subtract fix from value."""
+            vx = int(str(setting.value))
+            value = vx + int(fix)
+            if value < 0:
+                value = 0
+            if ndx < 0:
+                setattr(obj, atrb, value)
+            else:
+                setattr(obj[ndx], atrb, value)
+            return
+
+        def my_mhz_val(setting, obj, atrb, ndx=-1):
+            """ Callback to set freq back to Htz"""
+            vx = float(str(setting.value))
+            vx = int(vx * mhz1)
+            if ndx < 0:
+                setattr(obj, atrb, vx)
+            else:
+                setattr(obj[ndx], atrb, vx)
+            return
+
+        def my_bool(setting, obj, atrb, ndx=-1):
+            """ Callback to properly set boolean """
+            # set_settings is not setting [indexed] booleans???
+            vx = 0
+            if str(setting.value) == "True":
+                vx = 1
+            if ndx < 0:
+                setattr(obj, atrb, vx)
+            else:
+                setattr(obj[ndx], atrb, vx)
+            return
+
+        def my_asf_mode(setting, obj, nx=0):
+            """ Callback to extract mode and create asmode, asdata """
+            v1 = TS480_MODES.index(str(setting.value))
+            v2 = 0      # asdata
+            vx = v1 + 1     # stored as CAT values, same as xmode
+            if v1 == 7:
+                vx = 9
+            if v1 > 7:      # a Data mode
+                v2 = 1
+                if v1 == 8:
+                    vx = 1      # LSB
+                elif v1 == 9:
+                    vx = 2      # USB
+                elif v1 == 10:
+                    vx = 4      # FM
+            setattr(obj[nx], "asdata", v2)
+            setattr(obj[nx], "asmode", vx)
+            return
+
+        def my_fnctns(setting, obj, ndx, atrb):
+            """ Filter only valid key function assignments """
+            vx = int(str(setting.value))
+            if vx > 79:
+                vx = 99       # Off
+            setattr(obj[ndx], atrb, vx)
+            return
+
+        def my_labels(kx):
+            lbl = "%03i:" % kx      # SG EX number
+            lbl += self.EX_LBL[kx]      # and the label to match
+            return lbl
+
+        # ===== BASIC GROUP =====
+
+        options = ["TS-480HX (200W)", "TS-480SAT (100W + AT)",
+                   "Japanese 50W type", "Japanese 20W type"]
+        rx = RadioSettingValueString(14, 22, options[_sets.ty])
+        rset = RadioSetting("settings.ty", "FirmwareVersion", rx)
+        rset.set_apply_callback(_my_readonly, _sets, "ty")
+        basic.append(rset)
+
+        rx = RadioSettingValueInteger(0, 255, _sets.ag)
+        rset = RadioSetting("settings.ag", "AF Gain", rx)
+        #  rset.set_apply_callback(my_adjraw, _sets, "ag", -1)
+        basic.append(rset)
+
+        rx = RadioSettingValueInteger(0, 100, _sets.rg)
+        rset = RadioSetting("settings.rg", "RF Gain", rx)
+        #   rset.set_apply_callback(my_adjraw, _sets, "rg", -1)
+        basic.append(rset)
+
+        options = ["ANT1", "ANT2"]
+        # CAUTION: an has value of 1 or 2
+        rx = RadioSettingValueList(options, options[_sets.an - 1])
+        rset = RadioSetting("settings.an", "Antenna Selected", rx)
+        # Add 1 to the changed value. S/b 1/2
+        rset.set_apply_callback(my_val_list, options, _sets, "an", 1)
+        basic.append(rset)
+
+        rx = RadioSettingValueInteger(0, 100, _sets.mg)
+        rset = RadioSetting("settings.mg", "Microphone gain", rx)
+        basic.append(rset)
+
+        nx = 5      # Coarse step
+        if bool(_mex[0].ex021):   # Power Fine enabled in menu A
+            nx = 1
+        vx = _sets.pc       # Trap invalid values from run_tests.py
+        if vx < 5:
+            vx = 5
+        options = [200, 100, 50, 20]    # subject to firmware
+        rx = RadioSettingValueInteger(5, options[_sets.ty], vx, nx)
+        sx = "TX Output power (Watts)"
+        rset = RadioSetting("settings.pc", sx, rx)
+        basic.append(rset)
+
+        val = _sets.fa / mhz1       # valid range is for receiver
+        rx = RadioSettingValueFloat(0.05, 60.0, val, 0.001, 3)
+        sx = "VFO-A Frequency (MHz)"
+        rset = RadioSetting("settings.fa", sx, rx)
+        rset.set_apply_callback(my_mhz_val, _sets, "fa")
+        basic.append(rset)
+
+        val = _sets.fb / mhz1
+        rx = RadioSettingValueFloat(0.05, 60.0, val, 0.001, 3)
+        sx = "VFO-B Frequency (MHz)"
+        rset = RadioSetting("settings.fb", sx, rx)
+        rset.set_apply_callback(my_mhz_val, _sets, "fb")
+        basic.append(rset)
+
+        options = ["Menu A", "Menu B"]
+        rx = RadioSettingValueList(options, options[_sets.mf])
+        sx = "Menu Selected"
+        rset = RadioSetting("settings.mf", sx, rx)
+        rset.set_apply_callback(my_val_list, options, _sets, "mf")
+        basic.append(rset)
+
+        # ==== VFO Edges Group ================
+
+        for mx in range(100, 110):
+            val = _chm[mx].rxfreq / mhz1
+            if val < 1.8:       # Many operators never use this
+                val = 1.8       # So default is 0.0
+            rx = RadioSettingValueFloat(1.8, 54.0, val, 0.001, 3)
+            sx = "VFO-Band %i lower limit (MHz)" % (mx - 100)
+            rset = RadioSetting("ch_mem.rxfreq/%d" % mx, sx, rx)
+            rset.set_apply_callback(my_mhz_val, _chm, "rxfreq", mx)
+            pvfo.append(rset)
+
+            val = _chm[mx].txfreq / mhz1
+            if val < 1.8:
+                val = 54.0
+            rx = RadioSettingValueFloat(1.8, 54.0, val, 0.001, 3)
+            sx = "    VFO-Band %i upper limit (MHz)" % (mx - 100)
+            rset = RadioSetting("ch_mem.txfreq/%d" % mx, sx, rx)
+            rset.set_apply_callback(my_mhz_val, _chm, "txfreq", mx)
+            pvfo.append(rset)
+
+            kx = _chm[mx].xmode
+            options = ["None", "LSB", "USB", "CW", "FM", "AM", "FSK",
+                       "CW-R", "N/A", "FSK-R"]
+            rx = RadioSettingValueList(options, options[kx])
+            sx = "    VFO-Band %i Tx/Rx Mode" % (mx - 100)
+            rset = RadioSetting("ch_mem.xmode/%d" % mx, sx, rx)
+            rset.set_apply_callback(my_val_list, options, _chm,
+                                    "xmode", 0, mx)
+            pvfo.append(rset)
+
+        # ==== Menu A/B Group =================
+
+        for mx in range(2):      # A/B index
+            sx = my_labels(0)
+            rx = RadioSettingValueInteger(0, 4, _mex[mx].ex000)
+            rset = RadioSetting("exset.ex000", sx, rx)
+            if mx == 0:
+                mena.append(rset)
+            else:
+                menb.append(rset)
+
+            rx = RadioSettingValueInteger(0, 9, _mex[mx].ex012)
+            sx = my_labels(12)
+            rset = RadioSetting("exset.ex012", sx, rx)
+            if mx == 0:
+                mena.append(rset)
+            else:
+                menb.append(rset)
+
+            sx = my_labels(13)
+            rx = RadioSettingValueInteger(0, 9, _mex[mx].ex013)
+            rset = RadioSetting("exset.ex013", sx, rx)
+            if mx == 0:
+                mena.append(rset)
+            else:
+                menb.append(rset)
+
+            sx = my_labels(14)
+            rx = RadioSettingValueInteger(0, 9, _mex[mx].ex014)
+            rset = RadioSetting("exset.ex014", sx, rx)
+            if mx == 0:
+                mena.append(rset)
+            else:
+                menb.append(rset)
+
+            options = ["250", "500", "1000"]
+            rx = RadioSettingValueList(options, options[_mex[mx].ex003])
+            sx = my_labels(3)
+            rset = RadioSetting("exset.ex003/%d" % mx, sx, rx)
+            rset.set_apply_callback(my_val_list, options, _mex,
+                                    "ex003", 0, mx)
+            if mx == 0:
+                mena.append(rset)
+            else:
+                menb.append(rset)
+
+            rx = RadioSettingValueBoolean(bool(_mex[mx].ex007))
+            sx = my_labels(7)
+            rset = RadioSetting("exset.ex007/%d" % mx, sx, rx)
+            rset.set_apply_callback(my_bool, _mex, "ex007", mx)
+            if mx == 0:
+                mena.append(rset)
+            else:
+                menb.append(rset)
+
+            rx = RadioSettingValueBoolean(bool(_mex[mx].ex008))
+            sx = my_labels(8)
+            rset = RadioSetting("exset.ex008/%d" % mx, sx, rx)
+            rset.set_apply_callback(my_bool, _mex, "ex008", mx)
+            if mx == 0:
+                mena.append(rset)
+            else:
+                menb.append(rset)
+
+            options = ["100", "200", "300", "400", "500"]
+            rx = RadioSettingValueList(options, options[_mex[mx].ex009])
+            sx = my_labels(9)
+            rset = RadioSetting("exset.ex009/%d" % mx, sx, rx)
+            rset.set_apply_callback(my_val_list, options, _mex,
+                                    "ex009", 0, mx)
+            if mx == 0:
+                mena.append(rset)
+            else:
+                menb.append(rset)
+
+            rx = RadioSettingValueBoolean(bool(_mex[mx].ex010))
+            sx = my_labels(10)
+            rset = RadioSetting("exset.ex010/%d" % mx, sx, rx)
+            rset.set_apply_callback(my_bool, _mex, "ex010", mx)
+            if mx == 0:
+                mena.append(rset)
+            else:
+                menb.append(rset)
+
+            options = ["TO", "CO"]
+            rx = RadioSettingValueList(options, options[_mex[mx].ex011])
+            sx = my_labels(11)
+            rset = RadioSetting("exset.ex011/%d" % mx, sx, rx)
+            rset.set_apply_callback(my_val_list, options, _mex,
+                                    "ex011", 0, mx)
+            if mx == 0:
+                mena.append(rset)
+            else:
+                menb.append(rset)
+
+            rx = RadioSettingValueBoolean(bool(_mex[mx].ex021))
+            sx = my_labels(21)
+            rset = RadioSetting("exset.ex021/%d" % mx, sx, rx)
+            rset.set_apply_callback(my_bool, _mex, "ex021", mx)
+            if mx == 0:
+                mena.append(rset)
+            else:
+                menb.append(rset)
+
+            options = ["Off", "3", "5", "10", "20", "30"]
+            rx = RadioSettingValueList(options, options[_mex[mx].ex022])
+            sx = my_labels(22)
+            rset = RadioSetting("exset.ex022/%d" % mx, sx, rx)
+            rset.set_apply_callback(my_val_list, options, _mex,
+                                    "ex022", 0, mx)
+            if mx == 0:
+                mena.append(rset)
+            else:
+                menb.append(rset)
+
+            rx = RadioSettingValueInteger(0, 99, _mex[mx].ex048)
+            sx = my_labels(48)
+            rset = RadioSetting("exset.ex048/%d" % mx, sx, rx)
+            rset.set_apply_callback(my_fnctns, _mex, mx, "ex048")
+            if mx == 0:
+                mena.append(rset)
+            else:
+                menb.append(rset)
+
+            rx = RadioSettingValueInteger(0, 99, _mex[mx].ex049)
+            sx = my_labels(49)
+            rset = RadioSetting("exset.ex049/%d" % mx, sx, rx)
+            rset.set_apply_callback(my_fnctns, _mex, mx, "ex049")
+            if mx == 0:
+                mena.append(rset)
+            else:
+                menb.append(rset)
+
+            rx = RadioSettingValueInteger(0, 99, _mex[mx].ex050)
+            sx = my_labels(50)
+            rset = RadioSetting("exset.ex050/%d" % mx, sx, rx)
+            rset.set_apply_callback(my_fnctns, _mex, mx, "ex050")
+            if mx == 0:
+                mena.append(rset)
+            else:
+                menb.append(rset)
+
+            rx = RadioSettingValueInteger(0, 99, _mex[mx].ex051)
+            sx = my_labels(51)
+            rset = RadioSetting("exset.ex051/%d" % mx, sx, rx)
+            rset.set_apply_callback(my_fnctns, _mex, mx, "ex051")
+            if mx == 0:
+                mena.append(rset)
+            else:
+                menb.append(rset)
+
+            rx = RadioSettingValueInteger(0, 99, _mex[mx].ex052)
+            sx = my_labels(52)
+            rset = RadioSetting("exset.ex052/%d" % mx, sx, rx)
+            rset.set_apply_callback(my_fnctns, _mex, mx, "ex052")
+            if mx == 0:
+                mena.append(rset)
+            else:
+                menb.append(rset)
+        # End of for mx loop
+
+        # ==== Auto Scan Params (amode) ==============
+        for ix in range(32):
+            val = _asf[ix].asfreq / mhz1
+            rx = RadioSettingValueFloat(0.03, 60.0, val, 0.001, 3)
+            rset = RadioSetting("asf.asfreq/%d" % ix,
+                                "Scan %02i Freq (MHz)" % ix, rx)
+            rset.set_apply_callback(my_mhz_val, _asf, "asfreq", ix)
+            amode.append(rset)
+
+            mx = _asf[ix].asmode - 1     # Same logic as xmode
+            if _asf[ix].asmode == 9:
+                mx = 7
+            rx = RadioSettingValueList(TS480_MODES, TS480_MODES[mx])
+            rset = RadioSetting("asf.asmode/%d" % ix, "   Mode", rx)
+            rset.set_apply_callback(my_asf_mode, _asf, ix)
+            amode.append(rset)
+
+        # ==== Slow Scan Settings ===
+        for ix in range(10):        # Chans
+            for nx in range(5):     # spots
+                px = ((ix * 5) + nx)
+                val = _ssf[px].ssfreq / mhz1
+                stx = "      -   -   -    Slot %02i Freq (MHz)" % nx
+                if nx == 0:
+                    stx = "Slow Scan %02i, Slot 0 Freq (MHz" % ix
+                rx = RadioSettingValueFloat(0, 54.0, val, 0.001, 3)
+                rset = RadioSetting("ssf.ssfreq/%d" % px, stx, rx)
+                rset.set_apply_callback(my_mhz_val, _ssf, "ssfreq", px)
+                ssc.append(rset)
+
+        return group       # END get_settings()
+
+    def set_settings(self, settings):
+        _settings = self._memobj.settings
+        _mem = self._memobj
+        for element in settings:
+            if not isinstance(element, RadioSetting):
+                self.set_settings(element)
+                continue
+            else:
+                try:
+                    name = element.get_name()
+                    if "." in name:
+                        bits = name.split(".")
+                        obj = self._memobj
+                        for bit in bits[:-1]:
+                            if "/" in bit:
+                                bit, index = bit.split("/", 1)
+                                index = int(index)
+                                obj = getattr(obj, bit)[index]
+                            else:
+                                obj = getattr(obj, bit)
+                        setting = bits[-1]
+                    else:
+                        obj = _settings
+                        setting = element.get_name()
+
+                    if element.has_apply_callback():
+                        LOG.debug("Using apply callback")
+                        element.run_apply_callback()
+                    elif element.value.get_mutable():
+                        LOG.debug("Setting %s = %s" % (setting, element.value))
+                        setattr(obj, setting, element.value)
+                except Exception, e:
+                    LOG.debug(element.get_name())
+                    raise
+        return
diff -r 067b69c9b7d0 -r 6e9d8a59f376 tools/cpep8.manifest
--- a/tools/cpep8.manifest	Fri Nov 08 20:33:33 2019 -0500
+++ b/tools/cpep8.manifest	Sun Nov 10 09:28:01 2019 -0800
@@ -83,6 +83,8 @@
 ./chirp/drivers/tk8180.py
 ./chirp/drivers/tmv71.py
 ./chirp/drivers/tmv71_ll.py
+./chirp/drivers/ts480.py
+./chirp/drivers/ts590.py
 ./chirp/drivers/uv5r.py
 ./chirp/drivers/uvb5.py
 ./chirp/drivers/vx170.py
-------------- next part --------------
./chirp/__init__.py
./chirp/bandplan.py
./chirp/bandplan_au.py
./chirp/bandplan_iaru_r1.py
./chirp/bandplan_iaru_r2.py
./chirp/bandplan_iaru_r3.py
./chirp/bandplan_na.py
./chirp/bitwise.py
./chirp/bitwise_grammar.py
./chirp/chirp_common.py
./chirp/detect.py
./chirp/directory.py
./chirp/drivers/__init__.py
./chirp/drivers/alinco.py
./chirp/drivers/anytone.py
./chirp/drivers/ap510.py
./chirp/drivers/baofeng_uv3r.py
./chirp/drivers/baofeng_wp970i.py
./chirp/drivers/bjuv55.py
./chirp/drivers/btech.py
./chirp/drivers/ft1802.py
./chirp/drivers/ft1d.py
./chirp/drivers/ft2800.py
./chirp/drivers/ft2900.py
./chirp/drivers/ft4.py
./chirp/drivers/ft50.py
./chirp/drivers/ft60.py
./chirp/drivers/ft7100.py
./chirp/drivers/ft7800.py
./chirp/drivers/ft817.py
./chirp/drivers/ft818.py
./chirp/drivers/ft857.py
./chirp/drivers/ft90.py
./chirp/drivers/ftm350.py
./chirp/drivers/generic_csv.py
./chirp/drivers/generic_tpe.py
./chirp/drivers/generic_xml.py
./chirp/drivers/h777.py
./chirp/drivers/ic208.py
./chirp/drivers/ic2100.py
./chirp/drivers/ic2200.py
./chirp/drivers/ic2720.py
./chirp/drivers/ic2730.py
./chirp/drivers/ic2820.py
./chirp/drivers/ic9x.py
./chirp/drivers/ic9x_icf.py
./chirp/drivers/ic9x_icf_ll.py
./chirp/drivers/ic9x_ll.py
./chirp/drivers/icf.py
./chirp/drivers/icomciv.py
./chirp/drivers/icq7.py
./chirp/drivers/ict70.py
./chirp/drivers/ict7h.py
./chirp/drivers/ict8.py
./chirp/drivers/icw32.py
./chirp/drivers/icx8x.py
./chirp/drivers/icx8x_ll.py
./chirp/drivers/id31.py
./chirp/drivers/id51.py
./chirp/drivers/id800.py
./chirp/drivers/id880.py
./chirp/drivers/idrp.py
./chirp/drivers/kenwood_hmk.py
./chirp/drivers/kenwood_itm.py
./chirp/drivers/kenwood_live.py
./chirp/drivers/kguv8d.py
./chirp/drivers/kguv9dplus.py
./chirp/drivers/kyd.py
./chirp/drivers/leixen.py
./chirp/drivers/puxing.py
./chirp/drivers/radioddity_r2.py
./chirp/drivers/rfinder.py
./chirp/drivers/template.py
./chirp/drivers/th350.py
./chirp/drivers/th9800.py
./chirp/drivers/th_uv3r.py
./chirp/drivers/th_uv3r25.py
./chirp/drivers/th_uv8000.py
./chirp/drivers/th_uvf8d.py
./chirp/drivers/thd72.py
./chirp/drivers/thuv1f.py
./chirp/drivers/tk8102.py
./chirp/drivers/tk8180.py
./chirp/drivers/tmv71.py
./chirp/drivers/tmv71_ll.py
./chirp/drivers/ts480.py
./chirp/drivers/ts590.py
./chirp/drivers/uv5r.py
./chirp/drivers/uvb5.py
./chirp/drivers/vx170.py
./chirp/drivers/vx2.py
./chirp/drivers/vx3.py
./chirp/drivers/vx5.py
./chirp/drivers/vx510.py
./chirp/drivers/vx6.py
./chirp/drivers/vx7.py
./chirp/drivers/vx8.py
./chirp/drivers/vxa700.py
./chirp/drivers/wouxun.py
./chirp/drivers/wouxun_common.py
./chirp/drivers/yaesu_clone.py
./chirp/elib_intl.py
./chirp/errors.py
./chirp/import_logic.py
./chirp/logger.py
./chirp/memmap.py
./chirp/platform.py
./chirp/pyPEG.py
./chirp/radioreference.py
./chirp/settings.py
./chirp/ui/__init__.py
./chirp/ui/bandplans.py
./chirp/ui/bankedit.py
./chirp/ui/clone.py
./chirp/ui/cloneprog.py
./chirp/ui/common.py
./chirp/ui/config.py
./chirp/ui/dstaredit.py
./chirp/ui/editorset.py
./chirp/ui/fips.py
./chirp/ui/importdialog.py
./chirp/ui/inputdialog.py
./chirp/ui/mainapp.py
./chirp/ui/memdetail.py
./chirp/ui/memedit.py
./chirp/ui/miscwidgets.py
./chirp/ui/radiobrowser.py
./chirp/ui/reporting.py
./chirp/ui/settingsedit.py
./chirp/ui/shiftdialog.py
./chirp/util.py
./chirp/xml_ll.py
./chirpc
./chirpw
./locale/check_parameters.py
./rpttool
./setup.py
./share/make_supported.py
./tests/__init__.py
./tests/run_tests
./tests/unit/__init__.py
./tests/unit/base.py
./tests/unit/test_bitwise.py
./tests/unit/test_chirp_common.py
./tests/unit/test_import_logic.py
./tests/unit/test_mappingmodel.py
./tests/unit/test_memedit_edits.py
./tests/unit/test_platform.py
./tests/unit/test_settings.py
./tests/unit/test_shiftdialog.py
./tools/bitdiff.py
./tools/cpep8.py
./tools/img2thd72.py
-------------- next part --------------
A non-text attachment was scrubbed...
Name: Kenwood_TS-590SG_CloneMode.img
Type: application/octet-stream
Size: 3090 bytes
Desc: not available
Url : http://intrepid.danplanet.com/pipermail/chirp_devel/attachments/20191110/ad0478f4/attachment-0002.img 
-------------- next part --------------
A non-text attachment was scrubbed...
Name: Kenwood_TS-480_CloneMode.img
Type: application/octet-stream
Size: 3018 bytes
Desc: not available
Url : http://intrepid.danplanet.com/pipermail/chirp_devel/attachments/20191110/ad0478f4/attachment-0003.img 


More information about the chirp_devel mailing list