[chirp_devel] Information Prompt Patch

Rick DeWitt
Wed Jun 20 05:55:13 PDT 2018


Oops...
And Nope, not familiar with either one; all my s/w change tracking 
experience has been with in-house methods.

Rick DeWitt
AA0RD
Sequim, Washington, USA
360-681-3494

On 6/19/2018 4:24 PM, Dan Smith via chirp_devel wrote:
>> OK, mercurial has been giving me fits trying to split these changesets
>> out. So here is a combined patch with the diffs for chirp_common,
>> mainapp and the lt725uv driver. All in support of issue #5889, adding
>> the information prompt at download.
> I think you forgot to attach it...
>
>> I'm still trying to get the ft450d driver change (adding the info prompt
>> verbage) to process, keep getting -
>> "abort: working directory revision is not qtip" from the "hg qnew -ef
>> ft450d_info" cmd.
> This looks like you maybe have moved things around since your last q-operation, like maybe if you did a commit, rollback or checkout. Does that make sense?
>
> I was asking you on the other thread and never got a response, but ... are you more familiar with git than mercurial? I got the impression that you weren't familiar with either, but was hoping to use you as a data point either way.
>
> Thanks!
>
> --Dan
> _______________________________________________
> chirp_devel mailing list
> chirp_devel at intrepid.danplanet.com
> http://intrepid.danplanet.com/mailman/listinfo/chirp_devel
> Developer docs: http://chirp.danplanet.com/projects/chirp/wiki/Developers

-------------- next part --------------
# HG changeset patch
# User Rick DeWitt <aa0rd at yahoo.com>
# Date 1529443645 25200
#      Tue Jun 19 14:27:25 2018 -0700
# Node ID ed11ed06d671398c9e281b230a73f4c582e6de89
# Parent  b08fbd75a49999f7f8054a054d7ae6a8cc45c684
[chirp_common, mainapp, lt725uv] Add support for new "Info" prompt per issue #5889

diff -r b08fbd75a499 -r ed11ed06d671 chirp/chirp_common.py
--- a/chirp/chirp_common.py	Wed Jun 13 06:14:11 2018 -0700
+++ b/chirp/chirp_common.py	Tue Jun 19 14:27:25 2018 -0700
@@ -1,1485 +1,1487 @@
-# Copyright 2008 Dan Smith <dsmith at danplanet.com>
-#
-# This program is free software: you can redistribute it and/or modify
-# it under the terms of the GNU General Public License as published by
-# the Free Software Foundation, either version 3 of the License, or
-# (at your option) any later version.
-#
-# This program is distributed in the hope that it will be useful,
-# but WITHOUT ANY WARRANTY; without even the implied warranty of
-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
-# GNU General Public License for more details.
-#
-# You should have received a copy of the GNU General Public License
-# along with this program.  If not, see <http://www.gnu.org/licenses/>.
-
-import math
-from chirp import errors, memmap
-
-SEPCHAR = ","
-
-# 50 Tones
-TONES = [67.0, 69.3, 71.9, 74.4, 77.0, 79.7, 82.5,
-         85.4, 88.5, 91.5, 94.8, 97.4, 100.0, 103.5,
-         107.2, 110.9, 114.8, 118.8, 123.0, 127.3,
-         131.8, 136.5, 141.3, 146.2, 151.4, 156.7,
-         159.8, 162.2, 165.5, 167.9, 171.3, 173.8,
-         177.3, 179.9, 183.5, 186.2, 189.9, 192.8,
-         196.6, 199.5, 203.5, 206.5, 210.7, 218.1,
-         225.7, 229.1, 233.6, 241.8, 250.3, 254.1,
-         ]
-
-TONES_EXTRA = [56.0, 57.0, 58.0, 59.0, 60.0, 61.0, 62.0,
-               62.5, 63.0, 64.0]
-
-OLD_TONES = list(TONES)
-[OLD_TONES.remove(x) for x in [159.8, 165.5, 171.3, 177.3, 183.5, 189.9,
-                               196.6, 199.5, 206.5, 229.1, 254.1]]
-
-# 104 DTCS Codes
-DTCS_CODES = [
-    23,  25,  26,  31,  32,  36,  43,  47,  51,  53,  54,
-    65,  71,  72,  73,  74,  114, 115, 116, 122, 125, 131,
-    132, 134, 143, 145, 152, 155, 156, 162, 165, 172, 174,
-    205, 212, 223, 225, 226, 243, 244, 245, 246, 251, 252,
-    255, 261, 263, 265, 266, 271, 274, 306, 311, 315, 325,
-    331, 332, 343, 346, 351, 356, 364, 365, 371, 411, 412,
-    413, 423, 431, 432, 445, 446, 452, 454, 455, 462, 464,
-    465, 466, 503, 506, 516, 523, 526, 532, 546, 565, 606,
-    612, 624, 627, 631, 632, 654, 662, 664, 703, 712, 723,
-    731, 732, 734, 743, 754,
-]
-
-# 512 Possible DTCS Codes
-ALL_DTCS_CODES = []
-for a in range(0, 8):
-    for b in range(0, 8):
-        for c in range(0, 8):
-            ALL_DTCS_CODES.append((a * 100) + (b * 10) + c)
-
-CROSS_MODES = [
-    "Tone->Tone",
-    "DTCS->",
-    "->DTCS",
-    "Tone->DTCS",
-    "DTCS->Tone",
-    "->Tone",
-    "DTCS->DTCS",
-    "Tone->"
-]
-
-MODES = ["WFM", "FM", "NFM", "AM", "NAM", "DV", "USB", "LSB", "CW", "RTTY",
-         "DIG", "PKT", "NCW", "NCWR", "CWR", "P25", "Auto", "RTTYR",
-         "FSK", "FSKR", "DMR"]
-
-TONE_MODES = [
-    "",
-    "Tone",
-    "TSQL",
-    "DTCS",
-    "DTCS-R",
-    "TSQL-R",
-    "Cross",
-]
-
-TUNING_STEPS = [
-    5.0, 6.25, 10.0, 12.5, 15.0, 20.0, 25.0, 30.0, 50.0, 100.0,
-    125.0, 200.0,
-    # Need to fix drivers using this list as an index!
-    9.0, 1.0, 2.5,
-]
-
-SKIP_VALUES = ["", "S", "P"]
-
-CHARSET_UPPER_NUMERIC = "ABCDEFGHIJKLMNOPQRSTUVWXYZ 1234567890"
-CHARSET_ALPHANUMERIC = \
-    "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz 1234567890"
-CHARSET_ASCII = "".join([chr(x) for x in range(ord(" "), ord("~") + 1)])
-
-# http://aprs.org/aprs11/SSIDs.txt
-APRS_SSID = (
-    "0 Your primary station usually fixed and message capable",
-    "1 generic additional station, digi, mobile, wx, etc",
-    "2 generic additional station, digi, mobile, wx, etc",
-    "3 generic additional station, digi, mobile, wx, etc",
-    "4 generic additional station, digi, mobile, wx, etc",
-    "5 Other networks (Dstar, Iphones, Androids, Blackberry's etc)",
-    "6 Special activity, Satellite ops, camping or 6 meters, etc",
-    "7 walkie talkies, HT's or other human portable",
-    "8 boats, sailboats, RV's or second main mobile",
-    "9 Primary Mobile (usually message capable)",
-    "10 internet, Igates, echolink, winlink, AVRS, APRN, etc",
-    "11 balloons, aircraft, spacecraft, etc",
-    "12 APRStt, DTMF, RFID, devices, one-way trackers*, etc",
-    "13 Weather stations",
-    "14 Truckers or generally full time drivers",
-    "15 generic additional station, digi, mobile, wx, etc")
-APRS_POSITION_COMMENT = (
-    "off duty", "en route", "in service", "returning", "committed",
-    "special", "priority", "custom 0", "custom 1", "custom 2", "custom 3",
-    "custom 4", "custom 5", "custom 6", "EMERGENCY")
-# http://aprs.org/symbols/symbolsX.txt
-APRS_SYMBOLS = (
-    "Police/Sheriff", "[reserved]", "Digi", "Phone", "DX Cluster",
-    "HF Gateway", "Small Aircraft", "Mobile Satellite Groundstation",
-    "Wheelchair", "Snowmobile", "Red Cross", "Boy Scouts", "House QTH (VHF)",
-    "X", "Red Dot", "0 in Circle", "1 in Circle", "2 in Circle",
-    "3 in Circle", "4 in Circle", "5 in Circle", "6 in Circle", "7 in Circle",
-    "8 in Circle", "9 in Circle", "Fire", "Campground", "Motorcycle",
-    "Railroad Engine", "Car", "File Server", "Hurricane Future Prediction",
-    "Aid Station", "BBS or PBBS", "Canoe", "[reserved]", "Eyeball",
-    "Tractor/Farm Vehicle", "Grid Square", "Hotel", "TCP/IP", "[reserved]",
-    "School", "PC User", "MacAPRS", "NTS Station", "Balloon", "Police", "TBD",
-    "Recreational Vehicle", "Space Shuttle", "SSTV", "Bus", "ATV",
-    "National WX Service Site", "Helicopter", "Yacht/Sail Boat", "WinAPRS",
-    "Human/Person", "Triangle", "Mail/Postoffice", "Large Aircraft",
-    "WX Station", "Dish Antenna", "Ambulance", "Bicycle",
-    "Incident Command Post", "Dual Garage/Fire Dept", "Horse/Equestrian",
-    "Fire Truck", "Glider", "Hospital", "IOTA", "Jeep", "Truck", "Laptop",
-    "Mic-Repeater", "Node", "Emergency Operations Center", "Rover (dog)",
-    "Grid Square above 128m", "Repeater", "Ship/Power Boat", "Truck Stop",
-    "Truck (18 wheeler)", "Van", "Water Station", "X-APRS", "Yagi at QTH",
-    "TDB", "[reserved]"
-)
-
-
-def watts_to_dBm(watts):
-    """Converts @watts in watts to dBm"""
-    return int(10 * math.log10(int(watts * 1000)))
-
-
-def dBm_to_watts(dBm):
-    """Converts @dBm from dBm to watts"""
-    return int(math.pow(10, (dBm - 30) / 10))
-
-
-class PowerLevel:
-    """Represents a power level supported by a radio"""
-
-    def __init__(self, label, watts=0, dBm=0):
-        if watts:
-            dBm = watts_to_dBm(watts)
-        self._power = int(dBm)
-        self._label = label
-
-    def __str__(self):
-        return str(self._label)
-
-    def __int__(self):
-        return self._power
-
-    def __sub__(self, val):
-        return int(self) - int(val)
-
-    def __add__(self, val):
-        return int(self) + int(val)
-
-    def __eq__(self, val):
-        if val is not None:
-            return int(self) == int(val)
-        return False
-
-    def __lt__(self, val):
-        return int(self) < int(val)
-
-    def __gt__(self, val):
-        return int(self) > int(val)
-
-    def __nonzero__(self):
-        return int(self) != 0
-
-    def __repr__(self):
-        return "%s (%i dBm)" % (self._label, self._power)
-
-
-def parse_freq(freqstr):
-    """Parse a frequency string and return the value in integral Hz"""
-    freqstr = freqstr.strip()
-    if freqstr == "":
-        return 0
-    elif freqstr.endswith(" MHz"):
-        return parse_freq(freqstr.split(" ")[0])
-    elif freqstr.endswith(" kHz"):
-        return int(freqstr.split(" ")[0]) * 1000
-
-    if "." in freqstr:
-        mhz, khz = freqstr.split(".")
-        if mhz == "":
-            mhz = 0
-        khz = khz.ljust(6, "0")
-        if len(khz) > 6:
-            raise ValueError("Invalid kHz value: %s", khz)
-        mhz = int(mhz) * 1000000
-        khz = int(khz)
-    else:
-        mhz = int(freqstr) * 1000000
-        khz = 0
-
-    return mhz + khz
-
-
-def format_freq(freq):
-    """Format a frequency given in Hz as a string"""
-
-    return "%i.%06i" % (freq / 1000000, freq % 1000000)
-
-
-class ImmutableValueError(ValueError):
-    pass
-
-
-class Memory:
-    """Base class for a single radio memory"""
-    freq = 0
-    number = 0
-    extd_number = ""
-    name = ""
-    vfo = 0
-    rtone = 88.5
-    ctone = 88.5
-    dtcs = 23
-    rx_dtcs = 23
-    tmode = ""
-    cross_mode = "Tone->Tone"
-    dtcs_polarity = "NN"
-    skip = ""
-    power = None
-    duplex = ""
-    offset = 600000
-    mode = "FM"
-    tuning_step = 5.0
-
-    comment = ""
-
-    empty = False
-
-    immutable = []
-
-    # A RadioSettingGroup of additional settings supported by the radio,
-    # or an empty list if none
-    extra = []
-
-    def __init__(self):
-        self.freq = 0
-        self.number = 0
-        self.extd_number = ""
-        self.name = ""
-        self.vfo = 0
-        self.rtone = 88.5
-        self.ctone = 88.5
-        self.dtcs = 23
-        self.rx_dtcs = 23
-        self.tmode = ""
-        self.cross_mode = "Tone->Tone"
-        self.dtcs_polarity = "NN"
-        self.skip = ""
-        self.power = None
-        self.duplex = ""
-        self.offset = 600000
-        self.mode = "FM"
-        self.tuning_step = 5.0
-
-        self.comment = ""
-
-        self.empty = False
-
-        self.immutable = []
-
-    _valid_map = {
-        "rtone":          TONES + TONES_EXTRA,
-        "ctone":          TONES + TONES_EXTRA,
-        "dtcs":           ALL_DTCS_CODES,
-        "rx_dtcs":        ALL_DTCS_CODES,
-        "tmode":          TONE_MODES,
-        "dtcs_polarity":  ["NN", "NR", "RN", "RR"],
-        "cross_mode":     CROSS_MODES,
-        "mode":           MODES,
-        "duplex":         ["", "+", "-", "split", "off"],
-        "skip":           SKIP_VALUES,
-        "empty":          [True, False],
-        "dv_code":        [x for x in range(0, 100)],
-    }
-
-    def __repr__(self):
-        return "Memory[%i]" % self.number
-
-    def dupe(self):
-        """Return a deep copy of @self"""
-        mem = self.__class__()
-        for k, v in self.__dict__.items():
-            mem.__dict__[k] = v
-
-        return mem
-
-    def clone(self, source):
-        """Absorb all of the properties of @source"""
-        for k, v in source.__dict__.items():
-            self.__dict__[k] = v
-
-    CSV_FORMAT = ["Location", "Name", "Frequency",
-                  "Duplex", "Offset", "Tone",
-                  "rToneFreq", "cToneFreq", "DtcsCode",
-                  "DtcsPolarity", "Mode", "TStep",
-                  "Skip", "Comment",
-                  "URCALL", "RPT1CALL", "RPT2CALL", "DVCODE"]
-
-    def __setattr__(self, name, val):
-        if not hasattr(self, name):
-            raise ValueError("No such attribute `%s'" % name)
-
-        if name in self.immutable:
-            raise ImmutableValueError("Field %s is not " % name +
-                                      "mutable on this memory")
-
-        if name in self._valid_map and val not in self._valid_map[name]:
-            raise ValueError("`%s' is not in valid list: %s" %
-                             (val, self._valid_map[name]))
-
-        self.__dict__[name] = val
-
-    def format_freq(self):
-        """Return a properly-formatted string of this memory's frequency"""
-        return format_freq(self.freq)
-
-    def parse_freq(self, freqstr):
-        """Set the frequency from a string"""
-        self.freq = parse_freq(freqstr)
-        return self.freq
-
-    def __str__(self):
-        if self.tmode == "Tone":
-            tenc = "*"
-        else:
-            tenc = " "
-
-        if self.tmode == "TSQL":
-            tsql = "*"
-        else:
-            tsql = " "
-
-        if self.tmode == "DTCS":
-            dtcs = "*"
-        else:
-            dtcs = " "
-
-        if self.duplex == "":
-            dup = "/"
-        else:
-            dup = self.duplex
-
-        return \
-            "Memory %s: %s%s%s %s (%s) r%.1f%s c%.1f%s d%03i%s%s [%.2f]" % \
-            (self.number if self.extd_number == "" else self.extd_number,
-             format_freq(self.freq),
-             dup,
-             format_freq(self.offset),
-             self.mode,
-             self.name,
-             self.rtone,
-             tenc,
-             self.ctone,
-             tsql,
-             self.dtcs,
-             dtcs,
-             self.dtcs_polarity,
-             self.tuning_step)
-
-    def to_csv(self):
-        """Return a CSV representation of this memory"""
-        return [
-            "%i" % self.number,
-            "%s" % self.name,
-            format_freq(self.freq),
-            "%s" % self.duplex,
-            format_freq(self.offset),
-            "%s" % self.tmode,
-            "%.1f" % self.rtone,
-            "%.1f" % self.ctone,
-            "%03i" % self.dtcs,
-            "%s" % self.dtcs_polarity,
-            "%s" % self.mode,
-            "%.2f" % self.tuning_step,
-            "%s" % self.skip,
-            "%s" % self.comment,
-            "", "", "", ""]
-
-    @classmethod
-    def _from_csv(cls, _line):
-        line = _line.strip()
-        if line.startswith("Location"):
-            raise errors.InvalidMemoryLocation("Non-CSV line")
-
-        vals = line.split(SEPCHAR)
-        if len(vals) < 11:
-            raise errors.InvalidDataError("CSV format error " +
-                                          "(14 columns expected)")
-
-        if vals[10] == "DV":
-            mem = DVMemory()
-        else:
-            mem = Memory()
-
-        mem.really_from_csv(vals)
-        return mem
-
-    def really_from_csv(self, vals):
-        """Careful parsing of split-out @vals"""
-        try:
-            self.number = int(vals[0])
-        except:
-            raise errors.InvalidDataError(
-                "Location '%s' is not a valid integer" % vals[0])
-
-        self.name = vals[1]
-
-        try:
-            self.freq = float(vals[2])
-        except:
-            raise errors.InvalidDataError("Frequency is not a valid number")
-
-        if vals[3].strip() in ["+", "-", ""]:
-            self.duplex = vals[3].strip()
-        else:
-            raise errors.InvalidDataError("Duplex is not +,-, or empty")
-
-        try:
-            self.offset = float(vals[4])
-        except:
-            raise errors.InvalidDataError("Offset is not a valid number")
-
-        self.tmode = vals[5]
-        if self.tmode not in TONE_MODES:
-            raise errors.InvalidDataError("Invalid tone mode `%s'" %
-                                          self.tmode)
-
-        try:
-            self.rtone = float(vals[6])
-        except:
-            raise errors.InvalidDataError("rTone is not a valid number")
-        if self.rtone not in TONES:
-            raise errors.InvalidDataError("rTone is not valid")
-
-        try:
-            self.ctone = float(vals[7])
-        except:
-            raise errors.InvalidDataError("cTone is not a valid number")
-        if self.ctone not in TONES:
-            raise errors.InvalidDataError("cTone is not valid")
-
-        try:
-            self.dtcs = int(vals[8], 10)
-        except:
-            raise errors.InvalidDataError("DTCS code is not a valid number")
-        if self.dtcs not in DTCS_CODES:
-            raise errors.InvalidDataError("DTCS code is not valid")
-
-        try:
-            self.rx_dtcs = int(vals[8], 10)
-        except:
-            raise errors.InvalidDataError("DTCS Rx code is not a valid number")
-        if self.rx_dtcs not in DTCS_CODES:
-            raise errors.InvalidDataError("DTCS Rx code is not valid")
-
-        if vals[9] in ["NN", "NR", "RN", "RR"]:
-            self.dtcs_polarity = vals[9]
-        else:
-            raise errors.InvalidDataError("DtcsPolarity is not valid")
-
-        if vals[10] in MODES:
-            self.mode = vals[10]
-        else:
-            raise errors.InvalidDataError("Mode is not valid")
-
-        try:
-            self.tuning_step = float(vals[11])
-        except:
-            raise errors.InvalidDataError("Tuning step is invalid")
-
-        try:
-            self.skip = vals[12]
-        except:
-            raise errors.InvalidDataError("Skip value is not valid")
-
-        return True
-
-
-class DVMemory(Memory):
-    """A Memory with D-STAR attributes"""
-    dv_urcall = "CQCQCQ"
-    dv_rpt1call = ""
-    dv_rpt2call = ""
-    dv_code = 0
-
-    def __str__(self):
-        string = Memory.__str__(self)
-
-        string += " <%s,%s,%s>" % (self.dv_urcall,
-                                   self.dv_rpt1call,
-                                   self.dv_rpt2call)
-
-        return string
-
-    def to_csv(self):
-        return [
-            "%i" % self.number,
-            "%s" % self.name,
-            format_freq(self.freq),
-            "%s" % self.duplex,
-            format_freq(self.offset),
-            "%s" % self.tmode,
-            "%.1f" % self.rtone,
-            "%.1f" % self.ctone,
-            "%03i" % self.dtcs,
-            "%s" % self.dtcs_polarity,
-            "%s" % self.mode,
-            "%.2f" % self.tuning_step,
-            "%s" % self.skip,
-            "%s" % self.comment,
-            "%s" % self.dv_urcall,
-            "%s" % self.dv_rpt1call,
-            "%s" % self.dv_rpt2call,
-            "%i" % self.dv_code]
-
-    def really_from_csv(self, vals):
-        Memory.really_from_csv(self, vals)
-
-        self.dv_urcall = vals[15].rstrip()[:8]
-        self.dv_rpt1call = vals[16].rstrip()[:8]
-        self.dv_rpt2call = vals[17].rstrip()[:8]
-        try:
-            self.dv_code = int(vals[18].strip())
-        except Exception:
-            self.dv_code = 0
-
-
-class MemoryMapping(object):
-    """Base class for a memory mapping"""
-
-    def __init__(self, model, index, name):
-        self._model = model
-        self._index = index
-        self._name = name
-
-    def __str__(self):
-        return self.get_name()
-
-    def __repr__(self):
-        return "%s-%s" % (self.__class__.__name__, self._index)
-
-    def get_name(self):
-        """Returns the mapping name"""
-        return self._name
-
-    def get_index(self):
-        """Returns the immutable index (string or int)"""
-        return self._index
-
-    def __eq__(self, other):
-        return self.get_index() == other.get_index()
-
-
-class MappingModel(object):
-    """Base class for a memory mapping model"""
-
-    def __init__(self, radio, name):
-        self._radio = radio
-        self._name = name
-
-    def get_name(self):
-        return self._name
-
-    def get_num_mappings(self):
-        """Returns the number of mappings in the model (should be
-        callable without consulting the radio"""
-        raise NotImplementedError()
-
-    def get_mappings(self):
-        """Return a list of mappings"""
-        raise NotImplementedError()
-
-    def add_memory_to_mapping(self, memory, mapping):
-        """Add @memory to @mapping."""
-        raise NotImplementedError()
-
-    def remove_memory_from_mapping(self, memory, mapping):
-        """Remove @memory from @mapping.
-        Shall raise exception if @memory is not in @bank"""
-        raise NotImplementedError()
-
-    def get_mapping_memories(self, mapping):
-        """Return a list of memories in @mapping"""
-        raise NotImplementedError()
-
-    def get_memory_mappings(self, memory):
-        """Return a list of mappings that @memory is in"""
-        raise NotImplementedError()
-
-
-class Bank(MemoryMapping):
-    """Base class for a radio's Bank"""
-
-
-class NamedBank(Bank):
-    """A bank that can have a name"""
-
-    def set_name(self, name):
-        """Changes the user-adjustable bank name"""
-        self._name = name
-
-
-class BankModel(MappingModel):
-    """A bank model where one memory is in zero or one banks at any point"""
-
-    def __init__(self, radio, name='Banks'):
-        super(BankModel, self).__init__(radio, name)
-
-
-class MappingModelIndexInterface:
-    """Interface for mappings with index capabilities"""
-
-    def get_index_bounds(self):
-        """Returns a tuple (lo,hi) of the min and max mapping indices"""
-        raise NotImplementedError()
-
-    def get_memory_index(self, memory, mapping):
-        """Returns the index of @memory in @mapping"""
-        raise NotImplementedError()
-
-    def set_memory_index(self, memory, mapping, index):
-        """Sets the index of @memory in @mapping to @index"""
-        raise NotImplementedError()
-
-    def get_next_mapping_index(self, mapping):
-        """Returns the next available mapping index in @mapping, or raises
-        Exception if full"""
-        raise NotImplementedError()
-
-
-class MTOBankModel(BankModel):
-    """A bank model where one memory can be in multiple banks at once """
-    pass
-
-
-def console_status(status):
-    """Write a status object to the console"""
-    import logging
-    from chirp import logger
-    if not logger.is_visible(logging.WARN):
-        return
-    import sys
-    import os
-    sys.stdout.write("\r%s" % status)
-    if status.cur == status.max:
-        sys.stdout.write(os.linesep)
-
-
-class RadioPrompts:
-    """Radio prompt strings"""
-    experimental = None
-    pre_download = None
-    pre_upload = None
-    display_pre_upload_prompt_before_opening_port = True
-
-
-BOOLEAN = [True, False]
-
-
-class RadioFeatures:
-    """Radio Feature Flags"""
-    _valid_map = {
-        # General
-        "has_bank_index":       BOOLEAN,
-        "has_dtcs":             BOOLEAN,
-        "has_rx_dtcs":          BOOLEAN,
-        "has_dtcs_polarity":    BOOLEAN,
-        "has_mode":             BOOLEAN,
-        "has_offset":           BOOLEAN,
-        "has_name":             BOOLEAN,
-        "has_bank":             BOOLEAN,
-        "has_bank_names":       BOOLEAN,
-        "has_tuning_step":      BOOLEAN,
-        "has_ctone":            BOOLEAN,
-        "has_cross":            BOOLEAN,
-        "has_infinite_number":  BOOLEAN,
-        "has_nostep_tuning":    BOOLEAN,
-        "has_comment":          BOOLEAN,
-        "has_settings":         BOOLEAN,
-
-        # Attributes
-        "valid_modes":          [],
-        "valid_tmodes":         [],
-        "valid_duplexes":       [],
-        "valid_tuning_steps":   [],
-        "valid_bands":          [],
-        "valid_skips":          [],
-        "valid_power_levels":   [],
-        "valid_characters":     "",
-        "valid_name_length":    0,
-        "valid_cross_modes":    [],
-        "valid_dtcs_pols":      [],
-        "valid_dtcs_codes":     [],
-        "valid_special_chans":  [],
-
-        "has_sub_devices":      BOOLEAN,
-        "memory_bounds":        (0, 0),
-        "can_odd_split":        BOOLEAN,
-
-        # D-STAR
-        "requires_call_lists":  BOOLEAN,
-        "has_implicit_calls":   BOOLEAN,
-    }
-
-    def __setattr__(self, name, val):
-        if name.startswith("_"):
-            self.__dict__[name] = val
-            return
-        elif name not in self._valid_map.keys():
-            raise ValueError("No such attribute `%s'" % name)
-
-        if type(self._valid_map[name]) == tuple:
-            # Tuple, cardinality must match
-            if type(val) != tuple or len(val) != len(self._valid_map[name]):
-                raise ValueError("Invalid value `%s' for attribute `%s'" %
-                                 (val, name))
-        elif type(self._valid_map[name]) == list and not self._valid_map[name]:
-            # Empty list, must be another list
-            if type(val) != list:
-                raise ValueError("Invalid value `%s' for attribute `%s'" %
-                                 (val, name))
-        elif type(self._valid_map[name]) == str:
-            if type(val) != str:
-                raise ValueError("Invalid value `%s' for attribute `%s'" %
-                                 (val, name))
-        elif type(self._valid_map[name]) == int:
-            if type(val) != int:
-                raise ValueError("Invalid value `%s' for attribute `%s'" %
-                                 (val, name))
-        elif val not in self._valid_map[name]:
-            # Value not in the list of valid values
-            raise ValueError("Invalid value `%s' for attribute `%s'" % (val,
-                                                                        name))
-        self.__dict__[name] = val
-
-    def __getattr__(self, name):
-        raise AttributeError("pylint is confused by RadioFeatures")
-
-    def init(self, attribute, default, doc=None):
-        """Initialize a feature flag @attribute with default value @default,
-        and documentation string @doc"""
-        self.__setattr__(attribute, default)
-        self.__docs[attribute] = doc
-
-    def get_doc(self, attribute):
-        """Return the description of @attribute"""
-        return self.__docs[attribute]
-
-    def __init__(self):
-        self.__docs = {}
-        self.init("has_bank_index", False,
-                  "Indicates that memories in a bank can be stored in " +
-                  "an order other than in main memory")
-        self.init("has_dtcs", True,
-                  "Indicates that DTCS tone mode is available")
-        self.init("has_rx_dtcs", False,
-                  "Indicates that radio can use two different " +
-                  "DTCS codes for rx and tx")
-        self.init("has_dtcs_polarity", True,
-                  "Indicates that the DTCS polarity can be changed")
-        self.init("has_mode", True,
-                  "Indicates that multiple emission modes are supported")
-        self.init("has_offset", True,
-                  "Indicates that the TX offset memory property is supported")
-        self.init("has_name", True,
-                  "Indicates that an alphanumeric memory name is supported")
-        self.init("has_bank", True,
-                  "Indicates that memories may be placed into banks")
-        self.init("has_bank_names", False,
-                  "Indicates that banks may be named")
-        self.init("has_tuning_step", True,
-                  "Indicates that memories store their tuning step")
-        self.init("has_ctone", True,
-                  "Indicates that the radio keeps separate tone frequencies " +
-                  "for repeater and CTCSS operation")
-        self.init("has_cross", False,
-                  "Indicates that the radios supports different tone modes " +
-                  "on transmit and receive")
-        self.init("has_infinite_number", False,
-                  "Indicates that the radio is not constrained in the " +
-                  "number of memories that it can store")
-        self.init("has_nostep_tuning", False,
-                  "Indicates that the radio does not require a valid " +
-                  "tuning step to store a frequency")
-        self.init("has_comment", False,
-                  "Indicates that the radio supports storing a comment " +
-                  "with each memory")
-        self.init("has_settings", False,
-                  "Indicates that the radio supports general settings")
-
-        self.init("valid_modes", list(MODES),
-                  "Supported emission (or receive) modes")
-        self.init("valid_tmodes", [],
-                  "Supported tone squelch modes")
-        self.init("valid_duplexes", ["", "+", "-"],
-                  "Supported duplex modes")
-        self.init("valid_tuning_steps", list(TUNING_STEPS),
-                  "Supported tuning steps")
-        self.init("valid_bands", [],
-                  "Supported frequency ranges")
-        self.init("valid_skips", ["", "S"],
-                  "Supported memory scan skip settings")
-        self.init("valid_power_levels", [],
-                  "Supported power levels")
-        self.init("valid_characters", CHARSET_UPPER_NUMERIC,
-                  "Supported characters for a memory's alphanumeric tag")
-        self.init("valid_name_length", 6,
-                  "The maximum number of characters in a memory's " +
-                  "alphanumeric tag")
-        self.init("valid_cross_modes", list(CROSS_MODES),
-                  "Supported tone cross modes")
-        self.init("valid_dtcs_pols", ["NN", "RN", "NR", "RR"],
-                  "Supported DTCS polarities")
-        self.init("valid_dtcs_codes", list(DTCS_CODES),
-                  "Supported DTCS codes")
-        self.init("valid_special_chans", [],
-                  "Supported special channel names")
-
-        self.init("has_sub_devices", False,
-                  "Indicates that the radio behaves as two semi-independent " +
-                  "devices")
-        self.init("memory_bounds", (0, 1),
-                  "The minimum and maximum channel numbers")
-        self.init("can_odd_split", False,
-                  "Indicates that the radio can store an independent " +
-                  "transmit frequency")
-
-        self.init("requires_call_lists", True,
-                  "[D-STAR] Indicates that the radio requires all callsigns " +
-                  "to be in the master list and cannot be stored " +
-                  "arbitrarily in each memory channel")
-        self.init("has_implicit_calls", False,
-                  "[D-STAR] Indicates that the radio has an implied " +
-                  "callsign at the beginning of the master URCALL list")
-
-    def is_a_feature(self, name):
-        """Returns True if @name is a valid feature flag name"""
-        return name in self._valid_map.keys()
-
-    def __getitem__(self, name):
-        return self.__dict__[name]
-
-    def validate_memory(self, mem):
-        """Return a list of warnings and errors that will be encoundered
-        if trying to set @mem on the current radio"""
-        msgs = []
-
-        lo, hi = self.memory_bounds
-        if not self.has_infinite_number and \
-                (mem.number < lo or mem.number > hi) and \
-                mem.extd_number not in self.valid_special_chans:
-            msg = ValidationWarning("Location %i is out of range" % mem.number)
-            msgs.append(msg)
-
-        if (self.valid_modes and
-                mem.mode not in self.valid_modes and
-                mem.mode != "Auto"):
-            msg = ValidationError("Mode %s not supported" % mem.mode)
-            msgs.append(msg)
-
-        if self.valid_tmodes and mem.tmode not in self.valid_tmodes:
-            msg = ValidationError("Tone mode %s not supported" % mem.tmode)
-            msgs.append(msg)
-        else:
-            if mem.tmode == "Cross":
-                if self.valid_cross_modes and \
-                        mem.cross_mode not in self.valid_cross_modes:
-                    msg = ValidationError("Cross tone mode %s not supported" %
-                                          mem.cross_mode)
-                    msgs.append(msg)
-
-        if self.has_dtcs_polarity and \
-                mem.dtcs_polarity not in self.valid_dtcs_pols:
-            msg = ValidationError("DTCS Polarity %s not supported" %
-                                  mem.dtcs_polarity)
-            msgs.append(msg)
-
-        if self.valid_dtcs_codes and \
-                mem.dtcs not in self.valid_dtcs_codes:
-            msg = ValidationError("DTCS Code %03i not supported" % mem.dtcs)
-        if self.valid_dtcs_codes and \
-                mem.rx_dtcs not in self.valid_dtcs_codes:
-            msg = ValidationError("DTCS Code %03i not supported" % mem.rx_dtcs)
-
-        if self.valid_duplexes and mem.duplex not in self.valid_duplexes:
-            msg = ValidationError("Duplex %s not supported" % mem.duplex)
-            msgs.append(msg)
-
-        ts = mem.tuning_step
-        if self.valid_tuning_steps and ts not in self.valid_tuning_steps and \
-                not self.has_nostep_tuning:
-            msg = ValidationError("Tuning step %.2f not supported" % ts)
-            msgs.append(msg)
-
-        if self.valid_bands:
-            valid = False
-            for lo, hi in self.valid_bands:
-                if lo <= mem.freq < hi:
-                    valid = True
-                    break
-            if not valid:
-                msg = ValidationError(
-                    ("Frequency {freq} is out "
-                     "of supported range").format(freq=format_freq(mem.freq)))
-                msgs.append(msg)
-
-        if self.valid_bands and \
-                self.valid_duplexes and \
-                mem.duplex in ["split", "-", "+"]:
-            if mem.duplex == "split":
-                freq = mem.offset
-            elif mem.duplex == "-":
-                freq = mem.freq - mem.offset
-            elif mem.duplex == "+":
-                freq = mem.freq + mem.offset
-            valid = False
-            for lo, hi in self.valid_bands:
-                if lo <= freq < hi:
-                    valid = True
-                    break
-            if not valid:
-                msg = ValidationError(
-                    ("Tx freq {freq} is out "
-                     "of supported range").format(freq=format_freq(freq)))
-                msgs.append(msg)
-
-        if mem.power and \
-                self.valid_power_levels and \
-                mem.power not in self.valid_power_levels:
-            msg = ValidationWarning("Power level %s not supported" % mem.power)
-            msgs.append(msg)
-
-        if self.valid_tuning_steps and not self.has_nostep_tuning:
-            try:
-                step = required_step(mem.freq)
-                if step not in self.valid_tuning_steps:
-                    msg = ValidationError("Frequency requires %.2fkHz step" %
-                                          required_step(mem.freq))
-                    msgs.append(msg)
-            except errors.InvalidDataError, e:
-                msgs.append(str(e))
-
-        if self.valid_characters:
-            for char in mem.name:
-                if char not in self.valid_characters:
-                    msgs.append(ValidationWarning("Name character " +
-                                                  "`%s'" % char +
-                                                  " not supported"))
-                    break
-
-        return msgs
-
-
-class ValidationMessage(str):
-    """Base class for Validation Errors and Warnings"""
-    pass
-
-
-class ValidationWarning(ValidationMessage):
-    """A non-fatal warning during memory validation"""
-    pass
-
-
-class ValidationError(ValidationMessage):
-    """A fatal error during memory validation"""
-    pass
-
-
-class Alias(object):
-    VENDOR = "Unknown"
-    MODEL = "Unknown"
-    VARIANT = ""
-
-
-class Radio(Alias):
-    """Base class for all Radio drivers"""
-    BAUD_RATE = 9600
-    HARDWARE_FLOW = False
-    ALIASES = []
-
-    def status_fn(self, status):
-        """Deliver @status to the UI"""
-        console_status(status)
-
-    def __init__(self, pipe):
-        self.errors = []
-        self.pipe = pipe
-
-    def get_features(self):
-        """Return a RadioFeatures object for this radio"""
-        return RadioFeatures()
-
-    @classmethod
-    def get_name(cls):
-        """Return a printable name for this radio"""
-        return "%s %s" % (cls.VENDOR, cls.MODEL)
-
-    @classmethod
-    def get_prompts(cls):
-        """Return a set of strings for use in prompts"""
-        return RadioPrompts()
-
-    def set_pipe(self, pipe):
-        """Set the serial object to be used for communications"""
-        self.pipe = pipe
-
-    def get_memory(self, number):
-        """Return a Memory object for the memory at location @number"""
-        pass
-
-    def erase_memory(self, number):
-        """Erase memory at location @number"""
-        mem = Memory()
-        mem.number = number
-        mem.empty = True
-        self.set_memory(mem)
-
-    def get_memories(self, lo=None, hi=None):
-        """Get all the memories between @lo and @hi"""
-        pass
-
-    def set_memory(self, memory):
-        """Set the memory object @memory"""
-        pass
-
-    def get_mapping_models(self):
-        """Returns a list of MappingModel objects (or an empty list)"""
-        if hasattr(self, "get_bank_model"):
-            # FIXME: Backwards compatibility for old bank models
-            bank_model = self.get_bank_model()
-            if bank_model:
-                return [bank_model]
-        return []
-
-    def get_raw_memory(self, number):
-        """Return a raw string describing the memory at @number"""
-        pass
-
-    def filter_name(self, name):
-        """Filter @name to just the length and characters supported"""
-        rf = self.get_features()
-        if rf.valid_characters == rf.valid_characters.upper():
-            # Radio only supports uppercase, so help out here
-            name = name.upper()
-        return "".join([x for x in name[:rf.valid_name_length]
-                        if x in rf.valid_characters])
-
-    def get_sub_devices(self):
-        """Return a list of sub-device Radio objects, if
-        RadioFeatures.has_sub_devices is True"""
-        return []
-
-    def validate_memory(self, mem):
-        """Return a list of warnings and errors that will be encoundered
-        if trying to set @mem on the current radio"""
-        rf = self.get_features()
-        return rf.validate_memory(mem)
-
-    def get_settings(self):
-        """Returns a RadioSettings list containing one or more
-        RadioSettingGroup or RadioSetting objects. These represent general
-        setting knobs and dials that can be adjusted on the radio. If this
-        function is implemented, the has_settings RadioFeatures flag should
-        be True and set_settings() must be implemented as well."""
-        pass
-
-    def set_settings(self, settings):
-        """Accepts the top-level RadioSettingGroup returned from get_settings()
-        and adjusts the values in the radio accordingly. This function expects
-        the entire RadioSettingGroup hierarchy returned from get_settings().
-        If this function is implemented, the has_settings RadioFeatures flag
-        should be True and get_settings() must be implemented as well."""
-        pass
-
-
-class FileBackedRadio(Radio):
-    """A file-backed radio stores its data in a file"""
-    FILE_EXTENSION = "img"
-
-    def __init__(self, *args, **kwargs):
-        Radio.__init__(self, *args, **kwargs)
-        self._memobj = None
-
-    def save(self, filename):
-        """Save the radio's memory map to @filename"""
-        self.save_mmap(filename)
-
-    def load(self, filename):
-        """Load the radio's memory map object from @filename"""
-        self.load_mmap(filename)
-
-    def process_mmap(self):
-        """Process a newly-loaded or downloaded memory map"""
-        pass
-
-    def load_mmap(self, filename):
-        """Load the radio's memory map from @filename"""
-        mapfile = file(filename, "rb")
-        self._mmap = memmap.MemoryMap(mapfile.read())
-        mapfile.close()
-        self.process_mmap()
-
-    def save_mmap(self, filename):
-        """
-        try to open a file and write to it
-        If IOError raise a File Access Error Exception
-        """
-        try:
-            mapfile = file(filename, "wb")
-            mapfile.write(self._mmap.get_packed())
-            mapfile.close()
-        except IOError:
-            raise Exception("File Access Error")
-
-    def get_mmap(self):
-        """Return the radio's memory map object"""
-        return self._mmap
-
-
-class CloneModeRadio(FileBackedRadio):
-    """A clone-mode radio does a full memory dump in and out and we store
-    an image of the radio into an image file"""
-
-    _memsize = 0
-
-    def __init__(self, pipe):
-        self.errors = []
-        self._mmap = None
-
-        if isinstance(pipe, str):
-            self.pipe = None
-            self.load_mmap(pipe)
-        elif isinstance(pipe, memmap.MemoryMap):
-            self.pipe = None
-            self._mmap = pipe
-            self.process_mmap()
-        else:
-            FileBackedRadio.__init__(self, pipe)
-
-    def get_memsize(self):
-        """Return the radio's memory size"""
-        return self._memsize
-
-    @classmethod
-    def match_model(cls, filedata, filename):
-        """Given contents of a stored file (@filedata), return True if
-        this radio driver handles the represented model"""
-
-        # Unless the radio driver does something smarter, claim
-        # support if the data is the same size as our memory.
-        # Ideally, each radio would perform an intelligent analysis to
-        # make this determination to avoid model conflicts with
-        # memories of the same size.
-        return len(filedata) == cls._memsize
-
-    def sync_in(self):
-        "Initiate a radio-to-PC clone operation"
-        pass
-
-    def sync_out(self):
-        "Initiate a PC-to-radio clone operation"
-        pass
-
-
-class LiveRadio(Radio):
-    """Base class for all Live-Mode radios"""
-    pass
-
-
-class NetworkSourceRadio(Radio):
-    """Base class for all radios based on a network source"""
-
-    def do_fetch(self):
-        """Fetch the source data from the network"""
-        pass
-
-
-class IcomDstarSupport:
-    """Base interface for radios supporting Icom's D-STAR technology"""
-    MYCALL_LIMIT = (1, 1)
-    URCALL_LIMIT = (1, 1)
-    RPTCALL_LIMIT = (1, 1)
-
-    def get_urcall_list(self):
-        """Return a list of URCALL callsigns"""
-        return []
-
-    def get_repeater_call_list(self):
-        """Return a list of RPTCALL callsigns"""
-        return []
-
-    def get_mycall_list(self):
-        """Return a list of MYCALL callsigns"""
-        return []
-
-    def set_urcall_list(self, calls):
-        """Set the URCALL callsign list"""
-        pass
-
-    def set_repeater_call_list(self, calls):
-        """Set the RPTCALL callsign list"""
-        pass
-
-    def set_mycall_list(self, calls):
-        """Set the MYCALL callsign list"""
-        pass
-
-
-class ExperimentalRadio:
-    """Interface for experimental radios"""
-    @classmethod
-    def get_experimental_warning(cls):
-        return ("This radio's driver is marked as experimental and may " +
-                "be unstable or unsafe to use.")
-
-
-class Status:
-    """Clone status object for conveying clone progress to the UI"""
-    name = "Job"
-    msg = "Unknown"
-    max = 100
-    cur = 0
-
-    def __str__(self):
-        try:
-            pct = (self.cur / float(self.max)) * 100
-            nticks = int(pct) / 10
-            ticks = "=" * nticks
-        except ValueError:
-            pct = 0.0
-            ticks = "?" * 10
-
-        return "|%-10s| %2.1f%% %s" % (ticks, pct, self.msg)
-
-
-def is_fractional_step(freq):
-    """Returns True if @freq requires a 12.5kHz or 6.25kHz step"""
-    return not is_5_0(freq) and (is_12_5(freq) or is_6_25(freq))
-
-
-def is_5_0(freq):
-    """Returns True if @freq is reachable by a 5kHz step"""
-    return (freq % 5000) == 0
-
-
-def is_12_5(freq):
-    """Returns True if @freq is reachable by a 12.5kHz step"""
-    return (freq % 12500) == 0
-
-
-def is_6_25(freq):
-    """Returns True if @freq is reachable by a 6.25kHz step"""
-    return (freq % 6250) == 0
-
-
-def is_2_5(freq):
-    """Returns True if @freq is reachable by a 2.5kHz step"""
-    return (freq % 2500) == 0
-
-
-def required_step(freq):
-    """Returns the simplest tuning step that is required to reach @freq"""
-    if is_5_0(freq):
-        return 5.0
-    elif is_12_5(freq):
-        return 12.5
-    elif is_6_25(freq):
-        return 6.25
-    elif is_2_5(freq):
-        return 2.5
-    else:
-        raise errors.InvalidDataError("Unable to calculate the required " +
-                                      "tuning step for %i.%5i" %
-                                      (freq / 1000000, freq % 1000000))
-
-
-def fix_rounded_step(freq):
-    """Some radios imply the last bit of 12.5kHz and 6.25kHz step
-    frequencies. Take the base @freq and return the corrected one"""
-    try:
-        required_step(freq)
-        return freq
-    except errors.InvalidDataError:
-        pass
-
-    try:
-        required_step(freq + 500)
-        return freq + 500
-    except errors.InvalidDataError:
-        pass
-
-    try:
-        required_step(freq + 250)
-        return freq + 250
-    except errors.InvalidDataError:
-        pass
-
-    try:
-        required_step(freq + 750)
-        return float(freq + 750)
-    except errors.InvalidDataError:
-        pass
-
-    raise errors.InvalidDataError("Unable to correct rounded frequency " +
-                                  format_freq(freq))
-
-
-def _name(name, len, just_upper):
-    """Justify @name to @len, optionally converting to all uppercase"""
-    if just_upper:
-        name = name.upper()
-    return name.ljust(len)[:len]
-
-
-def name6(name, just_upper=True):
-    """6-char name"""
-    return _name(name, 6, just_upper)
-
-
-def name8(name, just_upper=False):
-    """8-char name"""
-    return _name(name, 8, just_upper)
-
-
-def name16(name, just_upper=False):
-    """16-char name"""
-    return _name(name, 16, just_upper)
-
-
-def to_GHz(val):
-    """Convert @val in GHz to Hz"""
-    return val * 1000000000
-
-
-def to_MHz(val):
-    """Convert @val in MHz to Hz"""
-    return val * 1000000
-
-
-def to_kHz(val):
-    """Convert @val in kHz to Hz"""
-    return val * 1000
-
-
-def from_GHz(val):
-    """Convert @val in Hz to GHz"""
-    return val / 100000000
-
-
-def from_MHz(val):
-    """Convert @val in Hz to MHz"""
-    return val / 100000
-
-
-def from_kHz(val):
-    """Convert @val in Hz to kHz"""
-    return val / 100
-
-
-def split_tone_decode(mem, txtone, rxtone):
-    """
-    Set tone mode and values on @mem based on txtone and rxtone specs like:
-    None, None, None
-    "Tone", 123.0, None
-    "DTCS", 23, "N"
-    """
-    txmode, txval, txpol = txtone
-    rxmode, rxval, rxpol = rxtone
-
-    mem.dtcs_polarity = "%s%s" % (txpol or "N", rxpol or "N")
-
-    if not txmode and not rxmode:
-        # No tone
-        return
-
-    if txmode == "Tone" and not rxmode:
-        mem.tmode = "Tone"
-        mem.rtone = txval
-        return
-
-    if txmode == rxmode == "Tone" and txval == rxval:
-        # TX and RX same tone -> TSQL
-        mem.tmode = "TSQL"
-        mem.ctone = txval
-        return
-
-    if txmode == rxmode == "DTCS" and txval == rxval:
-        mem.tmode = "DTCS"
-        mem.dtcs = txval
-        return
-
-    mem.tmode = "Cross"
-    mem.cross_mode = "%s->%s" % (txmode or "", rxmode or "")
-
-    if txmode == "Tone":
-        mem.rtone = txval
-    elif txmode == "DTCS":
-        mem.dtcs = txval
-
-    if rxmode == "Tone":
-        mem.ctone = rxval
-    elif rxmode == "DTCS":
-        mem.rx_dtcs = rxval
-
-
-def split_tone_encode(mem):
-    """
-    Returns TX, RX tone specs based on @mem like:
-    None, None, None
-    "Tone", 123.0, None
-    "DTCS", 23, "N"
-    """
-
-    txmode = ''
-    rxmode = ''
-    txval = None
-    rxval = None
-
-    if mem.tmode == "Tone":
-        txmode = "Tone"
-        txval = mem.rtone
-    elif mem.tmode == "TSQL":
-        txmode = rxmode = "Tone"
-        txval = rxval = mem.ctone
-    elif mem.tmode == "DTCS":
-        txmode = rxmode = "DTCS"
-        txval = rxval = mem.dtcs
-    elif mem.tmode == "Cross":
-        txmode, rxmode = mem.cross_mode.split("->", 1)
-        if txmode == "Tone":
-            txval = mem.rtone
-        elif txmode == "DTCS":
-            txval = mem.dtcs
-        if rxmode == "Tone":
-            rxval = mem.ctone
-        elif rxmode == "DTCS":
-            rxval = mem.rx_dtcs
-
-    if txmode == "DTCS":
-        txpol = mem.dtcs_polarity[0]
-    else:
-        txpol = None
-    if rxmode == "DTCS":
-        rxpol = mem.dtcs_polarity[1]
-    else:
-        rxpol = None
-
-    return ((txmode, txval, txpol),
-            (rxmode, rxval, rxpol))
-
-
-def sanitize_string(astring, validcharset=CHARSET_ASCII, replacechar='*'):
-    myfilter = ''.join(
-        [
-            [replacechar, chr(x)][chr(x) in validcharset]
-            for x in xrange(256)
-        ])
-    return astring.translate(myfilter)
+# Copyright 2008 Dan Smith <dsmith at danplanet.com>
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program.  If not, see <http://www.gnu.org/licenses/>.
+
+import math
+from chirp import errors, memmap
+
+SEPCHAR = ","
+
+# 50 Tones
+TONES = [67.0, 69.3, 71.9, 74.4, 77.0, 79.7, 82.5,
+         85.4, 88.5, 91.5, 94.8, 97.4, 100.0, 103.5,
+         107.2, 110.9, 114.8, 118.8, 123.0, 127.3,
+         131.8, 136.5, 141.3, 146.2, 151.4, 156.7,
+         159.8, 162.2, 165.5, 167.9, 171.3, 173.8,
+         177.3, 179.9, 183.5, 186.2, 189.9, 192.8,
+         196.6, 199.5, 203.5, 206.5, 210.7, 218.1,
+         225.7, 229.1, 233.6, 241.8, 250.3, 254.1,
+         ]
+
+TONES_EXTRA = [56.0, 57.0, 58.0, 59.0, 60.0, 61.0, 62.0,
+               62.5, 63.0, 64.0]
+
+OLD_TONES = list(TONES)
+[OLD_TONES.remove(x) for x in [159.8, 165.5, 171.3, 177.3, 183.5, 189.9,
+                               196.6, 199.5, 206.5, 229.1, 254.1]]
+
+# 104 DTCS Codes
+DTCS_CODES = [
+    23,  25,  26,  31,  32,  36,  43,  47,  51,  53,  54,
+    65,  71,  72,  73,  74,  114, 115, 116, 122, 125, 131,
+    132, 134, 143, 145, 152, 155, 156, 162, 165, 172, 174,
+    205, 212, 223, 225, 226, 243, 244, 245, 246, 251, 252,
+    255, 261, 263, 265, 266, 271, 274, 306, 311, 315, 325,
+    331, 332, 343, 346, 351, 356, 364, 365, 371, 411, 412,
+    413, 423, 431, 432, 445, 446, 452, 454, 455, 462, 464,
+    465, 466, 503, 506, 516, 523, 526, 532, 546, 565, 606,
+    612, 624, 627, 631, 632, 654, 662, 664, 703, 712, 723,
+    731, 732, 734, 743, 754,
+]
+
+# 512 Possible DTCS Codes
+ALL_DTCS_CODES = []
+for a in range(0, 8):
+    for b in range(0, 8):
+        for c in range(0, 8):
+            ALL_DTCS_CODES.append((a * 100) + (b * 10) + c)
+
+CROSS_MODES = [
+    "Tone->Tone",
+    "DTCS->",
+    "->DTCS",
+    "Tone->DTCS",
+    "DTCS->Tone",
+    "->Tone",
+    "DTCS->DTCS",
+    "Tone->"
+]
+
+MODES = ["WFM", "FM", "NFM", "AM", "NAM", "DV", "USB", "LSB", "CW", "RTTY",
+         "DIG", "PKT", "NCW", "NCWR", "CWR", "P25", "Auto", "RTTYR",
+         "FSK", "FSKR", "DMR"]
+
+TONE_MODES = [
+    "",
+    "Tone",
+    "TSQL",
+    "DTCS",
+    "DTCS-R",
+    "TSQL-R",
+    "Cross",
+]
+
+TUNING_STEPS = [
+    5.0, 6.25, 10.0, 12.5, 15.0, 20.0, 25.0, 30.0, 50.0, 100.0,
+    125.0, 200.0,
+    # Need to fix drivers using this list as an index!
+    9.0, 1.0, 2.5,
+]
+
+SKIP_VALUES = ["", "S", "P"]
+
+CHARSET_UPPER_NUMERIC = "ABCDEFGHIJKLMNOPQRSTUVWXYZ 1234567890"
+CHARSET_ALPHANUMERIC = \
+    "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz 1234567890"
+CHARSET_ASCII = "".join([chr(x) for x in range(ord(" "), ord("~") + 1)])
+
+# http://aprs.org/aprs11/SSIDs.txt
+APRS_SSID = (
+    "0 Your primary station usually fixed and message capable",
+    "1 generic additional station, digi, mobile, wx, etc",
+    "2 generic additional station, digi, mobile, wx, etc",
+    "3 generic additional station, digi, mobile, wx, etc",
+    "4 generic additional station, digi, mobile, wx, etc",
+    "5 Other networks (Dstar, Iphones, Androids, Blackberry's etc)",
+    "6 Special activity, Satellite ops, camping or 6 meters, etc",
+    "7 walkie talkies, HT's or other human portable",
+    "8 boats, sailboats, RV's or second main mobile",
+    "9 Primary Mobile (usually message capable)",
+    "10 internet, Igates, echolink, winlink, AVRS, APRN, etc",
+    "11 balloons, aircraft, spacecraft, etc",
+    "12 APRStt, DTMF, RFID, devices, one-way trackers*, etc",
+    "13 Weather stations",
+    "14 Truckers or generally full time drivers",
+    "15 generic additional station, digi, mobile, wx, etc")
+APRS_POSITION_COMMENT = (
+    "off duty", "en route", "in service", "returning", "committed",
+    "special", "priority", "custom 0", "custom 1", "custom 2", "custom 3",
+    "custom 4", "custom 5", "custom 6", "EMERGENCY")
+# http://aprs.org/symbols/symbolsX.txt
+APRS_SYMBOLS = (
+    "Police/Sheriff", "[reserved]", "Digi", "Phone", "DX Cluster",
+    "HF Gateway", "Small Aircraft", "Mobile Satellite Groundstation",
+    "Wheelchair", "Snowmobile", "Red Cross", "Boy Scouts", "House QTH (VHF)",
+    "X", "Red Dot", "0 in Circle", "1 in Circle", "2 in Circle",
+    "3 in Circle", "4 in Circle", "5 in Circle", "6 in Circle", "7 in Circle",
+    "8 in Circle", "9 in Circle", "Fire", "Campground", "Motorcycle",
+    "Railroad Engine", "Car", "File Server", "Hurricane Future Prediction",
+    "Aid Station", "BBS or PBBS", "Canoe", "[reserved]", "Eyeball",
+    "Tractor/Farm Vehicle", "Grid Square", "Hotel", "TCP/IP", "[reserved]",
+    "School", "PC User", "MacAPRS", "NTS Station", "Balloon", "Police", "TBD",
+    "Recreational Vehicle", "Space Shuttle", "SSTV", "Bus", "ATV",
+    "National WX Service Site", "Helicopter", "Yacht/Sail Boat", "WinAPRS",
+    "Human/Person", "Triangle", "Mail/Postoffice", "Large Aircraft",
+    "WX Station", "Dish Antenna", "Ambulance", "Bicycle",
+    "Incident Command Post", "Dual Garage/Fire Dept", "Horse/Equestrian",
+    "Fire Truck", "Glider", "Hospital", "IOTA", "Jeep", "Truck", "Laptop",
+    "Mic-Repeater", "Node", "Emergency Operations Center", "Rover (dog)",
+    "Grid Square above 128m", "Repeater", "Ship/Power Boat", "Truck Stop",
+    "Truck (18 wheeler)", "Van", "Water Station", "X-APRS", "Yagi at QTH",
+    "TDB", "[reserved]"
+)
+
+
+def watts_to_dBm(watts):
+    """Converts @watts in watts to dBm"""
+    return int(10 * math.log10(int(watts * 1000)))
+
+
+def dBm_to_watts(dBm):
+    """Converts @dBm from dBm to watts"""
+    return int(math.pow(10, (dBm - 30) / 10))
+
+
+class PowerLevel:
+    """Represents a power level supported by a radio"""
+
+    def __init__(self, label, watts=0, dBm=0):
+        if watts:
+            dBm = watts_to_dBm(watts)
+        self._power = int(dBm)
+        self._label = label
+
+    def __str__(self):
+        return str(self._label)
+
+    def __int__(self):
+        return self._power
+
+    def __sub__(self, val):
+        return int(self) - int(val)
+
+    def __add__(self, val):
+        return int(self) + int(val)
+
+    def __eq__(self, val):
+        if val is not None:
+            return int(self) == int(val)
+        return False
+
+    def __lt__(self, val):
+        return int(self) < int(val)
+
+    def __gt__(self, val):
+        return int(self) > int(val)
+
+    def __nonzero__(self):
+        return int(self) != 0
+
+    def __repr__(self):
+        return "%s (%i dBm)" % (self._label, self._power)
+
+
+def parse_freq(freqstr):
+    """Parse a frequency string and return the value in integral Hz"""
+    freqstr = freqstr.strip()
+    if freqstr == "":
+        return 0
+    elif freqstr.endswith(" MHz"):
+        return parse_freq(freqstr.split(" ")[0])
+    elif freqstr.endswith(" kHz"):
+        return int(freqstr.split(" ")[0]) * 1000
+
+    if "." in freqstr:
+        mhz, khz = freqstr.split(".")
+        if mhz == "":
+            mhz = 0
+        khz = khz.ljust(6, "0")
+        if len(khz) > 6:
+            raise ValueError("Invalid kHz value: %s", khz)
+        mhz = int(mhz) * 1000000
+        khz = int(khz)
+    else:
+        mhz = int(freqstr) * 1000000
+        khz = 0
+
+    return mhz + khz
+
+
+def format_freq(freq):
+    """Format a frequency given in Hz as a string"""
+
+    return "%i.%06i" % (freq / 1000000, freq % 1000000)
+
+
+class ImmutableValueError(ValueError):
+    pass
+
+
+class Memory:
+    """Base class for a single radio memory"""
+    freq = 0
+    number = 0
+    extd_number = ""
+    name = ""
+    vfo = 0
+    rtone = 88.5
+    ctone = 88.5
+    dtcs = 23
+    rx_dtcs = 23
+    tmode = ""
+    cross_mode = "Tone->Tone"
+    dtcs_polarity = "NN"
+    skip = ""
+    power = None
+    duplex = ""
+    offset = 600000
+    mode = "FM"
+    tuning_step = 5.0
+
+    comment = ""
+
+    empty = False
+
+    immutable = []
+
+    # A RadioSettingGroup of additional settings supported by the radio,
+    # or an empty list if none
+    extra = []
+
+    def __init__(self):
+        self.freq = 0
+        self.number = 0
+        self.extd_number = ""
+        self.name = ""
+        self.vfo = 0
+        self.rtone = 88.5
+        self.ctone = 88.5
+        self.dtcs = 23
+        self.rx_dtcs = 23
+        self.tmode = ""
+        self.cross_mode = "Tone->Tone"
+        self.dtcs_polarity = "NN"
+        self.skip = ""
+        self.power = None
+        self.duplex = ""
+        self.offset = 600000
+        self.mode = "FM"
+        self.tuning_step = 5.0
+
+        self.comment = ""
+
+        self.empty = False
+
+        self.immutable = []
+
+    _valid_map = {
+        "rtone":          TONES + TONES_EXTRA,
+        "ctone":          TONES + TONES_EXTRA,
+        "dtcs":           ALL_DTCS_CODES,
+        "rx_dtcs":        ALL_DTCS_CODES,
+        "tmode":          TONE_MODES,
+        "dtcs_polarity":  ["NN", "NR", "RN", "RR"],
+        "cross_mode":     CROSS_MODES,
+        "mode":           MODES,
+        "duplex":         ["", "+", "-", "split", "off"],
+        "skip":           SKIP_VALUES,
+        "empty":          [True, False],
+        "dv_code":        [x for x in range(0, 100)],
+    }
+
+    def __repr__(self):
+        return "Memory[%i]" % self.number
+
+    def dupe(self):
+        """Return a deep copy of @self"""
+        mem = self.__class__()
+        for k, v in self.__dict__.items():
+            mem.__dict__[k] = v
+
+        return mem
+
+    def clone(self, source):
+        """Absorb all of the properties of @source"""
+        for k, v in source.__dict__.items():
+            self.__dict__[k] = v
+
+    CSV_FORMAT = ["Location", "Name", "Frequency",
+                  "Duplex", "Offset", "Tone",
+                  "rToneFreq", "cToneFreq", "DtcsCode",
+                  "DtcsPolarity", "Mode", "TStep",
+                  "Skip", "Comment",
+                  "URCALL", "RPT1CALL", "RPT2CALL", "DVCODE"]
+
+    def __setattr__(self, name, val):
+        if not hasattr(self, name):
+            raise ValueError("No such attribute `%s'" % name)
+
+        if name in self.immutable:
+            raise ImmutableValueError("Field %s is not " % name +
+                                      "mutable on this memory")
+
+        if name in self._valid_map and val not in self._valid_map[name]:
+            raise ValueError("`%s' is not in valid list: %s" %
+                             (val, self._valid_map[name]))
+
+        self.__dict__[name] = val
+
+    def format_freq(self):
+        """Return a properly-formatted string of this memory's frequency"""
+        return format_freq(self.freq)
+
+    def parse_freq(self, freqstr):
+        """Set the frequency from a string"""
+        self.freq = parse_freq(freqstr)
+        return self.freq
+
+    def __str__(self):
+        if self.tmode == "Tone":
+            tenc = "*"
+        else:
+            tenc = " "
+
+        if self.tmode == "TSQL":
+            tsql = "*"
+        else:
+            tsql = " "
+
+        if self.tmode == "DTCS":
+            dtcs = "*"
+        else:
+            dtcs = " "
+
+        if self.duplex == "":
+            dup = "/"
+        else:
+            dup = self.duplex
+
+        return \
+            "Memory %s: %s%s%s %s (%s) r%.1f%s c%.1f%s d%03i%s%s [%.2f]" % \
+            (self.number if self.extd_number == "" else self.extd_number,
+             format_freq(self.freq),
+             dup,
+             format_freq(self.offset),
+             self.mode,
+             self.name,
+             self.rtone,
+             tenc,
+             self.ctone,
+             tsql,
+             self.dtcs,
+             dtcs,
+             self.dtcs_polarity,
+             self.tuning_step)
+
+    def to_csv(self):
+        """Return a CSV representation of this memory"""
+        return [
+            "%i" % self.number,
+            "%s" % self.name,
+            format_freq(self.freq),
+            "%s" % self.duplex,
+            format_freq(self.offset),
+            "%s" % self.tmode,
+            "%.1f" % self.rtone,
+            "%.1f" % self.ctone,
+            "%03i" % self.dtcs,
+            "%s" % self.dtcs_polarity,
+            "%s" % self.mode,
+            "%.2f" % self.tuning_step,
+            "%s" % self.skip,
+            "%s" % self.comment,
+            "", "", "", ""]
+
+    @classmethod
+    def _from_csv(cls, _line):
+        line = _line.strip()
+        if line.startswith("Location"):
+            raise errors.InvalidMemoryLocation("Non-CSV line")
+
+        vals = line.split(SEPCHAR)
+        if len(vals) < 11:
+            raise errors.InvalidDataError("CSV format error " +
+                                          "(14 columns expected)")
+
+        if vals[10] == "DV":
+            mem = DVMemory()
+        else:
+            mem = Memory()
+
+        mem.really_from_csv(vals)
+        return mem
+
+    def really_from_csv(self, vals):
+        """Careful parsing of split-out @vals"""
+        try:
+            self.number = int(vals[0])
+        except:
+            raise errors.InvalidDataError(
+                "Location '%s' is not a valid integer" % vals[0])
+
+        self.name = vals[1]
+
+        try:
+            self.freq = float(vals[2])
+        except:
+            raise errors.InvalidDataError("Frequency is not a valid number")
+
+        if vals[3].strip() in ["+", "-", ""]:
+            self.duplex = vals[3].strip()
+        else:
+            raise errors.InvalidDataError("Duplex is not +,-, or empty")
+
+        try:
+            self.offset = float(vals[4])
+        except:
+            raise errors.InvalidDataError("Offset is not a valid number")
+
+        self.tmode = vals[5]
+        if self.tmode not in TONE_MODES:
+            raise errors.InvalidDataError("Invalid tone mode `%s'" %
+                                          self.tmode)
+
+        try:
+            self.rtone = float(vals[6])
+        except:
+            raise errors.InvalidDataError("rTone is not a valid number")
+        if self.rtone not in TONES:
+            raise errors.InvalidDataError("rTone is not valid")
+
+        try:
+            self.ctone = float(vals[7])
+        except:
+            raise errors.InvalidDataError("cTone is not a valid number")
+        if self.ctone not in TONES:
+            raise errors.InvalidDataError("cTone is not valid")
+
+        try:
+            self.dtcs = int(vals[8], 10)
+        except:
+            raise errors.InvalidDataError("DTCS code is not a valid number")
+        if self.dtcs not in DTCS_CODES:
+            raise errors.InvalidDataError("DTCS code is not valid")
+
+        try:
+            self.rx_dtcs = int(vals[8], 10)
+        except:
+            raise errors.InvalidDataError("DTCS Rx code is not a valid number")
+        if self.rx_dtcs not in DTCS_CODES:
+            raise errors.InvalidDataError("DTCS Rx code is not valid")
+
+        if vals[9] in ["NN", "NR", "RN", "RR"]:
+            self.dtcs_polarity = vals[9]
+        else:
+            raise errors.InvalidDataError("DtcsPolarity is not valid")
+
+        if vals[10] in MODES:
+            self.mode = vals[10]
+        else:
+            raise errors.InvalidDataError("Mode is not valid")
+
+        try:
+            self.tuning_step = float(vals[11])
+        except:
+            raise errors.InvalidDataError("Tuning step is invalid")
+
+        try:
+            self.skip = vals[12]
+        except:
+            raise errors.InvalidDataError("Skip value is not valid")
+
+        return True
+
+
+class DVMemory(Memory):
+    """A Memory with D-STAR attributes"""
+    dv_urcall = "CQCQCQ"
+    dv_rpt1call = ""
+    dv_rpt2call = ""
+    dv_code = 0
+
+    def __str__(self):
+        string = Memory.__str__(self)
+
+        string += " <%s,%s,%s>" % (self.dv_urcall,
+                                   self.dv_rpt1call,
+                                   self.dv_rpt2call)
+
+        return string
+
+    def to_csv(self):
+        return [
+            "%i" % self.number,
+            "%s" % self.name,
+            format_freq(self.freq),
+            "%s" % self.duplex,
+            format_freq(self.offset),
+            "%s" % self.tmode,
+            "%.1f" % self.rtone,
+            "%.1f" % self.ctone,
+            "%03i" % self.dtcs,
+            "%s" % self.dtcs_polarity,
+            "%s" % self.mode,
+            "%.2f" % self.tuning_step,
+            "%s" % self.skip,
+            "%s" % self.comment,
+            "%s" % self.dv_urcall,
+            "%s" % self.dv_rpt1call,
+            "%s" % self.dv_rpt2call,
+            "%i" % self.dv_code]
+
+    def really_from_csv(self, vals):
+        Memory.really_from_csv(self, vals)
+
+        self.dv_urcall = vals[15].rstrip()[:8]
+        self.dv_rpt1call = vals[16].rstrip()[:8]
+        self.dv_rpt2call = vals[17].rstrip()[:8]
+        try:
+            self.dv_code = int(vals[18].strip())
+        except Exception:
+            self.dv_code = 0
+
+
+class MemoryMapping(object):
+    """Base class for a memory mapping"""
+
+    def __init__(self, model, index, name):
+        self._model = model
+        self._index = index
+        self._name = name
+
+    def __str__(self):
+        return self.get_name()
+
+    def __repr__(self):
+        return "%s-%s" % (self.__class__.__name__, self._index)
+
+    def get_name(self):
+        """Returns the mapping name"""
+        return self._name
+
+    def get_index(self):
+        """Returns the immutable index (string or int)"""
+        return self._index
+
+    def __eq__(self, other):
+        return self.get_index() == other.get_index()
+
+
+class MappingModel(object):
+    """Base class for a memory mapping model"""
+
+    def __init__(self, radio, name):
+        self._radio = radio
+        self._name = name
+
+    def get_name(self):
+        return self._name
+
+    def get_num_mappings(self):
+        """Returns the number of mappings in the model (should be
+        callable without consulting the radio"""
+        raise NotImplementedError()
+
+    def get_mappings(self):
+        """Return a list of mappings"""
+        raise NotImplementedError()
+
+    def add_memory_to_mapping(self, memory, mapping):
+        """Add @memory to @mapping."""
+        raise NotImplementedError()
+
+    def remove_memory_from_mapping(self, memory, mapping):
+        """Remove @memory from @mapping.
+        Shall raise exception if @memory is not in @bank"""
+        raise NotImplementedError()
+
+    def get_mapping_memories(self, mapping):
+        """Return a list of memories in @mapping"""
+        raise NotImplementedError()
+
+    def get_memory_mappings(self, memory):
+        """Return a list of mappings that @memory is in"""
+        raise NotImplementedError()
+
+
+class Bank(MemoryMapping):
+    """Base class for a radio's Bank"""
+
+
+class NamedBank(Bank):
+    """A bank that can have a name"""
+
+    def set_name(self, name):
+        """Changes the user-adjustable bank name"""
+        self._name = name
+
+
+class BankModel(MappingModel):
+    """A bank model where one memory is in zero or one banks at any point"""
+
+    def __init__(self, radio, name='Banks'):
+        super(BankModel, self).__init__(radio, name)
+
+
+class MappingModelIndexInterface:
+    """Interface for mappings with index capabilities"""
+
+    def get_index_bounds(self):
+        """Returns a tuple (lo,hi) of the min and max mapping indices"""
+        raise NotImplementedError()
+
+    def get_memory_index(self, memory, mapping):
+        """Returns the index of @memory in @mapping"""
+        raise NotImplementedError()
+
+    def set_memory_index(self, memory, mapping, index):
+        """Sets the index of @memory in @mapping to @index"""
+        raise NotImplementedError()
+
+    def get_next_mapping_index(self, mapping):
+        """Returns the next available mapping index in @mapping, or raises
+        Exception if full"""
+        raise NotImplementedError()
+
+
+class MTOBankModel(BankModel):
+    """A bank model where one memory can be in multiple banks at once """
+    pass
+
+
+def console_status(status):
+    """Write a status object to the console"""
+    import logging
+    from chirp import logger
+    if not logger.is_visible(logging.WARN):
+        return
+    import sys
+    import os
+    sys.stdout.write("\r%s" % status)
+    if status.cur == status.max:
+        sys.stdout.write(os.linesep)
+
+
+class RadioPrompts:
+    """Radio prompt strings"""
+    info = None
+    display_info = True
+    experimental = None
+    pre_download = None
+    pre_upload = None
+    display_pre_upload_prompt_before_opening_port = True
+
+
+BOOLEAN = [True, False]
+
+
+class RadioFeatures:
+    """Radio Feature Flags"""
+    _valid_map = {
+        # General
+        "has_bank_index":       BOOLEAN,
+        "has_dtcs":             BOOLEAN,
+        "has_rx_dtcs":          BOOLEAN,
+        "has_dtcs_polarity":    BOOLEAN,
+        "has_mode":             BOOLEAN,
+        "has_offset":           BOOLEAN,
+        "has_name":             BOOLEAN,
+        "has_bank":             BOOLEAN,
+        "has_bank_names":       BOOLEAN,
+        "has_tuning_step":      BOOLEAN,
+        "has_ctone":            BOOLEAN,
+        "has_cross":            BOOLEAN,
+        "has_infinite_number":  BOOLEAN,
+        "has_nostep_tuning":    BOOLEAN,
+        "has_comment":          BOOLEAN,
+        "has_settings":         BOOLEAN,
+
+        # Attributes
+        "valid_modes":          [],
+        "valid_tmodes":         [],
+        "valid_duplexes":       [],
+        "valid_tuning_steps":   [],
+        "valid_bands":          [],
+        "valid_skips":          [],
+        "valid_power_levels":   [],
+        "valid_characters":     "",
+        "valid_name_length":    0,
+        "valid_cross_modes":    [],
+        "valid_dtcs_pols":      [],
+        "valid_dtcs_codes":     [],
+        "valid_special_chans":  [],
+
+        "has_sub_devices":      BOOLEAN,
+        "memory_bounds":        (0, 0),
+        "can_odd_split":        BOOLEAN,
+
+        # D-STAR
+        "requires_call_lists":  BOOLEAN,
+        "has_implicit_calls":   BOOLEAN,
+    }
+
+    def __setattr__(self, name, val):
+        if name.startswith("_"):
+            self.__dict__[name] = val
+            return
+        elif name not in self._valid_map.keys():
+            raise ValueError("No such attribute `%s'" % name)
+
+        if type(self._valid_map[name]) == tuple:
+            # Tuple, cardinality must match
+            if type(val) != tuple or len(val) != len(self._valid_map[name]):
+                raise ValueError("Invalid value `%s' for attribute `%s'" %
+                                 (val, name))
+        elif type(self._valid_map[name]) == list and not self._valid_map[name]:
+            # Empty list, must be another list
+            if type(val) != list:
+                raise ValueError("Invalid value `%s' for attribute `%s'" %
+                                 (val, name))
+        elif type(self._valid_map[name]) == str:
+            if type(val) != str:
+                raise ValueError("Invalid value `%s' for attribute `%s'" %
+                                 (val, name))
+        elif type(self._valid_map[name]) == int:
+            if type(val) != int:
+                raise ValueError("Invalid value `%s' for attribute `%s'" %
+                                 (val, name))
+        elif val not in self._valid_map[name]:
+            # Value not in the list of valid values
+            raise ValueError("Invalid value `%s' for attribute `%s'" % (val,
+                                                                        name))
+        self.__dict__[name] = val
+
+    def __getattr__(self, name):
+        raise AttributeError("pylint is confused by RadioFeatures")
+
+    def init(self, attribute, default, doc=None):
+        """Initialize a feature flag @attribute with default value @default,
+        and documentation string @doc"""
+        self.__setattr__(attribute, default)
+        self.__docs[attribute] = doc
+
+    def get_doc(self, attribute):
+        """Return the description of @attribute"""
+        return self.__docs[attribute]
+
+    def __init__(self):
+        self.__docs = {}
+        self.init("has_bank_index", False,
+                  "Indicates that memories in a bank can be stored in " +
+                  "an order other than in main memory")
+        self.init("has_dtcs", True,
+                  "Indicates that DTCS tone mode is available")
+        self.init("has_rx_dtcs", False,
+                  "Indicates that radio can use two different " +
+                  "DTCS codes for rx and tx")
+        self.init("has_dtcs_polarity", True,
+                  "Indicates that the DTCS polarity can be changed")
+        self.init("has_mode", True,
+                  "Indicates that multiple emission modes are supported")
+        self.init("has_offset", True,
+                  "Indicates that the TX offset memory property is supported")
+        self.init("has_name", True,
+                  "Indicates that an alphanumeric memory name is supported")
+        self.init("has_bank", True,
+                  "Indicates that memories may be placed into banks")
+        self.init("has_bank_names", False,
+                  "Indicates that banks may be named")
+        self.init("has_tuning_step", True,
+                  "Indicates that memories store their tuning step")
+        self.init("has_ctone", True,
+                  "Indicates that the radio keeps separate tone frequencies " +
+                  "for repeater and CTCSS operation")
+        self.init("has_cross", False,
+                  "Indicates that the radios supports different tone modes " +
+                  "on transmit and receive")
+        self.init("has_infinite_number", False,
+                  "Indicates that the radio is not constrained in the " +
+                  "number of memories that it can store")
+        self.init("has_nostep_tuning", False,
+                  "Indicates that the radio does not require a valid " +
+                  "tuning step to store a frequency")
+        self.init("has_comment", False,
+                  "Indicates that the radio supports storing a comment " +
+                  "with each memory")
+        self.init("has_settings", False,
+                  "Indicates that the radio supports general settings")
+
+        self.init("valid_modes", list(MODES),
+                  "Supported emission (or receive) modes")
+        self.init("valid_tmodes", [],
+                  "Supported tone squelch modes")
+        self.init("valid_duplexes", ["", "+", "-"],
+                  "Supported duplex modes")
+        self.init("valid_tuning_steps", list(TUNING_STEPS),
+                  "Supported tuning steps")
+        self.init("valid_bands", [],
+                  "Supported frequency ranges")
+        self.init("valid_skips", ["", "S"],
+                  "Supported memory scan skip settings")
+        self.init("valid_power_levels", [],
+                  "Supported power levels")
+        self.init("valid_characters", CHARSET_UPPER_NUMERIC,
+                  "Supported characters for a memory's alphanumeric tag")
+        self.init("valid_name_length", 6,
+                  "The maximum number of characters in a memory's " +
+                  "alphanumeric tag")
+        self.init("valid_cross_modes", list(CROSS_MODES),
+                  "Supported tone cross modes")
+        self.init("valid_dtcs_pols", ["NN", "RN", "NR", "RR"],
+                  "Supported DTCS polarities")
+        self.init("valid_dtcs_codes", list(DTCS_CODES),
+                  "Supported DTCS codes")
+        self.init("valid_special_chans", [],
+                  "Supported special channel names")
+
+        self.init("has_sub_devices", False,
+                  "Indicates that the radio behaves as two semi-independent " +
+                  "devices")
+        self.init("memory_bounds", (0, 1),
+                  "The minimum and maximum channel numbers")
+        self.init("can_odd_split", False,
+                  "Indicates that the radio can store an independent " +
+                  "transmit frequency")
+
+        self.init("requires_call_lists", True,
+                  "[D-STAR] Indicates that the radio requires all callsigns " +
+                  "to be in the master list and cannot be stored " +
+                  "arbitrarily in each memory channel")
+        self.init("has_implicit_calls", False,
+                  "[D-STAR] Indicates that the radio has an implied " +
+                  "callsign at the beginning of the master URCALL list")
+
+    def is_a_feature(self, name):
+        """Returns True if @name is a valid feature flag name"""
+        return name in self._valid_map.keys()
+
+    def __getitem__(self, name):
+        return self.__dict__[name]
+
+    def validate_memory(self, mem):
+        """Return a list of warnings and errors that will be encoundered
+        if trying to set @mem on the current radio"""
+        msgs = []
+
+        lo, hi = self.memory_bounds
+        if not self.has_infinite_number and \
+                (mem.number < lo or mem.number > hi) and \
+                mem.extd_number not in self.valid_special_chans:
+            msg = ValidationWarning("Location %i is out of range" % mem.number)
+            msgs.append(msg)
+
+        if (self.valid_modes and
+                mem.mode not in self.valid_modes and
+                mem.mode != "Auto"):
+            msg = ValidationError("Mode %s not supported" % mem.mode)
+            msgs.append(msg)
+
+        if self.valid_tmodes and mem.tmode not in self.valid_tmodes:
+            msg = ValidationError("Tone mode %s not supported" % mem.tmode)
+            msgs.append(msg)
+        else:
+            if mem.tmode == "Cross":
+                if self.valid_cross_modes and \
+                        mem.cross_mode not in self.valid_cross_modes:
+                    msg = ValidationError("Cross tone mode %s not supported" %
+                                          mem.cross_mode)
+                    msgs.append(msg)
+
+        if self.has_dtcs_polarity and \
+                mem.dtcs_polarity not in self.valid_dtcs_pols:
+            msg = ValidationError("DTCS Polarity %s not supported" %
+                                  mem.dtcs_polarity)
+            msgs.append(msg)
+
+        if self.valid_dtcs_codes and \
+                mem.dtcs not in self.valid_dtcs_codes:
+            msg = ValidationError("DTCS Code %03i not supported" % mem.dtcs)
+        if self.valid_dtcs_codes and \
+                mem.rx_dtcs not in self.valid_dtcs_codes:
+            msg = ValidationError("DTCS Code %03i not supported" % mem.rx_dtcs)
+
+        if self.valid_duplexes and mem.duplex not in self.valid_duplexes:
+            msg = ValidationError("Duplex %s not supported" % mem.duplex)
+            msgs.append(msg)
+
+        ts = mem.tuning_step
+        if self.valid_tuning_steps and ts not in self.valid_tuning_steps and \
+                not self.has_nostep_tuning:
+            msg = ValidationError("Tuning step %.2f not supported" % ts)
+            msgs.append(msg)
+
+        if self.valid_bands:
+            valid = False
+            for lo, hi in self.valid_bands:
+                if lo <= mem.freq < hi:
+                    valid = True
+                    break
+            if not valid:
+                msg = ValidationError(
+                    ("Frequency {freq} is out "
+                     "of supported range").format(freq=format_freq(mem.freq)))
+                msgs.append(msg)
+
+        if self.valid_bands and \
+                self.valid_duplexes and \
+                mem.duplex in ["split", "-", "+"]:
+            if mem.duplex == "split":
+                freq = mem.offset
+            elif mem.duplex == "-":
+                freq = mem.freq - mem.offset
+            elif mem.duplex == "+":
+                freq = mem.freq + mem.offset
+            valid = False
+            for lo, hi in self.valid_bands:
+                if lo <= freq < hi:
+                    valid = True
+                    break
+            if not valid:
+                msg = ValidationError(
+                    ("Tx freq {freq} is out "
+                     "of supported range").format(freq=format_freq(freq)))
+                msgs.append(msg)
+
+        if mem.power and \
+                self.valid_power_levels and \
+                mem.power not in self.valid_power_levels:
+            msg = ValidationWarning("Power level %s not supported" % mem.power)
+            msgs.append(msg)
+
+        if self.valid_tuning_steps and not self.has_nostep_tuning:
+            try:
+                step = required_step(mem.freq)
+                if step not in self.valid_tuning_steps:
+                    msg = ValidationError("Frequency requires %.2fkHz step" %
+                                          required_step(mem.freq))
+                    msgs.append(msg)
+            except errors.InvalidDataError, e:
+                msgs.append(str(e))
+
+        if self.valid_characters:
+            for char in mem.name:
+                if char not in self.valid_characters:
+                    msgs.append(ValidationWarning("Name character " +
+                                                  "`%s'" % char +
+                                                  " not supported"))
+                    break
+
+        return msgs
+
+
+class ValidationMessage(str):
+    """Base class for Validation Errors and Warnings"""
+    pass
+
+
+class ValidationWarning(ValidationMessage):
+    """A non-fatal warning during memory validation"""
+    pass
+
+
+class ValidationError(ValidationMessage):
+    """A fatal error during memory validation"""
+    pass
+
+
+class Alias(object):
+    VENDOR = "Unknown"
+    MODEL = "Unknown"
+    VARIANT = ""
+
+
+class Radio(Alias):
+    """Base class for all Radio drivers"""
+    BAUD_RATE = 9600
+    HARDWARE_FLOW = False
+    ALIASES = []
+
+    def status_fn(self, status):
+        """Deliver @status to the UI"""
+        console_status(status)
+
+    def __init__(self, pipe):
+        self.errors = []
+        self.pipe = pipe
+
+    def get_features(self):
+        """Return a RadioFeatures object for this radio"""
+        return RadioFeatures()
+
+    @classmethod
+    def get_name(cls):
+        """Return a printable name for this radio"""
+        return "%s %s" % (cls.VENDOR, cls.MODEL)
+
+    @classmethod
+    def get_prompts(cls):
+        """Return a set of strings for use in prompts"""
+        return RadioPrompts()
+
+    def set_pipe(self, pipe):
+        """Set the serial object to be used for communications"""
+        self.pipe = pipe
+
+    def get_memory(self, number):
+        """Return a Memory object for the memory at location @number"""
+        pass
+
+    def erase_memory(self, number):
+        """Erase memory at location @number"""
+        mem = Memory()
+        mem.number = number
+        mem.empty = True
+        self.set_memory(mem)
+
+    def get_memories(self, lo=None, hi=None):
+        """Get all the memories between @lo and @hi"""
+        pass
+
+    def set_memory(self, memory):
+        """Set the memory object @memory"""
+        pass
+
+    def get_mapping_models(self):
+        """Returns a list of MappingModel objects (or an empty list)"""
+        if hasattr(self, "get_bank_model"):
+            # FIXME: Backwards compatibility for old bank models
+            bank_model = self.get_bank_model()
+            if bank_model:
+                return [bank_model]
+        return []
+
+    def get_raw_memory(self, number):
+        """Return a raw string describing the memory at @number"""
+        pass
+
+    def filter_name(self, name):
+        """Filter @name to just the length and characters supported"""
+        rf = self.get_features()
+        if rf.valid_characters == rf.valid_characters.upper():
+            # Radio only supports uppercase, so help out here
+            name = name.upper()
+        return "".join([x for x in name[:rf.valid_name_length]
+                        if x in rf.valid_characters])
+
+    def get_sub_devices(self):
+        """Return a list of sub-device Radio objects, if
+        RadioFeatures.has_sub_devices is True"""
+        return []
+
+    def validate_memory(self, mem):
+        """Return a list of warnings and errors that will be encoundered
+        if trying to set @mem on the current radio"""
+        rf = self.get_features()
+        return rf.validate_memory(mem)
+
+    def get_settings(self):
+        """Returns a RadioSettings list containing one or more
+        RadioSettingGroup or RadioSetting objects. These represent general
+        setting knobs and dials that can be adjusted on the radio. If this
+        function is implemented, the has_settings RadioFeatures flag should
+        be True and set_settings() must be implemented as well."""
+        pass
+
+    def set_settings(self, settings):
+        """Accepts the top-level RadioSettingGroup returned from get_settings()
+        and adjusts the values in the radio accordingly. This function expects
+        the entire RadioSettingGroup hierarchy returned from get_settings().
+        If this function is implemented, the has_settings RadioFeatures flag
+        should be True and get_settings() must be implemented as well."""
+        pass
+
+
+class FileBackedRadio(Radio):
+    """A file-backed radio stores its data in a file"""
+    FILE_EXTENSION = "img"
+
+    def __init__(self, *args, **kwargs):
+        Radio.__init__(self, *args, **kwargs)
+        self._memobj = None
+
+    def save(self, filename):
+        """Save the radio's memory map to @filename"""
+        self.save_mmap(filename)
+
+    def load(self, filename):
+        """Load the radio's memory map object from @filename"""
+        self.load_mmap(filename)
+
+    def process_mmap(self):
+        """Process a newly-loaded or downloaded memory map"""
+        pass
+
+    def load_mmap(self, filename):
+        """Load the radio's memory map from @filename"""
+        mapfile = file(filename, "rb")
+        self._mmap = memmap.MemoryMap(mapfile.read())
+        mapfile.close()
+        self.process_mmap()
+
+    def save_mmap(self, filename):
+        """
+        try to open a file and write to it
+        If IOError raise a File Access Error Exception
+        """
+        try:
+            mapfile = file(filename, "wb")
+            mapfile.write(self._mmap.get_packed())
+            mapfile.close()
+        except IOError:
+            raise Exception("File Access Error")
+
+    def get_mmap(self):
+        """Return the radio's memory map object"""
+        return self._mmap
+
+
+class CloneModeRadio(FileBackedRadio):
+    """A clone-mode radio does a full memory dump in and out and we store
+    an image of the radio into an image file"""
+
+    _memsize = 0
+
+    def __init__(self, pipe):
+        self.errors = []
+        self._mmap = None
+
+        if isinstance(pipe, str):
+            self.pipe = None
+            self.load_mmap(pipe)
+        elif isinstance(pipe, memmap.MemoryMap):
+            self.pipe = None
+            self._mmap = pipe
+            self.process_mmap()
+        else:
+            FileBackedRadio.__init__(self, pipe)
+
+    def get_memsize(self):
+        """Return the radio's memory size"""
+        return self._memsize
+
+    @classmethod
+    def match_model(cls, filedata, filename):
+        """Given contents of a stored file (@filedata), return True if
+        this radio driver handles the represented model"""
+
+        # Unless the radio driver does something smarter, claim
+        # support if the data is the same size as our memory.
+        # Ideally, each radio would perform an intelligent analysis to
+        # make this determination to avoid model conflicts with
+        # memories of the same size.
+        return len(filedata) == cls._memsize
+
+    def sync_in(self):
+        "Initiate a radio-to-PC clone operation"
+        pass
+
+    def sync_out(self):
+        "Initiate a PC-to-radio clone operation"
+        pass
+
+
+class LiveRadio(Radio):
+    """Base class for all Live-Mode radios"""
+    pass
+
+
+class NetworkSourceRadio(Radio):
+    """Base class for all radios based on a network source"""
+
+    def do_fetch(self):
+        """Fetch the source data from the network"""
+        pass
+
+
+class IcomDstarSupport:
+    """Base interface for radios supporting Icom's D-STAR technology"""
+    MYCALL_LIMIT = (1, 1)
+    URCALL_LIMIT = (1, 1)
+    RPTCALL_LIMIT = (1, 1)
+
+    def get_urcall_list(self):
+        """Return a list of URCALL callsigns"""
+        return []
+
+    def get_repeater_call_list(self):
+        """Return a list of RPTCALL callsigns"""
+        return []
+
+    def get_mycall_list(self):
+        """Return a list of MYCALL callsigns"""
+        return []
+
+    def set_urcall_list(self, calls):
+        """Set the URCALL callsign list"""
+        pass
+
+    def set_repeater_call_list(self, calls):
+        """Set the RPTCALL callsign list"""
+        pass
+
+    def set_mycall_list(self, calls):
+        """Set the MYCALL callsign list"""
+        pass
+
+
+class ExperimentalRadio:
+    """Interface for experimental radios"""
+    @classmethod
+    def get_experimental_warning(cls):
+        return ("This radio's driver is marked as experimental and may " +
+                "be unstable or unsafe to use.")
+
+
+class Status:
+    """Clone status object for conveying clone progress to the UI"""
+    name = "Job"
+    msg = "Unknown"
+    max = 100
+    cur = 0
+
+    def __str__(self):
+        try:
+            pct = (self.cur / float(self.max)) * 100
+            nticks = int(pct) / 10
+            ticks = "=" * nticks
+        except ValueError:
+            pct = 0.0
+            ticks = "?" * 10
+
+        return "|%-10s| %2.1f%% %s" % (ticks, pct, self.msg)
+
+
+def is_fractional_step(freq):
+    """Returns True if @freq requires a 12.5kHz or 6.25kHz step"""
+    return not is_5_0(freq) and (is_12_5(freq) or is_6_25(freq))
+
+
+def is_5_0(freq):
+    """Returns True if @freq is reachable by a 5kHz step"""
+    return (freq % 5000) == 0
+
+
+def is_12_5(freq):
+    """Returns True if @freq is reachable by a 12.5kHz step"""
+    return (freq % 12500) == 0
+
+
+def is_6_25(freq):
+    """Returns True if @freq is reachable by a 6.25kHz step"""
+    return (freq % 6250) == 0
+
+
+def is_2_5(freq):
+    """Returns True if @freq is reachable by a 2.5kHz step"""
+    return (freq % 2500) == 0
+
+
+def required_step(freq):
+    """Returns the simplest tuning step that is required to reach @freq"""
+    if is_5_0(freq):
+        return 5.0
+    elif is_12_5(freq):
+        return 12.5
+    elif is_6_25(freq):
+        return 6.25
+    elif is_2_5(freq):
+        return 2.5
+    else:
+        raise errors.InvalidDataError("Unable to calculate the required " +
+                                      "tuning step for %i.%5i" %
+                                      (freq / 1000000, freq % 1000000))
+
+
+def fix_rounded_step(freq):
+    """Some radios imply the last bit of 12.5kHz and 6.25kHz step
+    frequencies. Take the base @freq and return the corrected one"""
+    try:
+        required_step(freq)
+        return freq
+    except errors.InvalidDataError:
+        pass
+
+    try:
+        required_step(freq + 500)
+        return freq + 500
+    except errors.InvalidDataError:
+        pass
+
+    try:
+        required_step(freq + 250)
+        return freq + 250
+    except errors.InvalidDataError:
+        pass
+
+    try:
+        required_step(freq + 750)
+        return float(freq + 750)
+    except errors.InvalidDataError:
+        pass
+
+    raise errors.InvalidDataError("Unable to correct rounded frequency " +
+                                  format_freq(freq))
+
+
+def _name(name, len, just_upper):
+    """Justify @name to @len, optionally converting to all uppercase"""
+    if just_upper:
+        name = name.upper()
+    return name.ljust(len)[:len]
+
+
+def name6(name, just_upper=True):
+    """6-char name"""
+    return _name(name, 6, just_upper)
+
+
+def name8(name, just_upper=False):
+    """8-char name"""
+    return _name(name, 8, just_upper)
+
+
+def name16(name, just_upper=False):
+    """16-char name"""
+    return _name(name, 16, just_upper)
+
+
+def to_GHz(val):
+    """Convert @val in GHz to Hz"""
+    return val * 1000000000
+
+
+def to_MHz(val):
+    """Convert @val in MHz to Hz"""
+    return val * 1000000
+
+
+def to_kHz(val):
+    """Convert @val in kHz to Hz"""
+    return val * 1000
+
+
+def from_GHz(val):
+    """Convert @val in Hz to GHz"""
+    return val / 100000000
+
+
+def from_MHz(val):
+    """Convert @val in Hz to MHz"""
+    return val / 100000
+
+
+def from_kHz(val):
+    """Convert @val in Hz to kHz"""
+    return val / 100
+
+
+def split_tone_decode(mem, txtone, rxtone):
+    """
+    Set tone mode and values on @mem based on txtone and rxtone specs like:
+    None, None, None
+    "Tone", 123.0, None
+    "DTCS", 23, "N"
+    """
+    txmode, txval, txpol = txtone
+    rxmode, rxval, rxpol = rxtone
+
+    mem.dtcs_polarity = "%s%s" % (txpol or "N", rxpol or "N")
+
+    if not txmode and not rxmode:
+        # No tone
+        return
+
+    if txmode == "Tone" and not rxmode:
+        mem.tmode = "Tone"
+        mem.rtone = txval
+        return
+
+    if txmode == rxmode == "Tone" and txval == rxval:
+        # TX and RX same tone -> TSQL
+        mem.tmode = "TSQL"
+        mem.ctone = txval
+        return
+
+    if txmode == rxmode == "DTCS" and txval == rxval:
+        mem.tmode = "DTCS"
+        mem.dtcs = txval
+        return
+
+    mem.tmode = "Cross"
+    mem.cross_mode = "%s->%s" % (txmode or "", rxmode or "")
+
+    if txmode == "Tone":
+        mem.rtone = txval
+    elif txmode == "DTCS":
+        mem.dtcs = txval
+
+    if rxmode == "Tone":
+        mem.ctone = rxval
+    elif rxmode == "DTCS":
+        mem.rx_dtcs = rxval
+
+
+def split_tone_encode(mem):
+    """
+    Returns TX, RX tone specs based on @mem like:
+    None, None, None
+    "Tone", 123.0, None
+    "DTCS", 23, "N"
+    """
+
+    txmode = ''
+    rxmode = ''
+    txval = None
+    rxval = None
+
+    if mem.tmode == "Tone":
+        txmode = "Tone"
+        txval = mem.rtone
+    elif mem.tmode == "TSQL":
+        txmode = rxmode = "Tone"
+        txval = rxval = mem.ctone
+    elif mem.tmode == "DTCS":
+        txmode = rxmode = "DTCS"
+        txval = rxval = mem.dtcs
+    elif mem.tmode == "Cross":
+        txmode, rxmode = mem.cross_mode.split("->", 1)
+        if txmode == "Tone":
+            txval = mem.rtone
+        elif txmode == "DTCS":
+            txval = mem.dtcs
+        if rxmode == "Tone":
+            rxval = mem.ctone
+        elif rxmode == "DTCS":
+            rxval = mem.rx_dtcs
+
+    if txmode == "DTCS":
+        txpol = mem.dtcs_polarity[0]
+    else:
+        txpol = None
+    if rxmode == "DTCS":
+        rxpol = mem.dtcs_polarity[1]
+    else:
+        rxpol = None
+
+    return ((txmode, txval, txpol),
+            (rxmode, rxval, rxpol))
+
+
+def sanitize_string(astring, validcharset=CHARSET_ASCII, replacechar='*'):
+    myfilter = ''.join(
+        [
+            [replacechar, chr(x)][chr(x) in validcharset]
+            for x in xrange(256)
+        ])
+    return astring.translate(myfilter)
diff -r b08fbd75a499 -r ed11ed06d671 chirp/drivers/ft450d.py
--- a/chirp/drivers/ft450d.py	Wed Jun 13 06:14:11 2018 -0700
+++ b/chirp/drivers/ft450d.py	Tue Jun 19 14:27:25 2018 -0700
@@ -370,9 +370,20 @@
     @classmethod
     def get_prompts(cls):
         rp = chirp_common.RadioPrompts()
+        rp.info = _(dedent("""
+            The FT-450 radio driver loads the 'Special Channels' tab
+            with the PMS scanning range memories (group 11), 60meter
+            channels (group 12), the QMB (STO/RCL) memory, the HF and
+            50m HOME memories and all the A and B VFO memories.
+            There are VFO memories for the last frequency dialed in
+            each band. The last mem-tune config is also stored.
+            These Special Channels allow limited field editting.
+            This driver also populates the 'Other' tab in the channel
+            memory Properties window. This tab contains values for
+            those channel memory settings that don't fall under the
+            standard Chirp display columns.
+            """))
         rp.pre_download = _(dedent("""\
-            Note that this radio has 'Special Channels' and generates an
-            'Other' tab in the channel memory Properties window...
             1. Turn radio off.
             2. Connect cable to ACC jack.
             3. Press and hold in the [MODE &lt;] and [MODE &gt;] keys while
diff -r b08fbd75a499 -r ed11ed06d671 chirp/drivers/lt725uv.py
--- a/chirp/drivers/lt725uv.py	Wed Jun 13 06:14:11 2018 -0700
+++ b/chirp/drivers/lt725uv.py	Tue Jun 19 14:27:25 2018 -0700
@@ -1,1446 +1,1445 @@
-# Copyright 2016:
-# * Jim Unroe KC9HI, <rock.unroe at gmail.com>
-# Modified for Baojie BJ-218: 2018 by Rick DeWitt (RJD), <aa0rd at yahoo.com>#
-# This program is free software: you can redistribute it and/or modify
-# it under the terms of the GNU General Public License as published by
-# the Free Software Foundation, either version 2 of the License, or
-# (at your option) any later version.
-#
-# This program is distributed in the hope that it will be useful,
-# but WITHOUT ANY WARRANTY; without even the implied warranty of
-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
-# GNU General Public License for more details.
-#
-# You should have received a copy of the GNU General Public License
-# along with this program.  If not, see <http://www.gnu.org/licenses/>.
-
-import time
-import struct
-import logging
-import re
-
-LOG = logging.getLogger(__name__)
-
-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
-
-MEM_FORMAT = """
-#seekto 0x0200;
-struct {
-  u8  init_bank;
-  u8  volume;
-  u16 fm_freq;
-  u8  wtled;
-  u8  rxled;
-  u8  txled;
-  u8  ledsw;
-  u8  beep;
-  u8  ring;
-  u8  bcl;
-  u8  tot;
-  u16 sig_freq;
-  u16 dtmf_txms;
-  u8  init_sql;
-  u8  rptr_mode;
-} settings;
-
-#seekto 0x0240;
-struct {
-  u8  dtmf1_cnt;
-  u8  dtmf1[7];
-  u8  dtmf2_cnt;
-  u8  dtmf2[7];
-  u8  dtmf3_cnt;
-  u8  dtmf3[7];
-  u8  dtmf4_cnt;
-  u8  dtmf4[7];
-  u8  dtmf5_cnt;
-  u8  dtmf5[7];
-  u8  dtmf6_cnt;
-  u8  dtmf6[7];
-  u8  dtmf7_cnt;
-  u8  dtmf7[7];
-  u8  dtmf8_cnt;
-  u8  dtmf8[7];
-} dtmf_tab;
-
-#seekto 0x0280;
-struct {
-  u8  native_id_cnt;
-  u8  native_id_code[7];
-  u8  master_id_cnt;
-  u8  master_id_code[7];
-  u8  alarm_cnt;
-  u8  alarm_code[5];
-  u8  id_disp_cnt;
-  u8  id_disp_code[5];
-  u8  revive_cnt;
-  u8  revive_code[5];
-  u8  stun_cnt;
-  u8  stun_code[5];
-  u8  kill_cnt;
-  u8  kill_code[5];
-  u8  monitor_cnt;
-  u8  monitor_code[5];
-  u8  state_now;
-} codes;
-
-#seekto 0x02d0;
-struct {
-  u8  hello1_cnt;
-  char  hello1[7];
-  u8  hello2_cnt;
-  char  hello2[7];
-  u32  vhf_low;
-  u32  vhf_high;
-  u32  uhf_low;
-  u32  uhf_high;
-  u8  lims_on;
-} hello_lims;
-
-struct vfo {
-  u8  frq_chn_mode;
-  u8  chan_num;
-  u32 rxfreq;
-  u16 is_rxdigtone:1,
-      rxdtcs_pol:1,
-      rx_tone:14;
-  u8  rx_mode;
-  u8  unknown_ff;
-  u16 is_txdigtone:1,
-      txdtcs_pol:1,
-      tx_tone:14;
-  u8  launch_sig;
-  u8  tx_end_sig;
-  u8  bpower;
-  u8  fm_bw;
-  u8  cmp_nder;
-  u8  scrm_blr;
-  u8  shift;
-  u32 offset;
-  u16 step;
-  u8  sql;
-};
-
-#seekto 0x0300;
-struct {
-  struct vfo vfoa;
-} upper;
-
-#seekto 0x0380;
-struct {
-  struct vfo vfob;
-} lower;
-
-struct mem {
-  u32 rxfreq;
-  u16 is_rxdigtone:1,
-      rxdtcs_pol:1,
-      rxtone:14;
-  u8  recvmode;
-  u32 txfreq;
-  u16 is_txdigtone:1,
-      txdtcs_pol:1,
-      txtone:14;
-  u8  botsignal;
-  u8  eotsignal;
-  u8  power:1,
-      wide:1,
-      compandor:1
-      scrambler:1
-      unknown:4;
-  u8  namelen;
-  u8  name[7];
-};
-
-#seekto 0x0400;
-struct mem upper_memory[128];
-
-#seekto 0x1000;
-struct mem lower_memory[128];
-
-#seekto 0x1C00;
-struct {
-  char  mod_num[6];
-} mod_id;
-"""
-
-MEM_SIZE = 0x1C00
-BLOCK_SIZE = 0x40
-STIMEOUT = 2
-# Channel power: 2 levels
-POWER_LEVELS = [chirp_common.PowerLevel("Low", watts=5.00),
-                chirp_common.PowerLevel("High", watts=30.00)]
-
-LIST_RECVMODE = ["QT/DQT", "QT/DQT + Signaling"]
-LIST_SIGNAL = ["Off"] + ["DTMF%s" % x for x in range(1, 9)] + \
-              ["DTMF%s + Identity" % x for x in range(1, 9)] + \
-              ["Identity code"]
-# Band Power settings, can be different than channel power
-LIST_BPOWER = ["Low", "Mid", "High"]    # Tri-power models
-LIST_COLOR = ["Off", "Orange", "Blue", "Purple"]
-LIST_LEDSW = ["Auto", "On"]
-LIST_RING = ["Off"] + ["%s" % x for x in range(1, 10)]
-LIST_TDR_DEF = ["A-Upper", "B-Lower"]
-LIST_TIMEOUT = ["Off"] + ["%s" % x for x in range(30, 630, 30)]
-LIST_VFOMODE = ["Frequency Mode", "Channel Mode"]
-# Tones are numeric, Defined in \chirp\chirp_common.py
-TONES_CTCSS = sorted(chirp_common.TONES)
-# Converted to strings
-LIST_CTCSS = ["Off"] + [str(x) for x in TONES_CTCSS]
-# Now append the DxxxN and DxxxI DTCS codes from chirp_common
-for x in chirp_common.DTCS_CODES:
-    LIST_CTCSS.append("D{:03d}N".format(x))
-for x in chirp_common.DTCS_CODES:
-    LIST_CTCSS.append("D{:03d}R".format(x))
-LIST_BW = ["Narrow", "Wide"]
-LIST_SHIFT = ["Off"," + ", " - "]
-STEPS = [2.5, 5.0, 6.25, 10.0, 12.5, 20.0, 25.0, 50.0]
-LIST_STEPS = [str(x) for x in STEPS]
-LIST_STATE = ["Normal", "Stun", "Kill"]
-LIST_SSF = ["1000", "1450", "1750", "2100"]
-LIST_DTMFTX = ["50", "100", "150", "200", "300","500"]
-
-SETTING_LISTS = {
-"init_bank": LIST_TDR_DEF ,
-"tot": LIST_TIMEOUT,
-"wtled": LIST_COLOR,
-"rxled": LIST_COLOR,
-"txled": LIST_COLOR,
-"sig_freq": LIST_SSF,
-"dtmf_txms": LIST_DTMFTX,
-"ledsw": LIST_LEDSW,
-"frq_chn_mode": LIST_VFOMODE,
-"rx_tone": LIST_CTCSS,
-"tx_tone": LIST_CTCSS,
-"rx_mode": LIST_RECVMODE,
-"launch_sig": LIST_SIGNAL,
-"tx_end_sig": LIST_SIGNAL,
-"bpower":LIST_BPOWER,
-"fm_bw": LIST_BW,
-"shift": LIST_SHIFT,
-"step": LIST_STEPS,
-"ring": LIST_RING,
-"state_now": LIST_STATE
-}
-
-def _clean_buffer(radio):
-    radio.pipe.timeout = 0.005
-    junk = radio.pipe.read(256)
-    radio.pipe.timeout = STIMEOUT
-    if junk:
-        Log.debug("Got %i bytes of junk before starting" % len(junk))
-
-
-def _rawrecv(radio, amount):
-    """Raw read from the radio device"""
-    data = ""
-    try:
-        data = radio.pipe.read(amount)
-    except:
-        _exit_program_mode(radio)
-        msg = "Generic error reading data from radio; check your cable."
-        raise errors.RadioError(msg)
-
-    if len(data) != amount:
-        _exit_program_mode(radio)
-        msg = "Error reading from radio: not the amount of data we want."
-        raise errors.RadioError(msg)
-
-    return data
-
-
-def _rawsend(radio, data):
-    """Raw send to the radio device"""
-    try:
-        radio.pipe.write(data)
-    except:
-        raise errors.RadioError("Error sending data to radio")
-
-
-def _make_frame(cmd, addr, length, data=""):
-    """Pack the info in the headder format"""
-    frame = struct.pack(">4sHH", cmd, addr, length)
-    # Add the data if set
-    if len(data) != 0:
-        frame += data
-    # Return the data
-    return frame
-
-
-def _recv(radio, addr, length):
-    """Get data from the radio """
-
-    data = _rawrecv(radio, length)
-
-    # DEBUG
-    LOG.info("Response:")
-    LOG.debug(util.hexprint(data))
-
-    return data
-
-
-def _do_ident(radio):
-    """Put the radio in PROGRAM mode & identify it"""
-    # Set the serial discipline
-    radio.pipe.baudrate = 19200
-    radio.pipe.parity = "N"
-    radio.pipe.timeout = STIMEOUT
-
-    # Flush input buffer
-    _clean_buffer(radio)
-
-    magic = "PROM_LIN"
-
-    _rawsend(radio, magic)
-
-    ack = _rawrecv(radio, 1)
-    if ack != "\x06":
-        _exit_program_mode(radio)
-        if ack:
-            LOG.debug(repr(ack))
-        raise errors.RadioError("Radio did not respond")
-
-    return True
-
-
-def _exit_program_mode(radio):
-    endframe = "EXIT"
-    _rawsend(radio, endframe)
-
-
-def _download(radio):
-    """Get the memory map"""
-
-    # Put radio in program mode and identify it
-    _do_ident(radio)
-
-    # UI progress
-    status = chirp_common.Status()
-    status.cur = 0
-    status.max = MEM_SIZE / BLOCK_SIZE
-    status.msg = "Cloning from radio..."
-    radio.status_fn(status)
-
-    data = ""
-    for addr in range(0, MEM_SIZE, BLOCK_SIZE):
-        frame = _make_frame("READ", addr, BLOCK_SIZE)
-        # DEBUG
-        LOG.info("Request sent:")
-        LOG.debug(util.hexprint(frame))
-
-        # Sending the read request
-        _rawsend(radio, frame)
-
-        # Now we read
-        d = _recv(radio, addr, BLOCK_SIZE)
-
-        # Aggregate the data
-        data += d
-
-        # UI Update
-        status.cur = addr / BLOCK_SIZE
-        status.msg = "Cloning from radio..."
-        radio.status_fn(status)
-
-    _exit_program_mode(radio)
-
-    data += radio.MODEL.ljust(8)
-
-    return data
-
-
-def _upload(radio):
-    """Upload procedure"""
-
-    # Put radio in program mode and identify it
-    _do_ident(radio)
-
-    # UI progress
-    status = chirp_common.Status()
-    status.cur = 0
-    status.max = MEM_SIZE / BLOCK_SIZE
-    status.msg = "Cloning to radio..."
-    radio.status_fn(status)
-
-    # The fun starts here
-    for addr in range(0, MEM_SIZE, BLOCK_SIZE):
-        # Sending the data
-        data = radio.get_mmap()[addr:addr + BLOCK_SIZE]
-
-        frame = _make_frame("WRIE", addr, BLOCK_SIZE, data)
-
-        _rawsend(radio, frame)
-
-        # Receiving the response
-        ack = _rawrecv(radio, 1)
-        if ack != "\x06":
-            _exit_program_mode(radio)
-            msg = "Bad ack writing block 0x%04x" % addr
-            raise errors.RadioError(msg)
-
-        # UI Update
-        status.cur = addr / BLOCK_SIZE
-        status.msg = "Cloning to radio..."
-        radio.status_fn(status)
-
-    _exit_program_mode(radio)
-
-
-def model_match(cls, data):
-    """Match the opened/downloaded image to the correct version"""
-    if len(data) == 0x1C08:
-        rid = data[0x1C00:0x1C08]
-        return rid.startswith(cls.MODEL)
-    else:
-        return False
-
-
-def _split(rf, f1, f2):
-    """Returns False if the two freqs are in the same band (no split)
-    or True otherwise"""
-
-    # Determine if the two freqs are in the same band
-    for low, high in rf.valid_bands:
-        if f1 >= low and f1 <= high and \
-                f2 >= low and f2 <= high:
-            # If the two freqs are on the same Band this is not a split
-            return False
-
-    # If you get here is because the freq pairs are split
-    return True
-
-
- at directory.register
-class LT725UV(chirp_common.CloneModeRadio,
-              chirp_common.ExperimentalRadio):
-    """LUITON LT-725UV Radio"""
-    VENDOR = "LUITON"
-    MODEL = "LT-725UV"
-    MODES = ["NFM", "FM"]
-    TONES = chirp_common.TONES
-    DTCS_CODES = sorted(chirp_common.DTCS_CODES + [645])
-    NAME_LENGTH = 7
-    DTMF_CHARS = list("0123456789ABCD*#")
-
-    VALID_BANDS = [(136000000, 176000000),
-                   (400000000, 480000000)]
-
-    # Valid chars on the LCD
-    VALID_CHARS = chirp_common.CHARSET_ALPHANUMERIC + \
-        "`{|}!\"#$%&'()*+,-./:;<=>?@[]^_"
-
-    @classmethod
-    def get_prompts(cls):
-        rp = chirp_common.RadioPrompts()
-        rp.experimental = \
-            ('Some notes about POWER settings:\n'
-             '- The individual channel power settings are ignored'
-             ' by the radio.\n'
-             '  They are allowed to be set (and downloaded) in hopes of'
-             ' a future firmware update.\n'
-             '- Power settings done \'Live\' in the radio apply to the'
-             ' entire upper or lower band.\n'
-             '- Tri-power radio models will set and download the three'
-             ' band-power'
-             ' levels, but they are\n  converted to just Low and High at'
-             ' upload.'
-             ' The Mid setting reverts to Low.'
-             )
-
-        rp.pre_download = _(dedent("""\
-            Follow this instructions to download your info:
-
-            1 - Turn off your radio
-            2 - Connect your interface cable
-            3 - Turn on your radio
-            4 - Do the download of your radio data
-            """))
-        rp.pre_upload = _(dedent("""\
-            Follow this instructions to upload your info:
-
-            1 - Turn off your radio
-            2 - Connect your interface cable
-            3 - Turn on your radio
-            4 - Do the upload of your radio data
-            """))
-        return rp
-
-    def get_features(self):
-        rf = chirp_common.RadioFeatures()
-        rf.has_settings = True
-        rf.has_bank = False
-        rf.has_tuning_step = False
-        rf.can_odd_split = True
-        rf.has_name = True
-        rf.has_offset = True
-        rf.has_mode = True
-        rf.has_dtcs = True
-        rf.has_rx_dtcs = True
-        rf.has_dtcs_polarity = True
-        rf.has_ctone = True
-        rf.has_cross = True
-        rf.has_sub_devices = self.VARIANT == ""
-        rf.valid_modes = self.MODES
-        rf.valid_characters = self.VALID_CHARS
-        rf.valid_duplexes = ["", "-", "+", "split", "off"]
-        rf.valid_tmodes = ['', 'Tone', 'TSQL', 'DTCS', 'Cross']
-        rf.valid_cross_modes = [
-            "Tone->Tone",
-            "DTCS->",
-            "->DTCS",
-            "Tone->DTCS",
-            "DTCS->Tone",
-            "->Tone",
-            "DTCS->DTCS"]
-        rf.valid_skips = []
-        rf.valid_power_levels = POWER_LEVELS
-        rf.valid_name_length = self.NAME_LENGTH
-        rf.valid_dtcs_codes = self.DTCS_CODES
-        rf.valid_bands = self.VALID_BANDS
-        rf.memory_bounds = (1, 128)
-        return rf
-
-    def get_sub_devices(self):
-        return [LT725UVUpper(self._mmap), LT725UVLower(self._mmap)]
-
-    def sync_in(self):
-        """Download from radio"""
-        try:
-            data = _download(self)
-        except errors.RadioError:
-            # Pass through any real errors we raise
-            raise
-        except:
-            # 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()
-
-    def sync_out(self):
-        """Upload to radio"""
-        try:
-            _upload(self)
-        except:
-            # 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')
-
-    def process_mmap(self):
-        """Process the mem map into the mem object"""
-        self._memobj = bitwise.parse(MEM_FORMAT, self._mmap)
-
-    def get_raw_memory(self, number):
-        return repr(self._memobj.memory[number - 1])
-
-    def _memory_obj(self, suffix=""):
-        return getattr(self._memobj, "%s_memory%s" % (self._vfo, suffix))
-
-    def _get_dcs(self, val):
-        return int(str(val)[2:-18])
-
-    def _set_dcs(self, val):
-        return int(str(val), 16)
-
-    def get_memory(self, number):
-        _mem = self._memory_obj()[number - 1]
-
-        mem = chirp_common.Memory()
-        mem.number = number
-
-        if _mem.get_raw()[0] == "\xff":
-            mem.empty = True
-            return mem
-
-        mem.freq = int(_mem.rxfreq) * 10
-
-        if _mem.txfreq == 0xFFFFFFFF:
-            # TX freq not set
-            mem.duplex = "off"
-            mem.offset = 0
-        elif int(_mem.rxfreq) == int(_mem.txfreq):
-            mem.duplex = ""
-            mem.offset = 0
-        elif _split(self.get_features(), mem.freq, int(_mem.txfreq) * 10):
-            mem.duplex = "split"
-            mem.offset = int(_mem.txfreq) * 10
-        else:
-            mem.duplex = int(_mem.rxfreq) > int(_mem.txfreq) and "-" or "+"
-            mem.offset = abs(int(_mem.rxfreq) - int(_mem.txfreq)) * 10
-
-        for char in _mem.name[:_mem.namelen]:
-            mem.name += chr(char)
-
-        dtcs_pol = ["N", "N"]
-
-        if _mem.rxtone == 0x3FFF:
-            rxmode = ""
-        elif _mem.is_rxdigtone == 0:
-            # CTCSS
-            rxmode = "Tone"
-            mem.ctone = int(_mem.rxtone) / 10.0
-        else:
-            # Digital
-            rxmode = "DTCS"
-            mem.rx_dtcs = self._get_dcs(_mem.rxtone)
-            if _mem.rxdtcs_pol == 1:
-                dtcs_pol[1] = "R"
-
-        if _mem.txtone == 0x3FFF:
-            txmode = ""
-        elif _mem.is_txdigtone == 0:
-            # CTCSS
-            txmode = "Tone"
-            mem.rtone = int(_mem.txtone) / 10.0
-        else:
-            # Digital
-            txmode = "DTCS"
-            mem.dtcs = self._get_dcs(_mem.txtone)
-            if _mem.txdtcs_pol == 1:
-                dtcs_pol[0] = "R"
-
-        if txmode == "Tone" and not rxmode:
-            mem.tmode = "Tone"
-        elif txmode == rxmode and txmode == "Tone" and mem.rtone == mem.ctone:
-            mem.tmode = "TSQL"
-        elif txmode == rxmode and txmode == "DTCS" and mem.dtcs == mem.rx_dtcs:
-            mem.tmode = "DTCS"
-        elif rxmode or txmode:
-            mem.tmode = "Cross"
-            mem.cross_mode = "%s->%s" % (txmode, rxmode)
-
-        mem.dtcs_polarity = "".join(dtcs_pol)
-
-        mem.mode = _mem.wide and "FM" or "NFM"
-
-        mem.power = POWER_LEVELS[_mem.power]
-
-        # Extra
-        mem.extra = RadioSettingGroup("extra", "Extra")
-
-        if _mem.recvmode == 0xFF:
-            val = 0x00
-        else:
-            val = _mem.recvmode
-        recvmode = RadioSetting("recvmode", "Receiving mode",
-                                RadioSettingValueList(LIST_RECVMODE,
-                                    LIST_RECVMODE[val]))
-        mem.extra.append(recvmode)
-
-        if _mem.botsignal == 0xFF:
-            val = 0x00
-        else:
-            val = _mem.botsignal
-        botsignal = RadioSetting("botsignal", "Launch signaling",
-                                 RadioSettingValueList(LIST_SIGNAL,
-                                     LIST_SIGNAL[val]))
-        mem.extra.append(botsignal)
-
-        if _mem.eotsignal == 0xFF:
-            val = 0x00
-        else:
-            val = _mem.eotsignal
-
-        rx = RadioSettingValueList(LIST_SIGNAL, LIST_SIGNAL[val])
-        eotsignal = RadioSetting("eotsignal", "Transmit end signaling", rx)
-        mem.extra.append(eotsignal)
-
-        rx = RadioSettingValueBoolean(bool(_mem.compandor))
-        compandor = RadioSetting("compandor", "Compandor", rx)
-        mem.extra.append(compandor)
-
-        rx = RadioSettingValueBoolean(bool(_mem.scrambler))
-        scrambler = RadioSetting("scrambler", "Scrambler", rx)
-        mem.extra.append(scrambler)
-
-        return mem
-
-    def set_memory(self, mem):
-        _mem = self._memory_obj()[mem.number - 1]
-
-        if mem.empty:
-            _mem.set_raw("\xff" * 24)
-            _mem.namelen = 0
-            return
-
-        _mem.set_raw("\xFF" * 15 + "\x00\x00" + "\xFF" * 7)
-
-        _mem.rxfreq = mem.freq / 10
-        if mem.duplex == "off":
-            _mem.txfreq = 0xFFFFFFFF
-        elif mem.duplex == "split":
-            _mem.txfreq = mem.offset / 10
-        elif mem.duplex == "+":
-            _mem.txfreq = (mem.freq + mem.offset) / 10
-        elif mem.duplex == "-":
-            _mem.txfreq = (mem.freq - mem.offset) / 10
-        else:
-            _mem.txfreq = mem.freq / 10
-
-        _mem.namelen = len(mem.name)
-        _namelength = self.get_features().valid_name_length
-        for i in range(_namelength):
-            try:
-                _mem.name[i] = ord(mem.name[i])
-            except IndexError:
-                _mem.name[i] = 0xFF
-
-        rxmode = ""
-        txmode = ""
-
-        if mem.tmode == "Tone":
-            txmode = "Tone"
-        elif mem.tmode == "TSQL":
-            rxmode = "Tone"
-            txmode = "TSQL"
-        elif mem.tmode == "DTCS":
-            rxmode = "DTCSSQL"
-            txmode = "DTCS"
-        elif mem.tmode == "Cross":
-            txmode, rxmode = mem.cross_mode.split("->", 1)
-
-        if rxmode == "":
-            _mem.rxdtcs_pol = 1
-            _mem.is_rxdigtone = 1
-            _mem.rxtone = 0x3FFF
-        elif rxmode == "Tone":
-            _mem.rxdtcs_pol = 0
-            _mem.is_rxdigtone = 0
-            _mem.rxtone = int(mem.ctone * 10)
-        elif rxmode == "DTCSSQL":
-            _mem.rxdtcs_pol = 1 if mem.dtcs_polarity[1] == "R" else 0
-            _mem.is_rxdigtone = 1
-            _mem.rxtone = self._set_dcs(mem.dtcs)
-        elif rxmode == "DTCS":
-            _mem.rxdtcs_pol = 1 if mem.dtcs_polarity[1] == "R" else 0
-            _mem.is_rxdigtone = 1
-            _mem.rxtone = self._set_dcs(mem.rx_dtcs)
-
-        if txmode == "":
-            _mem.txdtcs_pol = 1
-            _mem.is_txdigtone = 1
-            _mem.txtone = 0x3FFF
-        elif txmode == "Tone":
-            _mem.txdtcs_pol = 0
-            _mem.is_txdigtone = 0
-            _mem.txtone = int(mem.rtone * 10)
-        elif txmode == "TSQL":
-            _mem.txdtcs_pol = 0
-            _mem.is_txdigtone = 0
-            _mem.txtone = int(mem.ctone * 10)
-        elif txmode == "DTCS":
-            _mem.txdtcs_pol = 1 if mem.dtcs_polarity[0] == "R" else 0
-            _mem.is_txdigtone = 1
-            _mem.txtone = self._set_dcs(mem.dtcs)
-
-        _mem.wide = self.MODES.index(mem.mode)
-        _mem.power = mem.power == POWER_LEVELS[1]
-
-        # Extra settings
-        for setting in mem.extra:
-            setattr(_mem, setting.get_name(), setting.value)
-
-    def get_settings(self):
-        """Translate the bit in the mem_struct into settings in the UI"""
-        # Define mem struct write-back shortcuts
-        _sets = self._memobj.settings
-        _vfoa = self._memobj.upper.vfoa
-        _vfob = self._memobj.lower.vfob
-        _lims = self._memobj.hello_lims
-        _codes = self._memobj.codes
-        _dtmf = self._memobj.dtmf_tab
-
-        basic = RadioSettingGroup("basic", "Basic Settings")
-        a_band = RadioSettingGroup("a_band", "VFO A-Upper Settings")
-        b_band = RadioSettingGroup("b_band", "VFO B-Lower Settings")
-        codes = RadioSettingGroup("codes", "Codes & DTMF Groups")
-        lims = RadioSettingGroup("lims", "PowerOn & Freq Limits")
-        group = RadioSettings(basic, a_band, b_band, lims, codes)
-
-        # Basic Settings
-        bnd_mode = RadioSetting("settings.init_bank", "TDR Band Default",
-                                RadioSettingValueList(LIST_TDR_DEF,
-                                    LIST_TDR_DEF[ _sets.init_bank]))
-        basic.append(bnd_mode)
-
-        volume = RadioSetting("settings.volume", "Volume",
-                              RadioSettingValueInteger(0, 20, _sets.volume))
-        basic.append(volume)
-
-        val = _vfoa.bpower        # 2bits values 0,1,2= Low, Mid, High
-        rx = RadioSettingValueList(LIST_BPOWER, LIST_BPOWER[val])
-        powera = RadioSetting("upper.vfoa.bpower", "Power (Upper)", rx)
-        basic.append(powera)
-
-        val = _vfob.bpower
-        rx = RadioSettingValueList(LIST_BPOWER, LIST_BPOWER[val])
-        powerb = RadioSetting("lower.vfob.bpower", "Power (Lower)", rx)
-        basic.append(powerb)
-
-        def my_word2raw(setting, obj, atrb, mlt=10):
-            """Callback function to convert UI floating value to u16 int"""
-            if str(setting.value) == "Off":
-               frq = 0x0FFFF
-            else:
-                frq = int(float(str(setting.value)) * float(mlt))
-            if frq == 0:
-                frq = 0xFFFF
-            setattr(obj, atrb, frq)
-            return
-
-        def my_adjraw(setting, obj, atrb, fix):
-            """Callback: add or subtract fix from value."""
-            vx = int(str(setting.value))
-            value = vx  + int(fix)
-            if value < 0:
-                value = 0
-            if atrb == "frq_chn_mode" and int(str(setting.value)) == 2:
-                value = vx * 2         # Special handling for frq_chn_mode
-            setattr(obj, atrb, value)
-            return
-
-        def my_dbl2raw(setting, obj, atrb, flg=1):
-            """Callback: convert from freq 146.7600 to 14760000 U32."""
-            value = chirp_common.parse_freq(str(setting.value)) / 10
-            # flg=1 means 0 becomes ff, else leave as possible 0
-            if flg == 1 and value == 0:
-                value = 0xFFFFFFFF
-            setattr(obj, atrb, value)
-            return
-
-        def my_val_list(setting, obj, atrb):
-            """Callback:from ValueList with non-sequential, actual values."""
-            value = int(str(setting.value))            # Get the integer value
-            if atrb == "tot":
-                value = int(value / 30)    # 30 second increments
-            setattr(obj, atrb, value)
-            return
-
-        def my_spcl(setting, obj, atrb):
-            """Callback: Special handling based on atrb."""
-            if atrb == "frq_chn_mode":
-                idx = LIST_VFOMODE.index (str(setting.value))  # Returns 0 or 1
-                value = idx * 2            # Set bit 1
-            setattr(obj, atrb, value)
-            return
-
-        def my_tone_strn(obj, is_atr, pol_atr, tone_atr):
-            """Generate the CTCS/DCS tone code string."""
-            vx = int(getattr(obj, tone_atr))
-            if vx == 16383 or vx == 0:
-                return "Off"                 # 16383 is all bits set
-            if getattr(obj, is_atr) == 0:             # Simple CTCSS code
-                tstr = str(vx / 10.0)
-            else:        # DCS
-                if getattr(obj, pol_atr) == 0:
-                    tstr = "D{:03x}R".format(vx)
-                else:
-                    tstr = "D{:03x}N".format(vx)
-            return tstr
-
-        def my_set_tone(setting, obj, is_atr, pol_atr, tone_atr):
-            """Callback- create the tone setting from string code."""
-            sx = str(setting.value)        # '131.8'  or 'D231N' or 'Off'
-            if sx == "Off":
-                isx = 1
-                polx = 1
-                tonx = 0x3FFF
-            elif sx[0] == "D":         # DCS
-                isx = 1
-                if sx[4] == "N":
-                    polx = 1
-                else:
-                    polx = 0
-                tonx = int(sx[1:4], 16)
-            else:                                     # CTCSS
-                isx = 0
-                polx = 0
-                tonx = int(float(sx) * 10.0)
-            setattr(obj, is_atr, isx)
-            setattr(obj, pol_atr, polx)
-            setattr(obj, tone_atr, tonx)
-            return
-
-        val = _sets.fm_freq / 10.0
-        if val == 0:
-            val = 88.9            # 0 is not valid
-        rx = RadioSettingValueFloat(65, 108.0, val, 0.1, 1)
-        rs = RadioSetting("settings.fm_freq", "FM Broadcast Freq (MHz)", rx)
-        rs.set_apply_callback(my_word2raw, _sets, "fm_freq")
-        basic.append(rs)
-
-        wtled = RadioSetting("settings.wtled", "Standby LED Color",
-                             RadioSettingValueList(LIST_COLOR, LIST_COLOR[
-                                 _sets.wtled]))
-        basic.append(wtled)
-
-        rxled = RadioSetting("settings.rxled", "RX LED Color",
-                             RadioSettingValueList(LIST_COLOR, LIST_COLOR[
-                                 _sets.rxled]))
-        basic.append(rxled)
-
-        txled = RadioSetting("settings.txled", "TX LED Color",
-                             RadioSettingValueList(LIST_COLOR, LIST_COLOR[
-                                 _sets.txled]))
-        basic.append(txled)
-
-        ledsw = RadioSetting("settings.ledsw", "Back light mode",
-                             RadioSettingValueList(LIST_LEDSW, LIST_LEDSW[
-                                 _sets.ledsw]))
-        basic.append(ledsw)
-
-        beep = RadioSetting("settings.beep", "Beep",
-                            RadioSettingValueBoolean(bool(_sets.beep)))
-        basic.append(beep)
-
-        ring = RadioSetting("settings.ring", "Ring",
-                            RadioSettingValueList(LIST_RING, LIST_RING[
-                                _sets.ring]))
-        basic.append(ring)
-
-        bcl = RadioSetting("settings.bcl", "Busy channel lockout",
-                           RadioSettingValueBoolean(bool(_sets.bcl)))
-        basic.append(bcl)
-
-        if _vfoa.sql == 0xFF:
-            val = 0x04
-        else:
-            val = _vfoa.sql
-        sqla = RadioSetting("upper.vfoa.sql", "Squelch (Upper)",
-                            RadioSettingValueInteger(0, 9, val))
-        basic.append(sqla)
-
-        if _vfob.sql == 0xFF:
-            val = 0x04
-        else:
-            val = _vfob.sql
-        sqlb = RadioSetting("lower.vfob.sql", "Squelch (Lower)",
-                            RadioSettingValueInteger(0, 9, val))
-        basic.append(sqlb)
-
-        tmp = str(int(_sets.tot) * 30)     # 30 sec step counter
-        rs = RadioSetting("settings.tot", "Transmit Timeout (Secs)",
-                           RadioSettingValueList(LIST_TIMEOUT, tmp))
-        rs.set_apply_callback(my_val_list, _sets, "tot")
-        basic.append(rs)
-
-        tmp = str(int(_sets.sig_freq))
-        rs = RadioSetting("settings.sig_freq", "Single Signaling Tone (Htz)",
-                          RadioSettingValueList(LIST_SSF, tmp))
-        rs.set_apply_callback(my_val_list, _sets, "sig_freq")
-        basic.append(rs)
-
-        tmp = str(int(_sets.dtmf_txms))
-        rs = RadioSetting("settings.dtmf_txms", "DTMF Tx Duration (mSecs)",
-                          RadioSettingValueList(LIST_DTMFTX, tmp))
-        rs.set_apply_callback(my_val_list, _sets, "dtmf_txms")
-        basic.append(rs)
-
-        rs = RadioSetting("settings.rptr_mode", "Repeater Mode",
-                          RadioSettingValueBoolean(bool(_sets.rptr_mode)))
-        basic.append(rs)
-
-        # UPPER BAND SETTINGS
-
-        # Freq Mode, convert bit 1 state to index pointer
-        val = _vfoa.frq_chn_mode / 2
-
-        rx = RadioSettingValueList(LIST_VFOMODE, LIST_VFOMODE[val])
-        rs = RadioSetting("upper.vfoa.frq_chn_mode", "Default Mode", rx)
-        rs.set_apply_callback(my_spcl, _vfoa, "frq_chn_mode")
-        a_band.append(rs)
-
-        val =_vfoa.chan_num + 1                  # Add 1 for 1-128 displayed
-        rs = RadioSetting("upper.vfoa.chan_num", "Initial Chan",
-                          RadioSettingValueInteger(1, 128, val))
-        rs.set_apply_callback(my_adjraw, _vfoa, "chan_num", -1)
-        a_band.append(rs)
-
-        val = _vfoa.rxfreq / 100000.0
-        if (val < 136.0 or val > 176.0):
-            val = 146.520            # 2m calling
-        rs = RadioSetting("upper.vfoa.rxfreq ", "Default Recv Freq (MHz)",
-                          RadioSettingValueFloat(136.0, 176.0, val, 0.001, 5))
-        rs.set_apply_callback(my_dbl2raw, _vfoa, "rxfreq")
-        a_band.append(rs)
-
-        tmp = my_tone_strn(_vfoa, "is_rxdigtone", "rxdtcs_pol", "rx_tone")
-        rs = RadioSetting("rx_tone", "Default Recv CTCSS (Htz)",
-                          RadioSettingValueList(LIST_CTCSS, tmp))
-        rs.set_apply_callback(my_set_tone, _vfoa, "is_rxdigtone",
-                              "rxdtcs_pol", "rx_tone")
-        a_band.append(rs)
-
-        rx = RadioSettingValueList(LIST_RECVMODE,
-                                   LIST_RECVMODE[_vfoa.rx_mode])
-        rs = RadioSetting("upper.vfoa.rx_mode", "Default Recv Mode", rx)
-        a_band.append(rs)
-
-        tmp = my_tone_strn(_vfoa, "is_txdigtone", "txdtcs_pol", "tx_tone")
-        rs = RadioSetting("tx_tone", "Default Xmit CTCSS (Htz)",
-                          RadioSettingValueList(LIST_CTCSS, tmp))
-        rs.set_apply_callback(my_set_tone, _vfoa, "is_txdigtone",
-                              "txdtcs_pol", "tx_tone")
-        a_band.append(rs)
-
-        rs = RadioSetting("upper.vfoa.launch_sig", "Launch Signaling",
-                          RadioSettingValueList(LIST_SIGNAL,
-                              LIST_SIGNAL[_vfoa.launch_sig]))
-        a_band.append(rs)
-
-        rx = RadioSettingValueList(LIST_SIGNAL,LIST_SIGNAL[_vfoa.tx_end_sig])
-        rs = RadioSetting("upper.vfoa.tx_end_sig", "Xmit End Signaling", rx)
-        a_band.append(rs)
-
-        rx = RadioSettingValueList(LIST_BW, LIST_BW[_vfoa.fm_bw])
-        rs = RadioSetting("upper.vfoa.fm_bw", "Wide/Narrow Band", rx)
-        a_band.append(rs)
-
-        rx = RadioSettingValueBoolean(bool(_vfoa.cmp_nder))
-        rs = RadioSetting("upper.vfoa.cmp_nder", "Compandor", rx)
-        a_band.append(rs)
-
-        rs = RadioSetting("upper.vfoa.scrm_blr", "Scrambler",
-                          RadioSettingValueBoolean(bool(_vfoa.scrm_blr)))
-        a_band.append(rs)
-
-        rx = RadioSettingValueList(LIST_SHIFT, LIST_SHIFT[_vfoa.shift])
-        rs = RadioSetting("upper.vfoa.shift", "Xmit Shift", rx)
-        a_band.append(rs)
-
-        val = _vfoa.offset / 100000.0
-        rs = RadioSetting("upper.vfoa.offset", "Xmit Offset (MHz)",
-                          RadioSettingValueFloat(0, 100.0, val, 0.001, 3))
-        # Allow zero value
-        rs.set_apply_callback(my_dbl2raw, _vfoa, "offset", 0)
-        a_band.append(rs)
-
-        tmp = str(_vfoa.step / 100.0)
-        rs = RadioSetting("step", "Freq step (KHz)",
-                          RadioSettingValueList(LIST_STEPS, tmp))
-        rs.set_apply_callback(my_word2raw, _vfoa,"step", 100)
-        a_band.append(rs)
-
-        # LOWER BAND SETTINGS
-
-        val = _vfob.frq_chn_mode / 2
-        rx = RadioSettingValueList(LIST_VFOMODE, LIST_VFOMODE[val])
-        rs = RadioSetting("lower.vfob.frq_chn_mode", "Default Mode", rx)
-        rs.set_apply_callback(my_spcl, _vfob, "frq_chn_mode")
-        b_band.append(rs)
-
-        val = _vfob.chan_num + 1
-        rs = RadioSetting("lower.vfob.chan_num", "Initial Chan",
-                          RadioSettingValueInteger(0, 127, val))
-        rs.set_apply_callback(my_adjraw, _vfob, "chan_num", -1)
-        b_band.append(rs)
-
-        val = _vfob.rxfreq / 100000.0
-        if (val < 400.0 or val > 480.0):
-            val = 446.0          # UHF calling
-        rs = RadioSetting("lower.vfob.rxfreq ", "Default Recv Freq (MHz)",
-                          RadioSettingValueFloat(400.0, 480.0, val, 0.001, 5))
-        rs.set_apply_callback(my_dbl2raw, _vfob, "rxfreq")
-        b_band.append(rs)
-
-        tmp = my_tone_strn(_vfob, "is_rxdigtone", "rxdtcs_pol", "rx_tone")
-        rs = RadioSetting("rx_tone", "Default Recv CTCSS (Htz)",
-                          RadioSettingValueList(LIST_CTCSS, tmp))
-        rs.set_apply_callback(my_set_tone, _vfob, "is_rxdigtone",
-                              "rxdtcs_pol", "rx_tone")
-        b_band.append(rs)
-
-        rx = RadioSettingValueList(LIST_RECVMODE, LIST_RECVMODE[_vfob.rx_mode])
-        rs = RadioSetting("lower.vfob.rx_mode", "Default Recv Mode", rx)
-        b_band.append(rs)
-
-        tmp = my_tone_strn(_vfob, "is_txdigtone", "txdtcs_pol", "tx_tone")
-        rs = RadioSetting("tx_tone", "Default Xmit CTCSS (Htz)",
-                          RadioSettingValueList(LIST_CTCSS, tmp))
-        rs.set_apply_callback(my_set_tone, _vfob, "is_txdigtone",
-                              "txdtcs_pol", "tx_tone")
-        b_band.append(rs)
-
-        rx = RadioSettingValueList(LIST_SIGNAL,LIST_SIGNAL[_vfob.launch_sig])
-        rs = RadioSetting("lower.vfob.launch_sig", "Launch Signaling", rx)
-        b_band.append(rs)
-
-        rx = RadioSettingValueList(LIST_SIGNAL,LIST_SIGNAL[_vfob.tx_end_sig])
-        rs = RadioSetting("lower.vfob.tx_end_sig", "Xmit End Signaling", rx)
-        b_band.append(rs)
-
-        rx = RadioSettingValueList(LIST_BW, LIST_BW[_vfob.fm_bw])
-        rs = RadioSetting("lower.vfob.fm_bw", "Wide/Narrow Band", rx)
-        b_band.append(rs)
-
-        rs = RadioSetting("lower.vfob.cmp_nder", "Compandor",
-                          RadioSettingValueBoolean(bool(_vfob.cmp_nder)))
-        b_band.append(rs)
-
-        rs = RadioSetting("lower.vfob.scrm_blr", "Scrambler",
-                          RadioSettingValueBoolean(bool(_vfob.scrm_blr)))
-        b_band.append(rs)
-
-        rx = RadioSettingValueList(LIST_SHIFT, LIST_SHIFT[_vfob.shift])
-        rs = RadioSetting("lower.vfob.shift", "Xmit Shift", rx)
-        b_band.append(rs)
-
-        val = _vfob.offset / 100000.0
-        rs = RadioSetting("lower.vfob.offset", "Xmit Offset (MHz)",
-                          RadioSettingValueFloat(0, 100.0, val, 0.001, 3))
-        rs.set_apply_callback(my_dbl2raw, _vfob, "offset", 0)
-        b_band.append(rs)
-
-        tmp = str(_vfob.step / 100.0)
-        rs = RadioSetting("step", "Freq step (KHz)",
-                          RadioSettingValueList(LIST_STEPS, tmp))
-        rs.set_apply_callback(my_word2raw, _vfob, "step", 100)
-        b_band.append(rs)
-
-        # PowerOn & Freq Limits Settings
-
-        def chars2str(cary, knt):
-            """Convert raw memory char array to a string: NOT a callback."""
-            stx = ""
-            for char in cary[:knt]:
-                stx += chr(char)
-            return stx
-
-        def my_str2ary(setting, obj, atrba, atrbc):
-            """Callback: convert 7-char string to char array with count."""
-            ary = ""
-            knt = 7
-            for j in range (6, -1, -1):       # Strip trailing spaces
-                if str(setting.value)[j] == "" or str(setting.value)[j] == " ":
-                    knt = knt - 1
-                else:
-                    break
-            for j in range(0, 7, 1):
-                if j < knt: ary += str(setting.value)[j]
-                else: ary += chr(0xFF)
-            setattr(obj, atrba, ary)
-            setattr(obj, atrbc, knt)
-            return
-
-        tmp = chars2str(_lims.hello1, _lims.hello1_cnt)
-        rs = RadioSetting("hello_lims.hello1", "Power-On Message 1",
-                          RadioSettingValueString(0, 7, tmp))
-        rs.set_apply_callback(my_str2ary, _lims, "hello1", "hello1_cnt")
-        lims.append(rs)
-
-        tmp = chars2str(_lims.hello2, _lims.hello2_cnt)
-        rs = RadioSetting("hello_lims.hello2", "Power-On Message 2",
-                          RadioSettingValueString(0, 7, tmp))
-        rs.set_apply_callback(my_str2ary, _lims,"hello2", "hello2_cnt")
-        lims.append(rs)
-
-        # VALID_BANDS = [(136000000, 176000000),400000000, 480000000)]
-
-        lval = _lims.vhf_low / 100000.0
-        uval = _lims.vhf_high / 100000.0
-        if lval >= uval:
-            lval = 144.0
-            uval = 158.0
-
-        rs = RadioSetting("hello_lims.vhf_low", "Lower VHF Band Limit (MHz)",
-                          RadioSettingValueFloat(136.0, 176.0, lval, 0.001, 3))
-        rs.set_apply_callback(my_dbl2raw, _lims, "vhf_low")
-        lims.append(rs)
-
-        rs = RadioSetting("hello_lims.vhf_high", "Upper VHF Band Limit (MHz)",
-                          RadioSettingValueFloat(136.0, 176.0, uval, 0.001, 3))
-        rs.set_apply_callback(my_dbl2raw, _lims, "vhf_high")
-        lims.append(rs)
-
-        lval = _lims.uhf_low / 100000.0
-        uval = _lims.uhf_high / 100000.0
-        if lval >= uval:
-            lval = 420.0
-            uval = 470.0
-
-        rs = RadioSetting("hello_lims.uhf_low", "Lower UHF Band Limit (MHz)",
-                          RadioSettingValueFloat(400.0, 480.0, lval, 0.001, 3))
-        rs.set_apply_callback(my_dbl2raw, _lims, "uhf_low")
-        lims.append(rs)
-
-        rs = RadioSetting("hello_lims.uhf_high", "Upper UHF Band Limit (MHz)",
-                          RadioSettingValueFloat(400.0, 480.0, uval, 0.001, 3))
-        rs.set_apply_callback(my_dbl2raw, _lims, "uhf_high")
-        lims.append(rs)
-
-        # Codes and DTMF Groups Settings
-
-        def make_dtmf(ary, knt):
-            """Generate the DTMF code 1-8, NOT a callback."""
-            tmp = ""
-            if knt > 0 and knt != 0xff:
-                for  val in ary[:knt]:
-                    if val > 0 and val <= 9:
-                        tmp += chr(val + 48)
-                    elif val == 0x0a:
-                        tmp += "0"
-                    elif val == 0x0d:
-                        tmp += "A"
-                    elif val == 0x0e:
-                        tmp += "B"
-                    elif val == 0x0f:
-                        tmp += "C"
-                    elif val == 0x00:
-                        tmp += "D"
-                    elif val == 0x0b:
-                        tmp += "*"
-                    elif val == 0x0c:
-                        tmp += "#"
-                    else:
-                        msg = ("Invalid Character. Must be: 0-9,A,B,C,D,*,#")
-                        raise InvalidValueError(msg)
-            return tmp
-
-        def my_dtmf2raw(setting, obj, atrba, atrbc, syz=7):
-            """Callback: DTMF Code; sends 5 or 7-byte string."""
-            draw = []
-            knt = syz
-            for j in range (syz - 1, -1, -1):       # Strip trailing spaces
-                if str(setting.value)[j] == "" or str(setting.value)[j] == " ":
-                    knt = knt - 1
-                else:
-                    break
-            for j in range(0, syz):
-                bx = str(setting.value)[j]
-                obx = ord(bx)
-                dig = 0x0ff
-                if j < knt and knt > 0:      # (Else) is pads
-                    if  bx == "0":
-                        dig = 0x0a
-                    elif  bx == "A":
-                        dig = 0x0d
-                    elif  bx == "B":
-                        dig = 0x0e
-                    elif  bx == "C":
-                        dig = 0x0f
-                    elif  bx == "D":
-                        dig = 0x00
-                    elif  bx == "*":
-                        dig = 0x0b
-                    elif  bx == "#":
-                        dig = 0x0c
-                    elif obx >= 49 and obx <= 57:
-                        dig = obx - 48
-                    else:
-                        msg = ("Must be: 0-9,A,B,C,D,*,#")
-                        raise InvalidValueError(msg)
-                    # - End if/elif/else for bx
-                # - End if J<=knt
-                draw.append(dig)         # Generate string of bytes
-            # - End for j
-            setattr(obj, atrba, draw)
-            setattr(obj, atrbc, knt)
-            return
-
-        tmp = make_dtmf(_codes.native_id_code, _codes.native_id_cnt)
-        rs = RadioSetting("codes.native_id_code", "Native ID Code",
-                          RadioSettingValueString(0, 7, tmp))
-        rs.set_apply_callback(my_dtmf2raw, _codes, "native_id_code",
-                              "native_id_cnt", 7)
-        codes.append(rs)
-
-        tmp = make_dtmf(_codes.master_id_code, _codes.master_id_cnt)
-        rs = RadioSetting("codes.master_id_code", "Master Control ID Code",
-                          RadioSettingValueString(0, 7, tmp))
-        rs.set_apply_callback(my_dtmf2raw, _codes, "master_id_code",
-                              "master_id_cnt",7)
-        codes.append(rs)
-
-        tmp = make_dtmf(_codes.alarm_code, _codes.alarm_cnt)
-        rs = RadioSetting("codes.alarm_code", "Alarm Code",
-                            RadioSettingValueString(0, 5, tmp))
-        rs.set_apply_callback(my_dtmf2raw, _codes, "alarm_code",
-                            "alarm_cnt", 5)
-        codes.append(rs)
-
-        tmp = make_dtmf(_codes.id_disp_code, _codes.id_disp_cnt)
-        rs = RadioSetting("codes.id_disp_code", "Identify Display Code",
-                          RadioSettingValueString(0, 5, tmp))
-        rs.set_apply_callback(my_dtmf2raw, _codes, "id_disp_code",
-                              "id_disp_cnt", 5)
-        codes.append(rs)
-
-        tmp = make_dtmf(_codes.revive_code, _codes.revive_cnt)
-        rs = RadioSetting("codes.revive_code", "Revive Code",
-                          RadioSettingValueString(0, 5, tmp))
-        rs.set_apply_callback(my_dtmf2raw, _codes,"revive_code",
-                              "revive_cnt", 5)
-        codes.append(rs)
-
-        tmp = make_dtmf(_codes.stun_code, _codes.stun_cnt)
-        rs = RadioSetting("codes.stun_code", "Remote Stun Code",
-                          RadioSettingValueString(0, 5, tmp))
-        rs.set_apply_callback(my_dtmf2raw,  _codes, "stun_code",
-                              "stun_cnt", 5)
-        codes.append(rs)
-
-        tmp = make_dtmf(_codes.kill_code, _codes.kill_cnt)
-        rs = RadioSetting("codes.kill_code", "Remote KILL Code",
-                          RadioSettingValueString(0, 5, tmp))
-        rs.set_apply_callback(my_dtmf2raw, _codes, "kill_code",
-                              "kill_cnt", 5)
-        codes.append(rs)
-
-        tmp = make_dtmf(_codes.monitor_code, _codes.monitor_cnt)
-        rs = RadioSetting("codes.monitor_code", "Monitor Code",
-                          RadioSettingValueString(0, 5, tmp))
-        rs.set_apply_callback(my_dtmf2raw, _codes, "monitor_code",
-                              "monitor_cnt", 5)
-        codes.append(rs)
-
-        val = _codes.state_now
-        if val > 2:
-            val = 0
-
-        rx = RadioSettingValueList(LIST_STATE, LIST_STATE[val])
-        rs = RadioSetting("codes.state_now", "Current State", rx)
-        codes.append(rs)
-
-        dtm = make_dtmf(_dtmf.dtmf1, _dtmf.dtmf1_cnt)
-        rs = RadioSetting("dtmf_tab.dtmf1", "DTMF1 String",
-                          RadioSettingValueString(0, 7, dtm))
-        rs.set_apply_callback(my_dtmf2raw, _dtmf, "dtmf1", "dtmf1_cnt")
-        codes.append(rs)
-
-        dtm = make_dtmf(_dtmf.dtmf2, _dtmf.dtmf2_cnt)
-        rs = RadioSetting("dtmf_tab.dtmf2", "DTMF2 String",
-                          RadioSettingValueString(0, 7, dtm))
-        rs.set_apply_callback(my_dtmf2raw, _dtmf, "dtmf2", "dtmf2_cnt")
-        codes.append(rs)
-
-        dtm = make_dtmf(_dtmf.dtmf3, _dtmf.dtmf3_cnt)
-        rs = RadioSetting("dtmf_tab.dtmf3", "DTMF3 String",
-                          RadioSettingValueString(0, 7, dtm))
-        rs.set_apply_callback(my_dtmf2raw, _dtmf, "dtmf3", "dtmf3_cnt")
-        codes.append(rs)
-
-        dtm = make_dtmf(_dtmf.dtmf4, _dtmf.dtmf4_cnt)
-        rs = RadioSetting("dtmf_tab.dtmf4", "DTMF4 String",
-                          RadioSettingValueString(0, 7, dtm))
-        rs.set_apply_callback(my_dtmf2raw, _dtmf, "dtmf4", "dtmf4_cnt")
-        codes.append(rs)
-
-        dtm = make_dtmf(_dtmf.dtmf5, _dtmf.dtmf5_cnt)
-        rs = RadioSetting("dtmf_tab.dtmf5", "DTMF5 String",
-                          RadioSettingValueString(0, 7, dtm))
-        rs.set_apply_callback(my_dtmf2raw, _dtmf, "dtmf5", "dtmf5_cnt")
-        codes.append(rs)
-
-        dtm = make_dtmf(_dtmf.dtmf6, _dtmf.dtmf6_cnt)
-        rs = RadioSetting("dtmf_tab.dtmf6", "DTMF6 String",
-                          RadioSettingValueString(0, 7, dtm))
-        rs.set_apply_callback(my_dtmf2raw, _dtmf, "dtmf6", "dtmf6_cnt")
-        codes.append(rs)
-
-        dtm = make_dtmf(_dtmf.dtmf7, _dtmf.dtmf7_cnt)
-        rs = RadioSetting("dtmf_tab.dtmf7", "DTMF7 String",
-                          RadioSettingValueString(0, 7, dtm))
-        rs.set_apply_callback(my_dtmf2raw, _dtmf, "dtmf7", "dtmf7_cnt")
-        codes.append(rs)
-
-        dtm = make_dtmf(_dtmf.dtmf8, _dtmf.dtmf8_cnt)
-        rs = RadioSetting("dtmf_tab.dtmf8", "DTMF8 String",
-                          RadioSettingValueString(0, 7, dtm))
-        rs.set_apply_callback(my_dtmf2raw, _dtmf, "dtmf8", "dtmf8_cnt")
-        codes.append(rs)
-
-        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
-
-
-    @classmethod
-    def match_model(cls, filedata, filename):
-        match_size = False
-        match_model = False
-
-        # Testing the file data size
-        if len(filedata) == MEM_SIZE + 8:
-            match_size = True
-
-        # Testing the firmware model fingerprint
-        match_model = model_match(cls, filedata)
-
-        if match_size and match_model:
-            return True
-        else:
-            return False
-
-
-class LT725UVUpper(LT725UV):
-    VARIANT = "Upper"
-    _vfo = "upper"
-
-
-class LT725UVLower(LT725UV):
-    VARIANT = "Lower"
-    _vfo = "lower"
-
-
-class Zastone(chirp_common.Alias):
-    """Declare BJ-218 alias for Zastone BJ-218."""
-    VENDOR = "Zastone"
-    MODEL = "BJ-218"
-
-
-class Hesenate(chirp_common.Alias):
-    """Declare BJ-218 alias for Hesenate BJ-218."""
-    VENDOR = "Hesenate"
-    MODEL = "BJ-218"
-
-
- at directory.register
-class Baojie218(LT725UV):
-    """Baojie BJ-218"""
-    VENDOR = "Baojie"
-    MODEL = "BJ-218"
-    ALIASES = [Zastone, Hesenate, ]
+# Copyright 2016:
+# * Jim Unroe KC9HI, <rock.unroe at gmail.com>
+# Modified for Baojie BJ-218: 2018 by Rick DeWitt (RJD), <aa0rd at yahoo.com>#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 2 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program.  If not, see <http://www.gnu.org/licenses/>.
+
+import time
+import struct
+import logging
+import re
+
+LOG = logging.getLogger(__name__)
+
+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
+
+MEM_FORMAT = """
+#seekto 0x0200;
+struct {
+  u8  init_bank;
+  u8  volume;
+  u16 fm_freq;
+  u8  wtled;
+  u8  rxled;
+  u8  txled;
+  u8  ledsw;
+  u8  beep;
+  u8  ring;
+  u8  bcl;
+  u8  tot;
+  u16 sig_freq;
+  u16 dtmf_txms;
+  u8  init_sql;
+  u8  rptr_mode;
+} settings;
+
+#seekto 0x0240;
+struct {
+  u8  dtmf1_cnt;
+  u8  dtmf1[7];
+  u8  dtmf2_cnt;
+  u8  dtmf2[7];
+  u8  dtmf3_cnt;
+  u8  dtmf3[7];
+  u8  dtmf4_cnt;
+  u8  dtmf4[7];
+  u8  dtmf5_cnt;
+  u8  dtmf5[7];
+  u8  dtmf6_cnt;
+  u8  dtmf6[7];
+  u8  dtmf7_cnt;
+  u8  dtmf7[7];
+  u8  dtmf8_cnt;
+  u8  dtmf8[7];
+} dtmf_tab;
+
+#seekto 0x0280;
+struct {
+  u8  native_id_cnt;
+  u8  native_id_code[7];
+  u8  master_id_cnt;
+  u8  master_id_code[7];
+  u8  alarm_cnt;
+  u8  alarm_code[5];
+  u8  id_disp_cnt;
+  u8  id_disp_code[5];
+  u8  revive_cnt;
+  u8  revive_code[5];
+  u8  stun_cnt;
+  u8  stun_code[5];
+  u8  kill_cnt;
+  u8  kill_code[5];
+  u8  monitor_cnt;
+  u8  monitor_code[5];
+  u8  state_now;
+} codes;
+
+#seekto 0x02d0;
+struct {
+  u8  hello1_cnt;
+  char  hello1[7];
+  u8  hello2_cnt;
+  char  hello2[7];
+  u32  vhf_low;
+  u32  vhf_high;
+  u32  uhf_low;
+  u32  uhf_high;
+  u8  lims_on;
+} hello_lims;
+
+struct vfo {
+  u8  frq_chn_mode;
+  u8  chan_num;
+  u32 rxfreq;
+  u16 is_rxdigtone:1,
+      rxdtcs_pol:1,
+      rx_tone:14;
+  u8  rx_mode;
+  u8  unknown_ff;
+  u16 is_txdigtone:1,
+      txdtcs_pol:1,
+      tx_tone:14;
+  u8  launch_sig;
+  u8  tx_end_sig;
+  u8  bpower;
+  u8  fm_bw;
+  u8  cmp_nder;
+  u8  scrm_blr;
+  u8  shift;
+  u32 offset;
+  u16 step;
+  u8  sql;
+};
+
+#seekto 0x0300;
+struct {
+  struct vfo vfoa;
+} upper;
+
+#seekto 0x0380;
+struct {
+  struct vfo vfob;
+} lower;
+
+struct mem {
+  u32 rxfreq;
+  u16 is_rxdigtone:1,
+      rxdtcs_pol:1,
+      rxtone:14;
+  u8  recvmode;
+  u32 txfreq;
+  u16 is_txdigtone:1,
+      txdtcs_pol:1,
+      txtone:14;
+  u8  botsignal;
+  u8  eotsignal;
+  u8  power:1,
+      wide:1,
+      compandor:1
+      scrambler:1
+      unknown:4;
+  u8  namelen;
+  u8  name[7];
+};
+
+#seekto 0x0400;
+struct mem upper_memory[128];
+
+#seekto 0x1000;
+struct mem lower_memory[128];
+
+#seekto 0x1C00;
+struct {
+  char  mod_num[6];
+} mod_id;
+"""
+
+MEM_SIZE = 0x1C00
+BLOCK_SIZE = 0x40
+STIMEOUT = 2
+# Channel power: 2 levels
+POWER_LEVELS = [chirp_common.PowerLevel("Low", watts=5.00),
+                chirp_common.PowerLevel("High", watts=30.00)]
+
+LIST_RECVMODE = ["QT/DQT", "QT/DQT + Signaling"]
+LIST_SIGNAL = ["Off"] + ["DTMF%s" % x for x in range(1, 9)] + \
+              ["DTMF%s + Identity" % x for x in range(1, 9)] + \
+              ["Identity code"]
+# Band Power settings, can be different than channel power
+LIST_BPOWER = ["Low", "Mid", "High"]    # Tri-power models
+LIST_COLOR = ["Off", "Orange", "Blue", "Purple"]
+LIST_LEDSW = ["Auto", "On"]
+LIST_RING = ["Off"] + ["%s" % x for x in range(1, 10)]
+LIST_TDR_DEF = ["A-Upper", "B-Lower"]
+LIST_TIMEOUT = ["Off"] + ["%s" % x for x in range(30, 630, 30)]
+LIST_VFOMODE = ["Frequency Mode", "Channel Mode"]
+# Tones are numeric, Defined in \chirp\chirp_common.py
+TONES_CTCSS = sorted(chirp_common.TONES)
+# Converted to strings
+LIST_CTCSS = ["Off"] + [str(x) for x in TONES_CTCSS]
+# Now append the DxxxN and DxxxI DTCS codes from chirp_common
+for x in chirp_common.DTCS_CODES:
+    LIST_CTCSS.append("D{:03d}N".format(x))
+for x in chirp_common.DTCS_CODES:
+    LIST_CTCSS.append("D{:03d}R".format(x))
+LIST_BW = ["Narrow", "Wide"]
+LIST_SHIFT = ["Off"," + ", " - "]
+STEPS = [2.5, 5.0, 6.25, 10.0, 12.5, 20.0, 25.0, 50.0]
+LIST_STEPS = [str(x) for x in STEPS]
+LIST_STATE = ["Normal", "Stun", "Kill"]
+LIST_SSF = ["1000", "1450", "1750", "2100"]
+LIST_DTMFTX = ["50", "100", "150", "200", "300","500"]
+
+SETTING_LISTS = {
+"init_bank": LIST_TDR_DEF ,
+"tot": LIST_TIMEOUT,
+"wtled": LIST_COLOR,
+"rxled": LIST_COLOR,
+"txled": LIST_COLOR,
+"sig_freq": LIST_SSF,
+"dtmf_txms": LIST_DTMFTX,
+"ledsw": LIST_LEDSW,
+"frq_chn_mode": LIST_VFOMODE,
+"rx_tone": LIST_CTCSS,
+"tx_tone": LIST_CTCSS,
+"rx_mode": LIST_RECVMODE,
+"launch_sig": LIST_SIGNAL,
+"tx_end_sig": LIST_SIGNAL,
+"bpower":LIST_BPOWER,
+"fm_bw": LIST_BW,
+"shift": LIST_SHIFT,
+"step": LIST_STEPS,
+"ring": LIST_RING,
+"state_now": LIST_STATE
+}
+
+def _clean_buffer(radio):
+    radio.pipe.timeout = 0.005
+    junk = radio.pipe.read(256)
+    radio.pipe.timeout = STIMEOUT
+    if junk:
+        Log.debug("Got %i bytes of junk before starting" % len(junk))
+
+
+def _rawrecv(radio, amount):
+    """Raw read from the radio device"""
+    data = ""
+    try:
+        data = radio.pipe.read(amount)
+    except:
+        _exit_program_mode(radio)
+        msg = "Generic error reading data from radio; check your cable."
+        raise errors.RadioError(msg)
+
+    if len(data) != amount:
+        _exit_program_mode(radio)
+        msg = "Error reading from radio: not the amount of data we want."
+        raise errors.RadioError(msg)
+
+    return data
+
+
+def _rawsend(radio, data):
+    """Raw send to the radio device"""
+    try:
+        radio.pipe.write(data)
+    except:
+        raise errors.RadioError("Error sending data to radio")
+
+
+def _make_frame(cmd, addr, length, data=""):
+    """Pack the info in the headder format"""
+    frame = struct.pack(">4sHH", cmd, addr, length)
+    # Add the data if set
+    if len(data) != 0:
+        frame += data
+    # Return the data
+    return frame
+
+
+def _recv(radio, addr, length):
+    """Get data from the radio """
+
+    data = _rawrecv(radio, length)
+
+    # DEBUG
+    LOG.info("Response:")
+    LOG.debug(util.hexprint(data))
+
+    return data
+
+
+def _do_ident(radio):
+    """Put the radio in PROGRAM mode & identify it"""
+    # Set the serial discipline
+    radio.pipe.baudrate = 19200
+    radio.pipe.parity = "N"
+    radio.pipe.timeout = STIMEOUT
+
+    # Flush input buffer
+    _clean_buffer(radio)
+
+    magic = "PROM_LIN"
+
+    _rawsend(radio, magic)
+
+    ack = _rawrecv(radio, 1)
+    if ack != "\x06":
+        _exit_program_mode(radio)
+        if ack:
+            LOG.debug(repr(ack))
+        raise errors.RadioError("Radio did not respond")
+
+    return True
+
+
+def _exit_program_mode(radio):
+    endframe = "EXIT"
+    _rawsend(radio, endframe)
+
+
+def _download(radio):
+    """Get the memory map"""
+
+    # Put radio in program mode and identify it
+    _do_ident(radio)
+
+    # UI progress
+    status = chirp_common.Status()
+    status.cur = 0
+    status.max = MEM_SIZE / BLOCK_SIZE
+    status.msg = "Cloning from radio..."
+    radio.status_fn(status)
+
+    data = ""
+    for addr in range(0, MEM_SIZE, BLOCK_SIZE):
+        frame = _make_frame("READ", addr, BLOCK_SIZE)
+        # DEBUG
+        LOG.info("Request sent:")
+        LOG.debug(util.hexprint(frame))
+
+        # Sending the read request
+        _rawsend(radio, frame)
+
+        # Now we read
+        d = _recv(radio, addr, BLOCK_SIZE)
+
+        # Aggregate the data
+        data += d
+
+        # UI Update
+        status.cur = addr / BLOCK_SIZE
+        status.msg = "Cloning from radio..."
+        radio.status_fn(status)
+
+    _exit_program_mode(radio)
+
+    data += radio.MODEL.ljust(8)
+
+    return data
+
+
+def _upload(radio):
+    """Upload procedure"""
+
+    # Put radio in program mode and identify it
+    _do_ident(radio)
+
+    # UI progress
+    status = chirp_common.Status()
+    status.cur = 0
+    status.max = MEM_SIZE / BLOCK_SIZE
+    status.msg = "Cloning to radio..."
+    radio.status_fn(status)
+
+    # The fun starts here
+    for addr in range(0, MEM_SIZE, BLOCK_SIZE):
+        # Sending the data
+        data = radio.get_mmap()[addr:addr + BLOCK_SIZE]
+
+        frame = _make_frame("WRIE", addr, BLOCK_SIZE, data)
+
+        _rawsend(radio, frame)
+
+        # Receiving the response
+        ack = _rawrecv(radio, 1)
+        if ack != "\x06":
+            _exit_program_mode(radio)
+            msg = "Bad ack writing block 0x%04x" % addr
+            raise errors.RadioError(msg)
+
+        # UI Update
+        status.cur = addr / BLOCK_SIZE
+        status.msg = "Cloning to radio..."
+        radio.status_fn(status)
+
+    _exit_program_mode(radio)
+
+
+def model_match(cls, data):
+    """Match the opened/downloaded image to the correct version"""
+    if len(data) == 0x1C08:
+        rid = data[0x1C00:0x1C08]
+        return rid.startswith(cls.MODEL)
+    else:
+        return False
+
+
+def _split(rf, f1, f2):
+    """Returns False if the two freqs are in the same band (no split)
+    or True otherwise"""
+
+    # Determine if the two freqs are in the same band
+    for low, high in rf.valid_bands:
+        if f1 >= low and f1 <= high and \
+                f2 >= low and f2 <= high:
+            # If the two freqs are on the same Band this is not a split
+            return False
+
+    # If you get here is because the freq pairs are split
+    return True
+
+
+ at directory.register
+class LT725UV(chirp_common.CloneModeRadio):
+    """LUITON LT-725UV Radio"""
+    VENDOR = "LUITON"
+    MODEL = "LT-725UV"
+    MODES = ["NFM", "FM"]
+    TONES = chirp_common.TONES
+    DTCS_CODES = sorted(chirp_common.DTCS_CODES + [645])
+    NAME_LENGTH = 7
+    DTMF_CHARS = list("0123456789ABCD*#")
+
+    VALID_BANDS = [(136000000, 176000000),
+                   (400000000, 480000000)]
+
+    # Valid chars on the LCD
+    VALID_CHARS = chirp_common.CHARSET_ALPHANUMERIC + \
+        "`{|}!\"#$%&'()*+,-./:;<=>?@[]^_"
+
+    @classmethod
+    def get_prompts(cls):
+        rp = chirp_common.RadioPrompts()
+        rp.info = \
+            ('Some notes about POWER settings:\n'
+             '- The individual channel power settings are ignored'
+             ' by the radio.\n'
+             '  They are allowed to be set (and downloaded) in hopes of'
+             ' a future firmware update.\n'
+             '- Power settings done \'Live\' in the radio apply to the'
+             ' entire upper or lower band.\n'
+             '- Tri-power radio models will set and download the three'
+             ' band-power'
+             ' levels, but they are converted to just Low and High at'
+             ' upload.'
+             ' The Mid setting reverts to Low.'
+             )
+
+        rp.pre_download = _(dedent("""\
+            Follow this instructions to download your info:
+
+            1 - Turn off your radio
+            2 - Connect your interface cable
+            3 - Turn on your radio
+            4 - Do the download of your radio data
+            """))
+        rp.pre_upload = _(dedent("""\
+            Follow this instructions to upload your info:
+
+            1 - Turn off your radio
+            2 - Connect your interface cable
+            3 - Turn on your radio
+            4 - Do the upload of your radio data
+            """))
+        return rp
+
+    def get_features(self):
+        rf = chirp_common.RadioFeatures()
+        rf.has_settings = True
+        rf.has_bank = False
+        rf.has_tuning_step = False
+        rf.can_odd_split = True
+        rf.has_name = True
+        rf.has_offset = True
+        rf.has_mode = True
+        rf.has_dtcs = True
+        rf.has_rx_dtcs = True
+        rf.has_dtcs_polarity = True
+        rf.has_ctone = True
+        rf.has_cross = True
+        rf.has_sub_devices = self.VARIANT == ""
+        rf.valid_modes = self.MODES
+        rf.valid_characters = self.VALID_CHARS
+        rf.valid_duplexes = ["", "-", "+", "split", "off"]
+        rf.valid_tmodes = ['', 'Tone', 'TSQL', 'DTCS', 'Cross']
+        rf.valid_cross_modes = [
+            "Tone->Tone",
+            "DTCS->",
+            "->DTCS",
+            "Tone->DTCS",
+            "DTCS->Tone",
+            "->Tone",
+            "DTCS->DTCS"]
+        rf.valid_skips = []
+        rf.valid_power_levels = POWER_LEVELS
+        rf.valid_name_length = self.NAME_LENGTH
+        rf.valid_dtcs_codes = self.DTCS_CODES
+        rf.valid_bands = self.VALID_BANDS
+        rf.memory_bounds = (1, 128)
+        return rf
+
+    def get_sub_devices(self):
+        return [LT725UVUpper(self._mmap), LT725UVLower(self._mmap)]
+
+    def sync_in(self):
+        """Download from radio"""
+        try:
+            data = _download(self)
+        except errors.RadioError:
+            # Pass through any real errors we raise
+            raise
+        except:
+            # 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()
+
+    def sync_out(self):
+        """Upload to radio"""
+        try:
+            _upload(self)
+        except:
+            # 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')
+
+    def process_mmap(self):
+        """Process the mem map into the mem object"""
+        self._memobj = bitwise.parse(MEM_FORMAT, self._mmap)
+
+    def get_raw_memory(self, number):
+        return repr(self._memobj.memory[number - 1])
+
+    def _memory_obj(self, suffix=""):
+        return getattr(self._memobj, "%s_memory%s" % (self._vfo, suffix))
+
+    def _get_dcs(self, val):
+        return int(str(val)[2:-18])
+
+    def _set_dcs(self, val):
+        return int(str(val), 16)
+
+    def get_memory(self, number):
+        _mem = self._memory_obj()[number - 1]
+
+        mem = chirp_common.Memory()
+        mem.number = number
+
+        if _mem.get_raw()[0] == "\xff":
+            mem.empty = True
+            return mem
+
+        mem.freq = int(_mem.rxfreq) * 10
+
+        if _mem.txfreq == 0xFFFFFFFF:
+            # TX freq not set
+            mem.duplex = "off"
+            mem.offset = 0
+        elif int(_mem.rxfreq) == int(_mem.txfreq):
+            mem.duplex = ""
+            mem.offset = 0
+        elif _split(self.get_features(), mem.freq, int(_mem.txfreq) * 10):
+            mem.duplex = "split"
+            mem.offset = int(_mem.txfreq) * 10
+        else:
+            mem.duplex = int(_mem.rxfreq) > int(_mem.txfreq) and "-" or "+"
+            mem.offset = abs(int(_mem.rxfreq) - int(_mem.txfreq)) * 10
+
+        for char in _mem.name[:_mem.namelen]:
+            mem.name += chr(char)
+
+        dtcs_pol = ["N", "N"]
+
+        if _mem.rxtone == 0x3FFF:
+            rxmode = ""
+        elif _mem.is_rxdigtone == 0:
+            # CTCSS
+            rxmode = "Tone"
+            mem.ctone = int(_mem.rxtone) / 10.0
+        else:
+            # Digital
+            rxmode = "DTCS"
+            mem.rx_dtcs = self._get_dcs(_mem.rxtone)
+            if _mem.rxdtcs_pol == 1:
+                dtcs_pol[1] = "R"
+
+        if _mem.txtone == 0x3FFF:
+            txmode = ""
+        elif _mem.is_txdigtone == 0:
+            # CTCSS
+            txmode = "Tone"
+            mem.rtone = int(_mem.txtone) / 10.0
+        else:
+            # Digital
+            txmode = "DTCS"
+            mem.dtcs = self._get_dcs(_mem.txtone)
+            if _mem.txdtcs_pol == 1:
+                dtcs_pol[0] = "R"
+
+        if txmode == "Tone" and not rxmode:
+            mem.tmode = "Tone"
+        elif txmode == rxmode and txmode == "Tone" and mem.rtone == mem.ctone:
+            mem.tmode = "TSQL"
+        elif txmode == rxmode and txmode == "DTCS" and mem.dtcs == mem.rx_dtcs:
+            mem.tmode = "DTCS"
+        elif rxmode or txmode:
+            mem.tmode = "Cross"
+            mem.cross_mode = "%s->%s" % (txmode, rxmode)
+
+        mem.dtcs_polarity = "".join(dtcs_pol)
+
+        mem.mode = _mem.wide and "FM" or "NFM"
+
+        mem.power = POWER_LEVELS[_mem.power]
+
+        # Extra
+        mem.extra = RadioSettingGroup("extra", "Extra")
+
+        if _mem.recvmode == 0xFF:
+            val = 0x00
+        else:
+            val = _mem.recvmode
+        recvmode = RadioSetting("recvmode", "Receiving mode",
+                                RadioSettingValueList(LIST_RECVMODE,
+                                    LIST_RECVMODE[val]))
+        mem.extra.append(recvmode)
+
+        if _mem.botsignal == 0xFF:
+            val = 0x00
+        else:
+            val = _mem.botsignal
+        botsignal = RadioSetting("botsignal", "Launch signaling",
+                                 RadioSettingValueList(LIST_SIGNAL,
+                                     LIST_SIGNAL[val]))
+        mem.extra.append(botsignal)
+
+        if _mem.eotsignal == 0xFF:
+            val = 0x00
+        else:
+            val = _mem.eotsignal
+
+        rx = RadioSettingValueList(LIST_SIGNAL, LIST_SIGNAL[val])
+        eotsignal = RadioSetting("eotsignal", "Transmit end signaling", rx)
+        mem.extra.append(eotsignal)
+
+        rx = RadioSettingValueBoolean(bool(_mem.compandor))
+        compandor = RadioSetting("compandor", "Compandor", rx)
+        mem.extra.append(compandor)
+
+        rx = RadioSettingValueBoolean(bool(_mem.scrambler))
+        scrambler = RadioSetting("scrambler", "Scrambler", rx)
+        mem.extra.append(scrambler)
+
+        return mem
+
+    def set_memory(self, mem):
+        _mem = self._memory_obj()[mem.number - 1]
+
+        if mem.empty:
+            _mem.set_raw("\xff" * 24)
+            _mem.namelen = 0
+            return
+
+        _mem.set_raw("\xFF" * 15 + "\x00\x00" + "\xFF" * 7)
+
+        _mem.rxfreq = mem.freq / 10
+        if mem.duplex == "off":
+            _mem.txfreq = 0xFFFFFFFF
+        elif mem.duplex == "split":
+            _mem.txfreq = mem.offset / 10
+        elif mem.duplex == "+":
+            _mem.txfreq = (mem.freq + mem.offset) / 10
+        elif mem.duplex == "-":
+            _mem.txfreq = (mem.freq - mem.offset) / 10
+        else:
+            _mem.txfreq = mem.freq / 10
+
+        _mem.namelen = len(mem.name)
+        _namelength = self.get_features().valid_name_length
+        for i in range(_namelength):
+            try:
+                _mem.name[i] = ord(mem.name[i])
+            except IndexError:
+                _mem.name[i] = 0xFF
+
+        rxmode = ""
+        txmode = ""
+
+        if mem.tmode == "Tone":
+            txmode = "Tone"
+        elif mem.tmode == "TSQL":
+            rxmode = "Tone"
+            txmode = "TSQL"
+        elif mem.tmode == "DTCS":
+            rxmode = "DTCSSQL"
+            txmode = "DTCS"
+        elif mem.tmode == "Cross":
+            txmode, rxmode = mem.cross_mode.split("->", 1)
+
+        if rxmode == "":
+            _mem.rxdtcs_pol = 1
+            _mem.is_rxdigtone = 1
+            _mem.rxtone = 0x3FFF
+        elif rxmode == "Tone":
+            _mem.rxdtcs_pol = 0
+            _mem.is_rxdigtone = 0
+            _mem.rxtone = int(mem.ctone * 10)
+        elif rxmode == "DTCSSQL":
+            _mem.rxdtcs_pol = 1 if mem.dtcs_polarity[1] == "R" else 0
+            _mem.is_rxdigtone = 1
+            _mem.rxtone = self._set_dcs(mem.dtcs)
+        elif rxmode == "DTCS":
+            _mem.rxdtcs_pol = 1 if mem.dtcs_polarity[1] == "R" else 0
+            _mem.is_rxdigtone = 1
+            _mem.rxtone = self._set_dcs(mem.rx_dtcs)
+
+        if txmode == "":
+            _mem.txdtcs_pol = 1
+            _mem.is_txdigtone = 1
+            _mem.txtone = 0x3FFF
+        elif txmode == "Tone":
+            _mem.txdtcs_pol = 0
+            _mem.is_txdigtone = 0
+            _mem.txtone = int(mem.rtone * 10)
+        elif txmode == "TSQL":
+            _mem.txdtcs_pol = 0
+            _mem.is_txdigtone = 0
+            _mem.txtone = int(mem.ctone * 10)
+        elif txmode == "DTCS":
+            _mem.txdtcs_pol = 1 if mem.dtcs_polarity[0] == "R" else 0
+            _mem.is_txdigtone = 1
+            _mem.txtone = self._set_dcs(mem.dtcs)
+
+        _mem.wide = self.MODES.index(mem.mode)
+        _mem.power = mem.power == POWER_LEVELS[1]
+
+        # Extra settings
+        for setting in mem.extra:
+            setattr(_mem, setting.get_name(), setting.value)
+
+    def get_settings(self):
+        """Translate the bit in the mem_struct into settings in the UI"""
+        # Define mem struct write-back shortcuts
+        _sets = self._memobj.settings
+        _vfoa = self._memobj.upper.vfoa
+        _vfob = self._memobj.lower.vfob
+        _lims = self._memobj.hello_lims
+        _codes = self._memobj.codes
+        _dtmf = self._memobj.dtmf_tab
+
+        basic = RadioSettingGroup("basic", "Basic Settings")
+        a_band = RadioSettingGroup("a_band", "VFO A-Upper Settings")
+        b_band = RadioSettingGroup("b_band", "VFO B-Lower Settings")
+        codes = RadioSettingGroup("codes", "Codes & DTMF Groups")
+        lims = RadioSettingGroup("lims", "PowerOn & Freq Limits")
+        group = RadioSettings(basic, a_band, b_band, lims, codes)
+
+        # Basic Settings
+        bnd_mode = RadioSetting("settings.init_bank", "TDR Band Default",
+                                RadioSettingValueList(LIST_TDR_DEF,
+                                    LIST_TDR_DEF[ _sets.init_bank]))
+        basic.append(bnd_mode)
+
+        volume = RadioSetting("settings.volume", "Volume",
+                              RadioSettingValueInteger(0, 20, _sets.volume))
+        basic.append(volume)
+
+        val = _vfoa.bpower        # 2bits values 0,1,2= Low, Mid, High
+        rx = RadioSettingValueList(LIST_BPOWER, LIST_BPOWER[val])
+        powera = RadioSetting("upper.vfoa.bpower", "Power (Upper)", rx)
+        basic.append(powera)
+
+        val = _vfob.bpower
+        rx = RadioSettingValueList(LIST_BPOWER, LIST_BPOWER[val])
+        powerb = RadioSetting("lower.vfob.bpower", "Power (Lower)", rx)
+        basic.append(powerb)
+
+        def my_word2raw(setting, obj, atrb, mlt=10):
+            """Callback function to convert UI floating value to u16 int"""
+            if str(setting.value) == "Off":
+               frq = 0x0FFFF
+            else:
+                frq = int(float(str(setting.value)) * float(mlt))
+            if frq == 0:
+                frq = 0xFFFF
+            setattr(obj, atrb, frq)
+            return
+
+        def my_adjraw(setting, obj, atrb, fix):
+            """Callback: add or subtract fix from value."""
+            vx = int(str(setting.value))
+            value = vx  + int(fix)
+            if value < 0:
+                value = 0
+            if atrb == "frq_chn_mode" and int(str(setting.value)) == 2:
+                value = vx * 2         # Special handling for frq_chn_mode
+            setattr(obj, atrb, value)
+            return
+
+        def my_dbl2raw(setting, obj, atrb, flg=1):
+            """Callback: convert from freq 146.7600 to 14760000 U32."""
+            value = chirp_common.parse_freq(str(setting.value)) / 10
+            # flg=1 means 0 becomes ff, else leave as possible 0
+            if flg == 1 and value == 0:
+                value = 0xFFFFFFFF
+            setattr(obj, atrb, value)
+            return
+
+        def my_val_list(setting, obj, atrb):
+            """Callback:from ValueList with non-sequential, actual values."""
+            value = int(str(setting.value))            # Get the integer value
+            if atrb == "tot":
+                value = int(value / 30)    # 30 second increments
+            setattr(obj, atrb, value)
+            return
+
+        def my_spcl(setting, obj, atrb):
+            """Callback: Special handling based on atrb."""
+            if atrb == "frq_chn_mode":
+                idx = LIST_VFOMODE.index (str(setting.value))  # Returns 0 or 1
+                value = idx * 2            # Set bit 1
+            setattr(obj, atrb, value)
+            return
+
+        def my_tone_strn(obj, is_atr, pol_atr, tone_atr):
+            """Generate the CTCS/DCS tone code string."""
+            vx = int(getattr(obj, tone_atr))
+            if vx == 16383 or vx == 0:
+                return "Off"                 # 16383 is all bits set
+            if getattr(obj, is_atr) == 0:             # Simple CTCSS code
+                tstr = str(vx / 10.0)
+            else:        # DCS
+                if getattr(obj, pol_atr) == 0:
+                    tstr = "D{:03x}R".format(vx)
+                else:
+                    tstr = "D{:03x}N".format(vx)
+            return tstr
+
+        def my_set_tone(setting, obj, is_atr, pol_atr, tone_atr):
+            """Callback- create the tone setting from string code."""
+            sx = str(setting.value)        # '131.8'  or 'D231N' or 'Off'
+            if sx == "Off":
+                isx = 1
+                polx = 1
+                tonx = 0x3FFF
+            elif sx[0] == "D":         # DCS
+                isx = 1
+                if sx[4] == "N":
+                    polx = 1
+                else:
+                    polx = 0
+                tonx = int(sx[1:4], 16)
+            else:                                     # CTCSS
+                isx = 0
+                polx = 0
+                tonx = int(float(sx) * 10.0)
+            setattr(obj, is_atr, isx)
+            setattr(obj, pol_atr, polx)
+            setattr(obj, tone_atr, tonx)
+            return
+
+        val = _sets.fm_freq / 10.0
+        if val == 0:
+            val = 88.9            # 0 is not valid
+        rx = RadioSettingValueFloat(65, 108.0, val, 0.1, 1)
+        rs = RadioSetting("settings.fm_freq", "FM Broadcast Freq (MHz)", rx)
+        rs.set_apply_callback(my_word2raw, _sets, "fm_freq")
+        basic.append(rs)
+
+        wtled = RadioSetting("settings.wtled", "Standby LED Color",
+                             RadioSettingValueList(LIST_COLOR, LIST_COLOR[
+                                 _sets.wtled]))
+        basic.append(wtled)
+
+        rxled = RadioSetting("settings.rxled", "RX LED Color",
+                             RadioSettingValueList(LIST_COLOR, LIST_COLOR[
+                                 _sets.rxled]))
+        basic.append(rxled)
+
+        txled = RadioSetting("settings.txled", "TX LED Color",
+                             RadioSettingValueList(LIST_COLOR, LIST_COLOR[
+                                 _sets.txled]))
+        basic.append(txled)
+
+        ledsw = RadioSetting("settings.ledsw", "Back light mode",
+                             RadioSettingValueList(LIST_LEDSW, LIST_LEDSW[
+                                 _sets.ledsw]))
+        basic.append(ledsw)
+
+        beep = RadioSetting("settings.beep", "Beep",
+                            RadioSettingValueBoolean(bool(_sets.beep)))
+        basic.append(beep)
+
+        ring = RadioSetting("settings.ring", "Ring",
+                            RadioSettingValueList(LIST_RING, LIST_RING[
+                                _sets.ring]))
+        basic.append(ring)
+
+        bcl = RadioSetting("settings.bcl", "Busy channel lockout",
+                           RadioSettingValueBoolean(bool(_sets.bcl)))
+        basic.append(bcl)
+
+        if _vfoa.sql == 0xFF:
+            val = 0x04
+        else:
+            val = _vfoa.sql
+        sqla = RadioSetting("upper.vfoa.sql", "Squelch (Upper)",
+                            RadioSettingValueInteger(0, 9, val))
+        basic.append(sqla)
+
+        if _vfob.sql == 0xFF:
+            val = 0x04
+        else:
+            val = _vfob.sql
+        sqlb = RadioSetting("lower.vfob.sql", "Squelch (Lower)",
+                            RadioSettingValueInteger(0, 9, val))
+        basic.append(sqlb)
+
+        tmp = str(int(_sets.tot) * 30)     # 30 sec step counter
+        rs = RadioSetting("settings.tot", "Transmit Timeout (Secs)",
+                           RadioSettingValueList(LIST_TIMEOUT, tmp))
+        rs.set_apply_callback(my_val_list, _sets, "tot")
+        basic.append(rs)
+
+        tmp = str(int(_sets.sig_freq))
+        rs = RadioSetting("settings.sig_freq", "Single Signaling Tone (Htz)",
+                          RadioSettingValueList(LIST_SSF, tmp))
+        rs.set_apply_callback(my_val_list, _sets, "sig_freq")
+        basic.append(rs)
+
+        tmp = str(int(_sets.dtmf_txms))
+        rs = RadioSetting("settings.dtmf_txms", "DTMF Tx Duration (mSecs)",
+                          RadioSettingValueList(LIST_DTMFTX, tmp))
+        rs.set_apply_callback(my_val_list, _sets, "dtmf_txms")
+        basic.append(rs)
+
+        rs = RadioSetting("settings.rptr_mode", "Repeater Mode",
+                          RadioSettingValueBoolean(bool(_sets.rptr_mode)))
+        basic.append(rs)
+
+        # UPPER BAND SETTINGS
+
+        # Freq Mode, convert bit 1 state to index pointer
+        val = _vfoa.frq_chn_mode / 2
+
+        rx = RadioSettingValueList(LIST_VFOMODE, LIST_VFOMODE[val])
+        rs = RadioSetting("upper.vfoa.frq_chn_mode", "Default Mode", rx)
+        rs.set_apply_callback(my_spcl, _vfoa, "frq_chn_mode")
+        a_band.append(rs)
+
+        val =_vfoa.chan_num + 1                  # Add 1 for 1-128 displayed
+        rs = RadioSetting("upper.vfoa.chan_num", "Initial Chan",
+                          RadioSettingValueInteger(1, 128, val))
+        rs.set_apply_callback(my_adjraw, _vfoa, "chan_num", -1)
+        a_band.append(rs)
+
+        val = _vfoa.rxfreq / 100000.0
+        if (val < 136.0 or val > 176.0):
+            val = 146.520            # 2m calling
+        rs = RadioSetting("upper.vfoa.rxfreq ", "Default Recv Freq (MHz)",
+                          RadioSettingValueFloat(136.0, 176.0, val, 0.001, 5))
+        rs.set_apply_callback(my_dbl2raw, _vfoa, "rxfreq")
+        a_band.append(rs)
+
+        tmp = my_tone_strn(_vfoa, "is_rxdigtone", "rxdtcs_pol", "rx_tone")
+        rs = RadioSetting("rx_tone", "Default Recv CTCSS (Htz)",
+                          RadioSettingValueList(LIST_CTCSS, tmp))
+        rs.set_apply_callback(my_set_tone, _vfoa, "is_rxdigtone",
+                              "rxdtcs_pol", "rx_tone")
+        a_band.append(rs)
+
+        rx = RadioSettingValueList(LIST_RECVMODE,
+                                   LIST_RECVMODE[_vfoa.rx_mode])
+        rs = RadioSetting("upper.vfoa.rx_mode", "Default Recv Mode", rx)
+        a_band.append(rs)
+
+        tmp = my_tone_strn(_vfoa, "is_txdigtone", "txdtcs_pol", "tx_tone")
+        rs = RadioSetting("tx_tone", "Default Xmit CTCSS (Htz)",
+                          RadioSettingValueList(LIST_CTCSS, tmp))
+        rs.set_apply_callback(my_set_tone, _vfoa, "is_txdigtone",
+                              "txdtcs_pol", "tx_tone")
+        a_band.append(rs)
+
+        rs = RadioSetting("upper.vfoa.launch_sig", "Launch Signaling",
+                          RadioSettingValueList(LIST_SIGNAL,
+                              LIST_SIGNAL[_vfoa.launch_sig]))
+        a_band.append(rs)
+
+        rx = RadioSettingValueList(LIST_SIGNAL,LIST_SIGNAL[_vfoa.tx_end_sig])
+        rs = RadioSetting("upper.vfoa.tx_end_sig", "Xmit End Signaling", rx)
+        a_band.append(rs)
+
+        rx = RadioSettingValueList(LIST_BW, LIST_BW[_vfoa.fm_bw])
+        rs = RadioSetting("upper.vfoa.fm_bw", "Wide/Narrow Band", rx)
+        a_band.append(rs)
+
+        rx = RadioSettingValueBoolean(bool(_vfoa.cmp_nder))
+        rs = RadioSetting("upper.vfoa.cmp_nder", "Compandor", rx)
+        a_band.append(rs)
+
+        rs = RadioSetting("upper.vfoa.scrm_blr", "Scrambler",
+                          RadioSettingValueBoolean(bool(_vfoa.scrm_blr)))
+        a_band.append(rs)
+
+        rx = RadioSettingValueList(LIST_SHIFT, LIST_SHIFT[_vfoa.shift])
+        rs = RadioSetting("upper.vfoa.shift", "Xmit Shift", rx)
+        a_band.append(rs)
+
+        val = _vfoa.offset / 100000.0
+        rs = RadioSetting("upper.vfoa.offset", "Xmit Offset (MHz)",
+                          RadioSettingValueFloat(0, 100.0, val, 0.001, 3))
+        # Allow zero value
+        rs.set_apply_callback(my_dbl2raw, _vfoa, "offset", 0)
+        a_band.append(rs)
+
+        tmp = str(_vfoa.step / 100.0)
+        rs = RadioSetting("step", "Freq step (KHz)",
+                          RadioSettingValueList(LIST_STEPS, tmp))
+        rs.set_apply_callback(my_word2raw, _vfoa,"step", 100)
+        a_band.append(rs)
+
+        # LOWER BAND SETTINGS
+
+        val = _vfob.frq_chn_mode / 2
+        rx = RadioSettingValueList(LIST_VFOMODE, LIST_VFOMODE[val])
+        rs = RadioSetting("lower.vfob.frq_chn_mode", "Default Mode", rx)
+        rs.set_apply_callback(my_spcl, _vfob, "frq_chn_mode")
+        b_band.append(rs)
+
+        val = _vfob.chan_num + 1
+        rs = RadioSetting("lower.vfob.chan_num", "Initial Chan",
+                          RadioSettingValueInteger(0, 127, val))
+        rs.set_apply_callback(my_adjraw, _vfob, "chan_num", -1)
+        b_band.append(rs)
+
+        val = _vfob.rxfreq / 100000.0
+        if (val < 400.0 or val > 480.0):
+            val = 446.0          # UHF calling
+        rs = RadioSetting("lower.vfob.rxfreq ", "Default Recv Freq (MHz)",
+                          RadioSettingValueFloat(400.0, 480.0, val, 0.001, 5))
+        rs.set_apply_callback(my_dbl2raw, _vfob, "rxfreq")
+        b_band.append(rs)
+
+        tmp = my_tone_strn(_vfob, "is_rxdigtone", "rxdtcs_pol", "rx_tone")
+        rs = RadioSetting("rx_tone", "Default Recv CTCSS (Htz)",
+                          RadioSettingValueList(LIST_CTCSS, tmp))
+        rs.set_apply_callback(my_set_tone, _vfob, "is_rxdigtone",
+                              "rxdtcs_pol", "rx_tone")
+        b_band.append(rs)
+
+        rx = RadioSettingValueList(LIST_RECVMODE, LIST_RECVMODE[_vfob.rx_mode])
+        rs = RadioSetting("lower.vfob.rx_mode", "Default Recv Mode", rx)
+        b_band.append(rs)
+
+        tmp = my_tone_strn(_vfob, "is_txdigtone", "txdtcs_pol", "tx_tone")
+        rs = RadioSetting("tx_tone", "Default Xmit CTCSS (Htz)",
+                          RadioSettingValueList(LIST_CTCSS, tmp))
+        rs.set_apply_callback(my_set_tone, _vfob, "is_txdigtone",
+                              "txdtcs_pol", "tx_tone")
+        b_band.append(rs)
+
+        rx = RadioSettingValueList(LIST_SIGNAL,LIST_SIGNAL[_vfob.launch_sig])
+        rs = RadioSetting("lower.vfob.launch_sig", "Launch Signaling", rx)
+        b_band.append(rs)
+
+        rx = RadioSettingValueList(LIST_SIGNAL,LIST_SIGNAL[_vfob.tx_end_sig])
+        rs = RadioSetting("lower.vfob.tx_end_sig", "Xmit End Signaling", rx)
+        b_band.append(rs)
+
+        rx = RadioSettingValueList(LIST_BW, LIST_BW[_vfob.fm_bw])
+        rs = RadioSetting("lower.vfob.fm_bw", "Wide/Narrow Band", rx)
+        b_band.append(rs)
+
+        rs = RadioSetting("lower.vfob.cmp_nder", "Compandor",
+                          RadioSettingValueBoolean(bool(_vfob.cmp_nder)))
+        b_band.append(rs)
+
+        rs = RadioSetting("lower.vfob.scrm_blr", "Scrambler",
+                          RadioSettingValueBoolean(bool(_vfob.scrm_blr)))
+        b_band.append(rs)
+
+        rx = RadioSettingValueList(LIST_SHIFT, LIST_SHIFT[_vfob.shift])
+        rs = RadioSetting("lower.vfob.shift", "Xmit Shift", rx)
+        b_band.append(rs)
+
+        val = _vfob.offset / 100000.0
+        rs = RadioSetting("lower.vfob.offset", "Xmit Offset (MHz)",
+                          RadioSettingValueFloat(0, 100.0, val, 0.001, 3))
+        rs.set_apply_callback(my_dbl2raw, _vfob, "offset", 0)
+        b_band.append(rs)
+
+        tmp = str(_vfob.step / 100.0)
+        rs = RadioSetting("step", "Freq step (KHz)",
+                          RadioSettingValueList(LIST_STEPS, tmp))
+        rs.set_apply_callback(my_word2raw, _vfob, "step", 100)
+        b_band.append(rs)
+
+        # PowerOn & Freq Limits Settings
+
+        def chars2str(cary, knt):
+            """Convert raw memory char array to a string: NOT a callback."""
+            stx = ""
+            for char in cary[:knt]:
+                stx += chr(char)
+            return stx
+
+        def my_str2ary(setting, obj, atrba, atrbc):
+            """Callback: convert 7-char string to char array with count."""
+            ary = ""
+            knt = 7
+            for j in range (6, -1, -1):       # Strip trailing spaces
+                if str(setting.value)[j] == "" or str(setting.value)[j] == " ":
+                    knt = knt - 1
+                else:
+                    break
+            for j in range(0, 7, 1):
+                if j < knt: ary += str(setting.value)[j]
+                else: ary += chr(0xFF)
+            setattr(obj, atrba, ary)
+            setattr(obj, atrbc, knt)
+            return
+
+        tmp = chars2str(_lims.hello1, _lims.hello1_cnt)
+        rs = RadioSetting("hello_lims.hello1", "Power-On Message 1",
+                          RadioSettingValueString(0, 7, tmp))
+        rs.set_apply_callback(my_str2ary, _lims, "hello1", "hello1_cnt")
+        lims.append(rs)
+
+        tmp = chars2str(_lims.hello2, _lims.hello2_cnt)
+        rs = RadioSetting("hello_lims.hello2", "Power-On Message 2",
+                          RadioSettingValueString(0, 7, tmp))
+        rs.set_apply_callback(my_str2ary, _lims,"hello2", "hello2_cnt")
+        lims.append(rs)
+
+        # VALID_BANDS = [(136000000, 176000000),400000000, 480000000)]
+
+        lval = _lims.vhf_low / 100000.0
+        uval = _lims.vhf_high / 100000.0
+        if lval >= uval:
+            lval = 144.0
+            uval = 158.0
+
+        rs = RadioSetting("hello_lims.vhf_low", "Lower VHF Band Limit (MHz)",
+                          RadioSettingValueFloat(136.0, 176.0, lval, 0.001, 3))
+        rs.set_apply_callback(my_dbl2raw, _lims, "vhf_low")
+        lims.append(rs)
+
+        rs = RadioSetting("hello_lims.vhf_high", "Upper VHF Band Limit (MHz)",
+                          RadioSettingValueFloat(136.0, 176.0, uval, 0.001, 3))
+        rs.set_apply_callback(my_dbl2raw, _lims, "vhf_high")
+        lims.append(rs)
+
+        lval = _lims.uhf_low / 100000.0
+        uval = _lims.uhf_high / 100000.0
+        if lval >= uval:
+            lval = 420.0
+            uval = 470.0
+
+        rs = RadioSetting("hello_lims.uhf_low", "Lower UHF Band Limit (MHz)",
+                          RadioSettingValueFloat(400.0, 480.0, lval, 0.001, 3))
+        rs.set_apply_callback(my_dbl2raw, _lims, "uhf_low")
+        lims.append(rs)
+
+        rs = RadioSetting("hello_lims.uhf_high", "Upper UHF Band Limit (MHz)",
+                          RadioSettingValueFloat(400.0, 480.0, uval, 0.001, 3))
+        rs.set_apply_callback(my_dbl2raw, _lims, "uhf_high")
+        lims.append(rs)
+
+        # Codes and DTMF Groups Settings
+
+        def make_dtmf(ary, knt):
+            """Generate the DTMF code 1-8, NOT a callback."""
+            tmp = ""
+            if knt > 0 and knt != 0xff:
+                for  val in ary[:knt]:
+                    if val > 0 and val <= 9:
+                        tmp += chr(val + 48)
+                    elif val == 0x0a:
+                        tmp += "0"
+                    elif val == 0x0d:
+                        tmp += "A"
+                    elif val == 0x0e:
+                        tmp += "B"
+                    elif val == 0x0f:
+                        tmp += "C"
+                    elif val == 0x00:
+                        tmp += "D"
+                    elif val == 0x0b:
+                        tmp += "*"
+                    elif val == 0x0c:
+                        tmp += "#"
+                    else:
+                        msg = ("Invalid Character. Must be: 0-9,A,B,C,D,*,#")
+                        raise InvalidValueError(msg)
+            return tmp
+
+        def my_dtmf2raw(setting, obj, atrba, atrbc, syz=7):
+            """Callback: DTMF Code; sends 5 or 7-byte string."""
+            draw = []
+            knt = syz
+            for j in range (syz - 1, -1, -1):       # Strip trailing spaces
+                if str(setting.value)[j] == "" or str(setting.value)[j] == " ":
+                    knt = knt - 1
+                else:
+                    break
+            for j in range(0, syz):
+                bx = str(setting.value)[j]
+                obx = ord(bx)
+                dig = 0x0ff
+                if j < knt and knt > 0:      # (Else) is pads
+                    if  bx == "0":
+                        dig = 0x0a
+                    elif  bx == "A":
+                        dig = 0x0d
+                    elif  bx == "B":
+                        dig = 0x0e
+                    elif  bx == "C":
+                        dig = 0x0f
+                    elif  bx == "D":
+                        dig = 0x00
+                    elif  bx == "*":
+                        dig = 0x0b
+                    elif  bx == "#":
+                        dig = 0x0c
+                    elif obx >= 49 and obx <= 57:
+                        dig = obx - 48
+                    else:
+                        msg = ("Must be: 0-9,A,B,C,D,*,#")
+                        raise InvalidValueError(msg)
+                    # - End if/elif/else for bx
+                # - End if J<=knt
+                draw.append(dig)         # Generate string of bytes
+            # - End for j
+            setattr(obj, atrba, draw)
+            setattr(obj, atrbc, knt)
+            return
+
+        tmp = make_dtmf(_codes.native_id_code, _codes.native_id_cnt)
+        rs = RadioSetting("codes.native_id_code", "Native ID Code",
+                          RadioSettingValueString(0, 7, tmp))
+        rs.set_apply_callback(my_dtmf2raw, _codes, "native_id_code",
+                              "native_id_cnt", 7)
+        codes.append(rs)
+
+        tmp = make_dtmf(_codes.master_id_code, _codes.master_id_cnt)
+        rs = RadioSetting("codes.master_id_code", "Master Control ID Code",
+                          RadioSettingValueString(0, 7, tmp))
+        rs.set_apply_callback(my_dtmf2raw, _codes, "master_id_code",
+                              "master_id_cnt",7)
+        codes.append(rs)
+
+        tmp = make_dtmf(_codes.alarm_code, _codes.alarm_cnt)
+        rs = RadioSetting("codes.alarm_code", "Alarm Code",
+                            RadioSettingValueString(0, 5, tmp))
+        rs.set_apply_callback(my_dtmf2raw, _codes, "alarm_code",
+                            "alarm_cnt", 5)
+        codes.append(rs)
+
+        tmp = make_dtmf(_codes.id_disp_code, _codes.id_disp_cnt)
+        rs = RadioSetting("codes.id_disp_code", "Identify Display Code",
+                          RadioSettingValueString(0, 5, tmp))
+        rs.set_apply_callback(my_dtmf2raw, _codes, "id_disp_code",
+                              "id_disp_cnt", 5)
+        codes.append(rs)
+
+        tmp = make_dtmf(_codes.revive_code, _codes.revive_cnt)
+        rs = RadioSetting("codes.revive_code", "Revive Code",
+                          RadioSettingValueString(0, 5, tmp))
+        rs.set_apply_callback(my_dtmf2raw, _codes,"revive_code",
+                              "revive_cnt", 5)
+        codes.append(rs)
+
+        tmp = make_dtmf(_codes.stun_code, _codes.stun_cnt)
+        rs = RadioSetting("codes.stun_code", "Remote Stun Code",
+                          RadioSettingValueString(0, 5, tmp))
+        rs.set_apply_callback(my_dtmf2raw,  _codes, "stun_code",
+                              "stun_cnt", 5)
+        codes.append(rs)
+
+        tmp = make_dtmf(_codes.kill_code, _codes.kill_cnt)
+        rs = RadioSetting("codes.kill_code", "Remote KILL Code",
+                          RadioSettingValueString(0, 5, tmp))
+        rs.set_apply_callback(my_dtmf2raw, _codes, "kill_code",
+                              "kill_cnt", 5)
+        codes.append(rs)
+
+        tmp = make_dtmf(_codes.monitor_code, _codes.monitor_cnt)
+        rs = RadioSetting("codes.monitor_code", "Monitor Code",
+                          RadioSettingValueString(0, 5, tmp))
+        rs.set_apply_callback(my_dtmf2raw, _codes, "monitor_code",
+                              "monitor_cnt", 5)
+        codes.append(rs)
+
+        val = _codes.state_now
+        if val > 2:
+            val = 0
+
+        rx = RadioSettingValueList(LIST_STATE, LIST_STATE[val])
+        rs = RadioSetting("codes.state_now", "Current State", rx)
+        codes.append(rs)
+
+        dtm = make_dtmf(_dtmf.dtmf1, _dtmf.dtmf1_cnt)
+        rs = RadioSetting("dtmf_tab.dtmf1", "DTMF1 String",
+                          RadioSettingValueString(0, 7, dtm))
+        rs.set_apply_callback(my_dtmf2raw, _dtmf, "dtmf1", "dtmf1_cnt")
+        codes.append(rs)
+
+        dtm = make_dtmf(_dtmf.dtmf2, _dtmf.dtmf2_cnt)
+        rs = RadioSetting("dtmf_tab.dtmf2", "DTMF2 String",
+                          RadioSettingValueString(0, 7, dtm))
+        rs.set_apply_callback(my_dtmf2raw, _dtmf, "dtmf2", "dtmf2_cnt")
+        codes.append(rs)
+
+        dtm = make_dtmf(_dtmf.dtmf3, _dtmf.dtmf3_cnt)
+        rs = RadioSetting("dtmf_tab.dtmf3", "DTMF3 String",
+                          RadioSettingValueString(0, 7, dtm))
+        rs.set_apply_callback(my_dtmf2raw, _dtmf, "dtmf3", "dtmf3_cnt")
+        codes.append(rs)
+
+        dtm = make_dtmf(_dtmf.dtmf4, _dtmf.dtmf4_cnt)
+        rs = RadioSetting("dtmf_tab.dtmf4", "DTMF4 String",
+                          RadioSettingValueString(0, 7, dtm))
+        rs.set_apply_callback(my_dtmf2raw, _dtmf, "dtmf4", "dtmf4_cnt")
+        codes.append(rs)
+
+        dtm = make_dtmf(_dtmf.dtmf5, _dtmf.dtmf5_cnt)
+        rs = RadioSetting("dtmf_tab.dtmf5", "DTMF5 String",
+                          RadioSettingValueString(0, 7, dtm))
+        rs.set_apply_callback(my_dtmf2raw, _dtmf, "dtmf5", "dtmf5_cnt")
+        codes.append(rs)
+
+        dtm = make_dtmf(_dtmf.dtmf6, _dtmf.dtmf6_cnt)
+        rs = RadioSetting("dtmf_tab.dtmf6", "DTMF6 String",
+                          RadioSettingValueString(0, 7, dtm))
+        rs.set_apply_callback(my_dtmf2raw, _dtmf, "dtmf6", "dtmf6_cnt")
+        codes.append(rs)
+
+        dtm = make_dtmf(_dtmf.dtmf7, _dtmf.dtmf7_cnt)
+        rs = RadioSetting("dtmf_tab.dtmf7", "DTMF7 String",
+                          RadioSettingValueString(0, 7, dtm))
+        rs.set_apply_callback(my_dtmf2raw, _dtmf, "dtmf7", "dtmf7_cnt")
+        codes.append(rs)
+
+        dtm = make_dtmf(_dtmf.dtmf8, _dtmf.dtmf8_cnt)
+        rs = RadioSetting("dtmf_tab.dtmf8", "DTMF8 String",
+                          RadioSettingValueString(0, 7, dtm))
+        rs.set_apply_callback(my_dtmf2raw, _dtmf, "dtmf8", "dtmf8_cnt")
+        codes.append(rs)
+
+        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
+
+
+    @classmethod
+    def match_model(cls, filedata, filename):
+        match_size = False
+        match_model = False
+
+        # Testing the file data size
+        if len(filedata) == MEM_SIZE + 8:
+            match_size = True
+
+        # Testing the firmware model fingerprint
+        match_model = model_match(cls, filedata)
+
+        if match_size and match_model:
+            return True
+        else:
+            return False
+
+
+class LT725UVUpper(LT725UV):
+    VARIANT = "Upper"
+    _vfo = "upper"
+
+
+class LT725UVLower(LT725UV):
+    VARIANT = "Lower"
+    _vfo = "lower"
+
+
+class Zastone(chirp_common.Alias):
+    """Declare BJ-218 alias for Zastone BJ-218."""
+    VENDOR = "Zastone"
+    MODEL = "BJ-218"
+
+
+class Hesenate(chirp_common.Alias):
+    """Declare BJ-218 alias for Hesenate BJ-218."""
+    VENDOR = "Hesenate"
+    MODEL = "BJ-218"
+
+
+ at directory.register
+class Baojie218(LT725UV):
+    """Baojie BJ-218"""
+    VENDOR = "Baojie"
+    MODEL = "BJ-218"
+    ALIASES = [Zastone, Hesenate, ]
diff -r b08fbd75a499 -r ed11ed06d671 chirp/ui/mainapp.py
--- a/chirp/ui/mainapp.py	Wed Jun 13 06:14:11 2018 -0700
+++ b/chirp/ui/mainapp.py	Tue Jun 19 14:27:25 2018 -0700
@@ -1,2082 +1,2130 @@
-# Copyright 2008 Dan Smith <dsmith at danplanet.com>
-# Copyright 2012 Tom Hayward <tom at tomh.us>
-#
-# This program is free software: you can redistribute it and/or modify
-# it under the terms of the GNU General Public License as published by
-# the Free Software Foundation, either version 3 of the License, or
-# (at your option) any later version.
-#
-# This program is distributed in the hope that it will be useful,
-# but WITHOUT ANY WARRANTY; without even the implied warranty of
-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
-# GNU General Public License for more details.
-#
-# You should have received a copy of the GNU General Public License
-# along with this program.  If not, see <http://www.gnu.org/licenses/>.
-
-from datetime import datetime
-import os
-import tempfile
-import urllib
-import webbrowser
-from glob import glob
-import shutil
-import time
-import logging
-import gtk
-import gobject
-import sys
-
-from chirp.ui import inputdialog, common
-from chirp import platform, directory, util
-from chirp.drivers import generic_xml, generic_csv, repeaterbook
-from chirp.drivers import ic9x, kenwood_live, idrp, vx7, vx5, vx6
-from chirp.drivers import icf, ic9x_icf
-from chirp import CHIRP_VERSION, chirp_common, detect, errors
-from chirp.ui import editorset, clone, miscwidgets, config, reporting, fips
-from chirp.ui import bandplans
-
-gobject.threads_init()
-
-LOG = logging.getLogger(__name__)
-
-if __name__ == "__main__":
-    sys.path.insert(0, "..")
-
-try:
-    import serial
-except ImportError, e:
-    common.log_exception()
-    common.show_error("\nThe Pyserial module is not installed!")
-
-
-CONF = config.get()
-
-KEEP_RECENT = 8
-
-RB_BANDS = {
-    "--All--":                  0,
-    "10 meters (29MHz)":        29,
-    "6 meters (54MHz)":         5,
-    "2 meters (144MHz)":        14,
-    "1.25 meters (220MHz)":     22,
-    "70 centimeters (440MHz)":  4,
-    "33 centimeters (900MHz)":  9,
-    "23 centimeters (1.2GHz)":  12,
-}
-
-
-def key_bands(band):
-    if band.startswith("-"):
-        return -1
-
-    amount, units, mhz = band.split(" ")
-    scale = units == "meters" and 100 or 1
-
-    return 100000 - (float(amount) * scale)
-
-
-class ModifiedError(Exception):
-    pass
-
-
-class ChirpMain(gtk.Window):
-
-    def get_current_editorset(self):
-        page = self.tabs.get_current_page()
-        if page is not None:
-            return self.tabs.get_nth_page(page)
-        else:
-            return None
-
-    def ev_tab_switched(self, pagenum=None):
-        def set_action_sensitive(action, sensitive):
-            self.menu_ag.get_action(action).set_sensitive(sensitive)
-
-        if pagenum is not None:
-            eset = self.tabs.get_nth_page(pagenum)
-        else:
-            eset = self.get_current_editorset()
-
-        upload_sens = bool(eset and
-                           isinstance(eset.radio, chirp_common.CloneModeRadio))
-
-        if not eset or isinstance(eset.radio, chirp_common.LiveRadio):
-            save_sens = False
-        elif isinstance(eset.radio, chirp_common.NetworkSourceRadio):
-            save_sens = False
-        else:
-            save_sens = True
-
-        for i in ["import", "importsrc", "stock"]:
-            set_action_sensitive(i,
-                                 eset is not None and not eset.get_read_only())
-
-        for i in ["save", "saveas"]:
-            set_action_sensitive(i, save_sens)
-
-        for i in ["upload"]:
-            set_action_sensitive(i, upload_sens)
-
-        for i in ["cancelq"]:
-            set_action_sensitive(i, eset is not None and not save_sens)
-
-        for i in ["export", "close", "columns", "irbook", "irfinder",
-                  "move_up", "move_dn", "exchange", "iradioreference",
-                  "cut", "copy", "paste", "delete", "viewdeveloper",
-                  "all", "properties"]:
-            set_action_sensitive(i, eset is not None)
-
-    def ev_status(self, editorset, msg):
-        self.sb_radio.pop(0)
-        self.sb_radio.push(0, msg)
-
-    def ev_usermsg(self, editorset, msg):
-        self.sb_general.pop(0)
-        self.sb_general.push(0, msg)
-
-    def ev_editor_selected(self, editorset, editortype):
-        mappings = {
-            "memedit": ["view", "edit"],
-        }
-
-        for _editortype, actions in mappings.items():
-            for _action in actions:
-                action = self.menu_ag.get_action(_action)
-                action.set_sensitive(editortype.startswith(_editortype))
-
-    def _connect_editorset(self, eset):
-        eset.connect("want-close", self.do_close)
-        eset.connect("status", self.ev_status)
-        eset.connect("usermsg", self.ev_usermsg)
-        eset.connect("editor-selected", self.ev_editor_selected)
-
-    def do_diff_radio(self):
-        if self.tabs.get_n_pages() < 2:
-            common.show_error("Diff tabs requires at least two open tabs!")
-            return
-
-        esets = []
-        for i in range(0, self.tabs.get_n_pages()):
-            esets.append(self.tabs.get_nth_page(i))
-
-        d = gtk.Dialog(title="Diff Radios",
-                       buttons=(gtk.STOCK_OK, gtk.RESPONSE_OK,
-                                gtk.STOCK_CANCEL, gtk.RESPONSE_CANCEL),
-                       parent=self)
-
-        label = gtk.Label("")
-        label.set_markup("<b>-1</b> for either Mem # does a full-file hex " +
-                         "dump with diffs highlighted.\n" +
-                         "<b>-2</b> for first Mem # shows " +
-                         "<b>only</b> the diffs.")
-        d.vbox.pack_start(label, True, True, 0)
-        label.show()
-
-        choices = []
-        for eset in esets:
-            choices.append("%s %s (%s)" % (eset.rthread.radio.VENDOR,
-                                           eset.rthread.radio.MODEL,
-                                           eset.filename))
-        choice_a = miscwidgets.make_choice(choices, False, choices[0])
-        choice_a.show()
-        chan_a = gtk.SpinButton()
-        chan_a.get_adjustment().set_all(1, -2, 999, 1, 10, 0)
-        chan_a.show()
-        hbox = gtk.HBox(False, 3)
-        hbox.pack_start(choice_a, 1, 1, 1)
-        hbox.pack_start(chan_a, 0, 0, 0)
-        hbox.show()
-        d.vbox.pack_start(hbox, 0, 0, 0)
-
-        choice_b = miscwidgets.make_choice(choices, False, choices[1])
-        choice_b.show()
-        chan_b = gtk.SpinButton()
-        chan_b.get_adjustment().set_all(1, -1, 999, 1, 10, 0)
-        chan_b.show()
-        hbox = gtk.HBox(False, 3)
-        hbox.pack_start(choice_b, 1, 1, 1)
-        hbox.pack_start(chan_b, 0, 0, 0)
-        hbox.show()
-        d.vbox.pack_start(hbox, 0, 0, 0)
-
-        r = d.run()
-        sel_a = choice_a.get_active_text()
-        sel_chan_a = chan_a.get_value()
-        sel_b = choice_b.get_active_text()
-        sel_chan_b = chan_b.get_value()
-        d.destroy()
-        if r == gtk.RESPONSE_CANCEL:
-            return
-
-        if sel_a == sel_b:
-            common.show_error("Can't diff the same tab!")
-            return
-
-        LOG.debug("Selected %s@%i and %s@%i" %
-                  (sel_a, sel_chan_a, sel_b, sel_chan_b))
-        name_a = os.path.basename(sel_a)
-        name_a = name_a[:name_a.rindex(")")]
-        name_b = os.path.basename(sel_b)
-        name_b = name_b[:name_b.rindex(")")]
-        diffwintitle = "%s@%i  diff  %s@%i" % (
-            name_a, sel_chan_a, name_b, sel_chan_b)
-
-        eset_a = esets[choices.index(sel_a)]
-        eset_b = esets[choices.index(sel_b)]
-
-        def _show_diff(mem_b, mem_a):
-            # Step 3: Show the diff
-            diff = common.simple_diff(mem_a, mem_b)
-            common.show_diff_blob(diffwintitle, diff)
-
-        def _get_mem_b(mem_a):
-            # Step 2: Get memory b
-            job = common.RadioJob(_show_diff, "get_raw_memory",
-                                  int(sel_chan_b))
-            job.set_cb_args(mem_a)
-            eset_b.rthread.submit(job)
-
-        if sel_chan_a >= 0 and sel_chan_b >= 0:
-            # Diff numbered memory
-            # Step 1: Get memory a
-            job = common.RadioJob(_get_mem_b, "get_raw_memory",
-                                  int(sel_chan_a))
-            eset_a.rthread.submit(job)
-        elif isinstance(eset_a.rthread.radio, chirp_common.CloneModeRadio) and\
-                isinstance(eset_b.rthread.radio, chirp_common.CloneModeRadio):
-            # Diff whole (can do this without a job, since both are clone-mode)
-            try:
-                addrfmt = CONF.get('hexdump_addrfmt', section='developer',
-                                   raw=True)
-            except:
-                pass
-            a = util.hexprint(eset_a.rthread.radio._mmap.get_packed(),
-                              addrfmt=addrfmt)
-            b = util.hexprint(eset_b.rthread.radio._mmap.get_packed(),
-                              addrfmt=addrfmt)
-            if sel_chan_a == -2:
-                diffsonly = True
-            else:
-                diffsonly = False
-            common.show_diff_blob(diffwintitle,
-                                  common.simple_diff(a, b, diffsonly))
-        else:
-            common.show_error("Cannot diff whole live-mode radios!")
-
-    def do_new(self):
-        eset = editorset.EditorSet(_("Untitled") + ".csv", self)
-        self._connect_editorset(eset)
-        eset.prime()
-        eset.show()
-
-        tab = self.tabs.append_page(eset, eset.get_tab_label())
-        self.tabs.set_current_page(tab)
-
-    def _do_manual_select(self, filename):
-        radiolist = {}
-        for drv, radio in directory.DRV_TO_RADIO.items():
-            if not issubclass(radio, chirp_common.CloneModeRadio):
-                continue
-            radiolist["%s %s" % (radio.VENDOR, radio.MODEL)] = drv
-
-        lab = gtk.Label("""<b><big>Unable to detect model!</big></b>
-
-If you think that it is valid, you can select a radio model below to
-force an open attempt. If selecting the model manually works, please
-file a bug on the website and attach your image. If selecting the model
-does not work, it is likely that you are trying to open some other type
-of file.
-""")
-
-        lab.set_justify(gtk.JUSTIFY_FILL)
-        lab.set_line_wrap(True)
-        lab.set_use_markup(True)
-        lab.show()
-        choice = miscwidgets.make_choice(sorted(radiolist.keys()), False,
-                                         sorted(radiolist.keys())[0])
-        d = gtk.Dialog(title="Detection Failed",
-                       buttons=(gtk.STOCK_OK, gtk.RESPONSE_OK,
-                                gtk.STOCK_CANCEL, gtk.RESPONSE_CANCEL))
-        d.vbox.pack_start(lab, 0, 0, 0)
-        d.vbox.pack_start(choice, 0, 0, 0)
-        d.vbox.set_spacing(5)
-        choice.show()
-        d.set_default_size(400, 200)
-        # d.set_resizable(False)
-        r = d.run()
-        d.destroy()
-        if r != gtk.RESPONSE_OK:
-            return
-        try:
-            rc = directory.DRV_TO_RADIO[radiolist[choice.get_active_text()]]
-            return rc(filename)
-        except:
-            return
-
-    def do_open(self, fname=None, tempname=None):
-        if not fname:
-            types = [(_("All files") + " (*.*)", "*"),
-                     (_("CHIRP Radio Images") + " (*.img)", "*.img"),
-                     (_("CHIRP Files") + " (*.chirp)", "*.chirp"),
-                     (_("CSV Files") + " (*.csv)", "*.csv"),
-                     (_("DAT Files") + " (*.dat)", "*.dat"),
-                     (_("EVE Files (VX5)") + " (*.eve)", "*.eve"),
-                     (_("ICF Files") + " (*.icf)", "*.icf"),
-                     (_("VX5 Commander Files") + " (*.vx5)", "*.vx5"),
-                     (_("VX6 Commander Files") + " (*.vx6)", "*.vx6"),
-                     (_("VX7 Commander Files") + " (*.vx7)", "*.vx7"),
-                     ]
-            fname = platform.get_platform().gui_open_file(types=types)
-            if not fname:
-                return
-
-        self.record_recent_file(fname)
-
-        if icf.is_icf_file(fname):
-            a = common.ask_yesno_question(
-                _("ICF files cannot be edited, only displayed or imported "
-                  "into another file. Open in read-only mode?"),
-                self)
-            if not a:
-                return
-            read_only = True
-        else:
-            read_only = False
-
-        if icf.is_9x_icf(fname):
-            # We have to actually instantiate the IC9xICFRadio to get its
-            # sub-devices
-            radio = ic9x_icf.IC9xICFRadio(fname)
-        else:
-            try:
-                radio = directory.get_radio_by_image(fname)
-            except errors.ImageDetectFailed:
-                radio = self._do_manual_select(fname)
-                if not radio:
-                    return
-                LOG.debug("Manually selected %s" % radio)
-            except Exception, e:
-                common.log_exception()
-                common.show_error(os.path.basename(fname) + ": " + str(e))
-                return
-
-        first_tab = False
-        try:
-            eset = editorset.EditorSet(radio, self,
-                                       filename=fname,
-                                       tempname=tempname)
-        except Exception, e:
-            common.log_exception()
-            common.show_error(
-                _("There was an error opening {fname}: {error}").format(
-                    fname=fname,
-                    error=e))
-            return
-
-        eset.set_read_only(read_only)
-        self._connect_editorset(eset)
-        eset.show()
-        self.tabs.append_page(eset, eset.get_tab_label())
-
-        if hasattr(eset.rthread.radio, "errors") and \
-                eset.rthread.radio.errors:
-            msg = _("{num} errors during open:").format(
-                num=len(eset.rthread.radio.errors))
-            common.show_error_text(msg,
-                                   "\r\n".join(eset.rthread.radio.errors))
-
-    def do_live_warning(self, radio):
-        d = gtk.MessageDialog(parent=self, buttons=gtk.BUTTONS_OK)
-        d.set_markup("<big><b>" + _("Note:") + "</b></big>")
-        msg = _("The {vendor} {model} operates in <b>live mode</b>. "
-                "This means that any changes you make are immediately sent "
-                "to the radio. Because of this, you cannot perform the "
-                "<u>Save</u> or <u>Upload</u> operations. If you wish to "
-                "edit the contents offline, please <u>Export</u> to a CSV "
-                "file, using the <b>File menu</b>.")
-        msg = msg.format(vendor=radio.VENDOR, model=radio.MODEL)
-        d.format_secondary_markup(msg)
-
-        again = gtk.CheckButton(_("Don't show this again"))
-        again.show()
-        d.vbox.pack_start(again, 0, 0, 0)
-        d.run()
-        CONF.set_bool("live_mode", again.get_active(), "noconfirm")
-        d.destroy()
-
-    def do_open_live(self, radio, tempname=None, read_only=False):
-        eset = editorset.EditorSet(radio, self, tempname=tempname)
-        eset.connect("want-close", self.do_close)
-        eset.connect("status", self.ev_status)
-        eset.set_read_only(read_only)
-        eset.show()
-        self.tabs.append_page(eset, eset.get_tab_label())
-
-        if isinstance(radio, chirp_common.LiveRadio):
-            reporting.report_model_usage(radio, "live", True)
-            if not CONF.get_bool("live_mode", "noconfirm"):
-                self.do_live_warning(radio)
-
-    def do_save(self, eset=None):
-        if not eset:
-            eset = self.get_current_editorset()
-
-        # For usability, allow Ctrl-S to short-circuit to Save-As if
-        # we are working on a yet-to-be-saved image
-        if not os.path.exists(eset.filename):
-            return self.do_saveas()
-
-        eset.save()
-
-    def do_saveas(self):
-        eset = self.get_current_editorset()
-
-        label = _("{vendor} {model} image file").format(
-            vendor=eset.radio.VENDOR,
-            model=eset.radio.MODEL)
-
-        defname_format = CONF.get("default_filename", "global") or \
-            "{vendor}_{model}_{date}"
-        defname = defname_format.format(
-            vendor=eset.radio.VENDOR,
-            model=eset.radio.MODEL,
-            date=datetime.now().strftime('%Y%m%d')
-        ).replace('/', '_')
-
-        types = [(label + " (*.%s)" % eset.radio.FILE_EXTENSION,
-                  eset.radio.FILE_EXTENSION)]
-
-        if isinstance(eset.radio, vx7.VX7Radio):
-            types += [(_("VX7 Commander") + " (*.vx7)", "vx7")]
-        elif isinstance(eset.radio, vx6.VX6Radio):
-            types += [(_("VX6 Commander") + " (*.vx6)", "vx6")]
-        elif isinstance(eset.radio, vx5.VX5Radio):
-            types += [(_("EVE") + " (*.eve)", "eve")]
-            types += [(_("VX5 Commander") + " (*.vx5)", "vx5")]
-
-        while True:
-            fname = platform.get_platform().gui_save_file(default_name=defname,
-                                                          types=types)
-            if not fname:
-                return
-
-            if os.path.exists(fname):
-                dlg = inputdialog.OverwriteDialog(fname)
-                owrite = dlg.run()
-                dlg.destroy()
-                if owrite == gtk.RESPONSE_OK:
-                    break
-            else:
-                break
-
-        try:
-            eset.save(fname)
-        except Exception, e:
-            d = inputdialog.ExceptionDialog(e)
-            d.run()
-            d.destroy()
-
-    def cb_clonein(self, radio, emsg=None):
-        radio.pipe.close()
-        reporting.report_model_usage(radio, "download", bool(emsg))
-        if not emsg:
-            self.do_open_live(radio, tempname="(" + _("Untitled") + ")")
-        else:
-            d = inputdialog.ExceptionDialog(emsg)
-            d.run()
-            d.destroy()
-
-    def cb_cloneout(self, radio, emsg=None):
-        radio.pipe.close()
-        reporting.report_model_usage(radio, "upload", True)
-        if emsg:
-            d = inputdialog.ExceptionDialog(emsg)
-            d.run()
-            d.destroy()
-
-    def _get_recent_list(self):
-        recent = []
-        for i in range(0, KEEP_RECENT):
-            fn = CONF.get("recent%i" % i, "state")
-            if fn:
-                recent.append(fn)
-        return recent
-
-    def _set_recent_list(self, recent):
-        for fn in recent:
-            CONF.set("recent%i" % recent.index(fn), fn, "state")
-
-    def update_recent_files(self):
-        i = 0
-        for fname in self._get_recent_list():
-            action_name = "recent%i" % i
-            path = "/MenuBar/file/recent"
-
-            old_action = self.menu_ag.get_action(action_name)
-            if old_action:
-                self.menu_ag.remove_action(old_action)
-
-            file_basename = os.path.basename(fname).replace("_", "__")
-            action = gtk.Action(
-                action_name, "_%i. %s" % (i + 1, file_basename),
-                _("Open recent file {name}").format(name=fname), "")
-            action.connect("activate", lambda a, f: self.do_open(f), fname)
-            mid = self.menu_uim.new_merge_id()
-            self.menu_uim.add_ui(mid, path,
-                                 action_name, action_name,
-                                 gtk.UI_MANAGER_MENUITEM, False)
-            self.menu_ag.add_action(action)
-            i += 1
-
-    def record_recent_file(self, filename):
-
-        recent_files = self._get_recent_list()
-        if filename not in recent_files:
-            if len(recent_files) == KEEP_RECENT:
-                del recent_files[-1]
-            recent_files.insert(0, filename)
-            self._set_recent_list(recent_files)
-
-        self.update_recent_files()
-
-    def import_stock_config(self, action, config):
-        eset = self.get_current_editorset()
-        count = eset.do_import(config)
-
-    def copy_shipped_stock_configs(self, stock_dir):
-        basepath = platform.get_platform().find_resource("stock_configs")
-
-        files = glob(os.path.join(basepath, "*.csv"))
-        for fn in files:
-            if os.path.exists(os.path.join(stock_dir, os.path.basename(fn))):
-                LOG.info("Skipping existing stock config")
-                continue
-            try:
-                shutil.copy(fn, stock_dir)
-                LOG.debug("Copying %s -> %s" % (fn, stock_dir))
-            except Exception, e:
-                LOG.error("Unable to copy %s to %s: %s" % (fn, stock_dir, e))
-                return False
-        return True
-
-    def update_stock_configs(self):
-        stock_dir = platform.get_platform().config_file("stock_configs")
-        if not os.path.isdir(stock_dir):
-            try:
-                os.mkdir(stock_dir)
-            except Exception, e:
-                LOG.error("Unable to create directory: %s" % stock_dir)
-                return
-        if not self.copy_shipped_stock_configs(stock_dir):
-            return
-
-        def _do_import_action(config):
-            name = os.path.splitext(os.path.basename(config))[0]
-            action_name = "stock-%i" % configs.index(config)
-            path = "/MenuBar/radio/stock"
-            action = gtk.Action(action_name,
-                                name,
-                                _("Import stock "
-                                  "configuration {name}").format(name=name),
-                                "")
-            action.connect("activate", self.import_stock_config, config)
-            mid = self.menu_uim.new_merge_id()
-            mid = self.menu_uim.add_ui(mid, path,
-                                       action_name, action_name,
-                                       gtk.UI_MANAGER_MENUITEM, False)
-            self.menu_ag.add_action(action)
-
-        def _do_open_action(config):
-            name = os.path.splitext(os.path.basename(config))[0]
-            action_name = "openstock-%i" % configs.index(config)
-            path = "/MenuBar/file/openstock"
-            action = gtk.Action(action_name,
-                                name,
-                                _("Open stock "
-                                  "configuration {name}").format(name=name),
-                                "")
-            action.connect("activate", lambda a, c: self.do_open(c), config)
-            mid = self.menu_uim.new_merge_id()
-            mid = self.menu_uim.add_ui(mid, path,
-                                       action_name, action_name,
-                                       gtk.UI_MANAGER_MENUITEM, False)
-            self.menu_ag.add_action(action)
-
-        configs = glob(os.path.join(stock_dir, "*.csv"))
-        for config in configs:
-            _do_import_action(config)
-            _do_open_action(config)
-
-    def _confirm_experimental(self, rclass):
-        sql_key = "warn_experimental_%s" % directory.radio_class_id(rclass)
-        if CONF.is_defined(sql_key, "state") and \
-                not CONF.get_bool(sql_key, "state"):
-            return True
-
-        title = _("Proceed with experimental driver?")
-        text = rclass.get_prompts().experimental
-        msg = _("This radio's driver is experimental. "
-                "Do you want to proceed?")
-        resp, squelch = common.show_warning(msg, text,
-                                            title=title,
-                                            buttons=gtk.BUTTONS_YES_NO,
-                                            can_squelch=True)
-        if resp == gtk.RESPONSE_YES:
-            CONF.set_bool(sql_key, not squelch, "state")
-        return resp == gtk.RESPONSE_YES
-
-    def _show_instructions(self, radio, message):
-        if message is None:
-            return
-
-        if CONF.get_bool("clone_instructions", "noconfirm"):
-            return
-
-        d = gtk.MessageDialog(parent=self, buttons=gtk.BUTTONS_OK)
-        d.set_markup("<big><b>" + _("{name} Instructions").format(
-                     name=radio.get_name()) + "</b></big>")
-        msg = _("{instructions}").format(instructions=message)
-        d.format_secondary_markup(msg)
-
-        again = gtk.CheckButton(
-            _("Don't show instructions for any radio again"))
-        again.show()
-        again.connect("toggled", lambda action:
-                      self.clonemenu.set_active(not action.get_active()))
-        d.vbox.pack_start(again, 0, 0, 0)
-        h_button_box = d.vbox.get_children()[2]
-        try:
-            ok_button = h_button_box.get_children()[0]
-            ok_button.grab_default()
-            ok_button.grab_focus()
-        except AttributeError:
-            # don't grab focus on GTK+ 2.0
-            pass
-        d.run()
-        d.destroy()
-
-    def do_download(self, port=None, rtype=None):
-        d = clone.CloneSettingsDialog(parent=self)
-        settings = d.run()
-        d.destroy()
-        if not settings:
-            return
-
-        rclass = settings.radio_class
-        if issubclass(rclass, chirp_common.ExperimentalRadio) and \
-                not self._confirm_experimental(rclass):
-            # User does not want to proceed with experimental driver
-            return
-
-        self._show_instructions(rclass, rclass.get_prompts().pre_download)
-
-        LOG.debug("User selected %s %s on port %s" %
-                  (rclass.VENDOR, rclass.MODEL, settings.port))
-
-        try:
-            ser = serial.Serial(port=settings.port,
-                                baudrate=rclass.BAUD_RATE,
-                                rtscts=rclass.HARDWARE_FLOW,
-                                timeout=0.25)
-            ser.flushInput()
-        except serial.SerialException, e:
-            d = inputdialog.ExceptionDialog(e)
-            d.run()
-            d.destroy()
-            return
-
-        radio = settings.radio_class(ser)
-
-        fn = tempfile.mktemp()
-        if isinstance(radio, chirp_common.CloneModeRadio):
-            ct = clone.CloneThread(radio, "in", cb=self.cb_clonein,
-                                   parent=self)
-            ct.start()
-        else:
-            self.do_open_live(radio)
-
-    def do_upload(self, port=None, rtype=None):
-        eset = self.get_current_editorset()
-        radio = eset.radio
-
-        settings = clone.CloneSettings()
-        settings.radio_class = radio.__class__
-
-        d = clone.CloneSettingsDialog(settings, parent=self)
-        settings = d.run()
-        d.destroy()
-        if not settings:
-            return
-        prompts = radio.get_prompts()
-
-        if prompts.display_pre_upload_prompt_before_opening_port is True:
-            LOG.debug("Opening port after pre_upload prompt.")
-            self._show_instructions(radio, prompts.pre_upload)
-
-        if isinstance(radio, chirp_common.ExperimentalRadio) and \
-                not self._confirm_experimental(radio.__class__):
-            # User does not want to proceed with experimental driver
-            return
-
-        try:
-            ser = serial.Serial(port=settings.port,
-                                baudrate=radio.BAUD_RATE,
-                                rtscts=radio.HARDWARE_FLOW,
-                                timeout=0.25)
-            ser.flushInput()
-        except serial.SerialException, e:
-            d = inputdialog.ExceptionDialog(e)
-            d.run()
-            d.destroy()
-            return
-
-        if prompts.display_pre_upload_prompt_before_opening_port is False:
-            LOG.debug("Opening port before pre_upload prompt.")
-            self._show_instructions(radio, prompts.pre_upload)
-
-        radio.set_pipe(ser)
-
-        ct = clone.CloneThread(radio, "out", cb=self.cb_cloneout, parent=self)
-        ct.start()
-
-    def do_close(self, tab_child=None):
-        if tab_child:
-            eset = tab_child
-        else:
-            eset = self.get_current_editorset()
-
-        if not eset:
-            return False
-
-        if eset.is_modified():
-            dlg = miscwidgets.YesNoDialog(
-                title=_("Save Changes?"), parent=self,
-                buttons=(gtk.STOCK_YES, gtk.RESPONSE_YES,
-                         gtk.STOCK_NO, gtk.RESPONSE_NO,
-                         gtk.STOCK_CANCEL, gtk.RESPONSE_CANCEL))
-            dlg.set_text(_("File is modified, save changes before closing?"))
-            res = dlg.run()
-            dlg.destroy()
-
-            if res == gtk.RESPONSE_YES:
-                self.do_save(eset)
-            elif res != gtk.RESPONSE_NO:
-                raise ModifiedError()
-
-        eset.rthread.stop()
-        eset.rthread.join()
-
-        eset.prepare_close()
-
-        if eset.radio.pipe:
-            eset.radio.pipe.close()
-
-        if isinstance(eset.radio, chirp_common.LiveRadio):
-            action = self.menu_ag.get_action("openlive")
-            if action:
-                action.set_sensitive(True)
-
-        page = self.tabs.page_num(eset)
-        if page is not None:
-            self.tabs.remove_page(page)
-
-        return True
-
-    def do_import(self):
-        types = [(_("All files") + " (*.*)", "*"),
-                 (_("CHIRP Files") + " (*.chirp)", "*.chirp"),
-                 (_("CHIRP Radio Images") + " (*.img)", "*.img"),
-                 (_("CSV Files") + " (*.csv)", "*.csv"),
-                 (_("DAT Files") + " (*.dat)", "*.dat"),
-                 (_("EVE Files (VX5)") + " (*.eve)", "*.eve"),
-                 (_("ICF Files") + " (*.icf)", "*.icf"),
-                 (_("Kenwood HMK Files") + " (*.hmk)", "*.hmk"),
-                 (_("Kenwood ITM Files") + " (*.itm)", "*.itm"),
-                 (_("Travel Plus Files") + " (*.tpe)", "*.tpe"),
-                 (_("VX5 Commander Files") + " (*.vx5)", "*.vx5"),
-                 (_("VX6 Commander Files") + " (*.vx6)", "*.vx6"),
-                 (_("VX7 Commander Files") + " (*.vx7)", "*.vx7")]
-        filen = platform.get_platform().gui_open_file(types=types)
-        if not filen:
-            return
-
-        eset = self.get_current_editorset()
-        count = eset.do_import(filen)
-        reporting.report_model_usage(eset.rthread.radio, "import", count > 0)
-
-    def do_dmrmarc_prompt(self):
-        fields = {"1City":      (gtk.Entry(), lambda x: x),
-                  "2State":     (gtk.Entry(), lambda x: x),
-                  "3Country":   (gtk.Entry(), lambda x: x),
-                  }
-
-        d = inputdialog.FieldDialog(title=_("DMR-MARC Repeater Database Dump"),
-                                    parent=self)
-        for k in sorted(fields.keys()):
-            d.add_field(k[1:], fields[k][0])
-            fields[k][0].set_text(CONF.get(k[1:], "dmrmarc") or "")
-
-        while d.run() == gtk.RESPONSE_OK:
-            for k in sorted(fields.keys()):
-                widget, validator = fields[k]
-                try:
-                    if validator(widget.get_text()):
-                        CONF.set(k[1:], widget.get_text(), "dmrmarc")
-                        continue
-                except Exception:
-                    pass
-
-            d.destroy()
-            return True
-
-        d.destroy()
-        return False
-
-    def do_dmrmarc(self, do_import):
-        self.window.set_cursor(gtk.gdk.Cursor(gtk.gdk.WATCH))
-        if not self.do_dmrmarc_prompt():
-            self.window.set_cursor(None)
-            return
-
-        city = CONF.get("city", "dmrmarc")
-        state = CONF.get("state", "dmrmarc")
-        country = CONF.get("country", "dmrmarc")
-
-        # Do this in case the import process is going to take a while
-        # to make sure we process events leading up to this
-        gtk.gdk.window_process_all_updates()
-        while gtk.events_pending():
-            gtk.main_iteration(False)
-
-        if do_import:
-            eset = self.get_current_editorset()
-            dmrmarcstr = "dmrmarc://%s/%s/%s" % (city, state, country)
-            eset.do_import(dmrmarcstr)
-        else:
-            try:
-                from chirp import dmrmarc
-                radio = dmrmarc.DMRMARCRadio(None)
-                radio.set_params(city, state, country)
-                self.do_open_live(radio, read_only=True)
-            except errors.RadioError, e:
-                common.show_error(e)
-
-        self.window.set_cursor(None)
-
-    def do_repeaterbook_political_prompt(self):
-        if not CONF.get_bool("has_seen_credit", "repeaterbook"):
-            d = gtk.MessageDialog(parent=self, buttons=gtk.BUTTONS_OK)
-            d.set_markup("<big><big><b>RepeaterBook</b></big>\r\n" +
-                         "<i>North American Repeater Directory</i></big>")
-            d.format_secondary_markup("For more information about this " +
-                                      "free service, please go to\r\n" +
-                                      "http://www.repeaterbook.com")
-            d.run()
-            d.destroy()
-            CONF.set_bool("has_seen_credit", True, "repeaterbook")
-
-        default_state = "Oregon"
-        default_county = "--All--"
-        default_band = "--All--"
-        try:
-            try:
-                code = int(CONF.get("state", "repeaterbook"))
-            except:
-                code = CONF.get("state", "repeaterbook")
-            for k, v in fips.FIPS_STATES.items():
-                if code == v:
-                    default_state = k
-                    break
-
-            code = CONF.get("county", "repeaterbook")
-            items = fips.FIPS_COUNTIES[fips.FIPS_STATES[default_state]].items()
-            for k, v in items:
-                if code == v:
-                    default_county = k
-                    break
-
-            code = int(CONF.get("band", "repeaterbook"))
-            for k, v in RB_BANDS.items():
-                if code == v:
-                    default_band = k
-                    break
-        except:
-            pass
-
-        state = miscwidgets.make_choice(sorted(fips.FIPS_STATES.keys()),
-                                        False, default_state)
-        county = miscwidgets.make_choice(
-            sorted(fips.FIPS_COUNTIES[fips.FIPS_STATES[default_state]].keys()),
-            False, default_county)
-        band = miscwidgets.make_choice(sorted(RB_BANDS.keys(), key=key_bands),
-                                       False, default_band)
-
-        def _changed(box, county):
-            state = fips.FIPS_STATES[box.get_active_text()]
-            county.get_model().clear()
-            for fips_county in sorted(fips.FIPS_COUNTIES[state].keys()):
-                county.append_text(fips_county)
-            county.set_active(0)
-
-        state.connect("changed", _changed, county)
-
-        d = inputdialog.FieldDialog(title=_("RepeaterBook Query"), parent=self)
-        d.add_field("State", state)
-        d.add_field("County", county)
-        d.add_field("Band", band)
-
-        r = d.run()
-        d.destroy()
-        if r != gtk.RESPONSE_OK:
-            return False
-
-        code = fips.FIPS_STATES[state.get_active_text()]
-        county_id = fips.FIPS_COUNTIES[code][county.get_active_text()]
-        freq = RB_BANDS[band.get_active_text()]
-        CONF.set("state", str(code), "repeaterbook")
-        CONF.set("county", str(county_id), "repeaterbook")
-        CONF.set("band", str(freq), "repeaterbook")
-
-        return True
-
-    def do_repeaterbook_political(self, do_import):
-        self.window.set_cursor(gtk.gdk.Cursor(gtk.gdk.WATCH))
-        if not self.do_repeaterbook_political_prompt():
-            self.window.set_cursor(None)
-            return
-
-        try:
-            code = "%02i" % int(CONF.get("state", "repeaterbook"))
-        except:
-            try:
-                code = CONF.get("state", "repeaterbook")
-            except:
-                code = '41'  # Oregon default
-
-        try:
-            county = CONF.get("county", "repeaterbook")
-        except:
-            county = '%'  # --All-- default
-
-        try:
-            band = int(CONF.get("band", "repeaterbook"))
-        except:
-            band = 14  # 2m default
-
-        query = "http://www.repeaterbook.com/repeaters/downloads/chirp.php" + \
-            "?func=default&state_id=%s&band=%s&freq=%%&band6=%%&loc=%%" + \
-            "&county_id=%s&status_id=%%&features=%%&coverage=%%&use=%%"
-        query = query % (code,
-                         band and band or "%%",
-                         county and county or "%%")
-        print query
-
-        # Do this in case the import process is going to take a while
-        # to make sure we process events leading up to this
-        gtk.gdk.window_process_all_updates()
-        while gtk.events_pending():
-            gtk.main_iteration(False)
-
-        fn = tempfile.mktemp(".csv")
-        filename, headers = urllib.urlretrieve(query, fn)
-        if not os.path.exists(filename):
-            LOG.error("Failed, headers were: %s", headers)
-            common.show_error(_("RepeaterBook query failed"))
-            self.window.set_cursor(None)
-            return
-
-        try:
-            # Validate CSV
-            radio = repeaterbook.RBRadio(filename)
-            if radio.errors:
-                reporting.report_misc_error("repeaterbook",
-                                            ("query=%s\n" % query) +
-                                            ("\n") +
-                                            ("\n".join(radio.errors)))
-        except errors.InvalidDataError, e:
-            common.show_error(str(e))
-            self.window.set_cursor(None)
-            return
-        except Exception, e:
-            common.log_exception()
-
-        reporting.report_model_usage(radio, "import", True)
-
-        self.window.set_cursor(None)
-        if do_import:
-            eset = self.get_current_editorset()
-            count = eset.do_import(filename)
-        else:
-            self.do_open_live(radio, read_only=True)
-
-    def do_repeaterbook_proximity_prompt(self):
-        default_band = "--All--"
-        try:
-            code = int(CONF.get("band", "repeaterbook"))
-            for k, v in RB_BANDS.items():
-                if code == v:
-                    default_band = k
-                    break
-        except:
-            pass
-        fields = {"1Location":  (gtk.Entry(), lambda x: x.get_text()),
-                  "2Distance":  (gtk.Entry(), lambda x: x.get_text()),
-                  "3Band":      (miscwidgets.make_choice(
-                                sorted(RB_BANDS.keys(), key=key_bands),
-                                False, default_band),
-                                lambda x: RB_BANDS[x.get_active_text()]),
-                  }
-
-        d = inputdialog.FieldDialog(title=_("RepeaterBook Query"),
-                                    parent=self)
-        for k in sorted(fields.keys()):
-            d.add_field(k[1:], fields[k][0])
-            if isinstance(fields[k][0], gtk.Entry):
-                fields[k][0].set_text(
-                    CONF.get(k[1:].lower(), "repeaterbook") or "")
-
-        while d.run() == gtk.RESPONSE_OK:
-            valid = True
-            for k, (widget, fn) in fields.items():
-                try:
-                    CONF.set(k[1:].lower(), str(fn(widget)), "repeaterbook")
-                    continue
-                except:
-                    pass
-                common.show_error("Invalid value for %s" % k[1:])
-                valid = False
-                break
-
-            if valid:
-                d.destroy()
-                return True
-
-        d.destroy()
-        return False
-
-    def do_repeaterbook_proximity(self, do_import):
-        self.window.set_cursor(gtk.gdk.Cursor(gtk.gdk.WATCH))
-        if not self.do_repeaterbook_proximity_prompt():
-            self.window.set_cursor(None)
-            return
-
-        loc = CONF.get("location", "repeaterbook")
-
-        try:
-            dist = int(CONF.get("distance", "repeaterbook"))
-        except:
-            dist = 20
-
-        try:
-            band = int(CONF.get("band", "repeaterbook")) or '%'
-            band = str(band)
-        except:
-            band = '%'
-
-        query = "https://www.repeaterbook.com/repeaters/downloads/CHIRP/" \
-                "app_direct.php?loc=%s&band=%s&dist=%s" % (loc, band, dist)
-        print query
-
-        # Do this in case the import process is going to take a while
-        # to make sure we process events leading up to this
-        gtk.gdk.window_process_all_updates()
-        while gtk.events_pending():
-            gtk.main_iteration(False)
-
-        fn = tempfile.mktemp(".csv")
-        filename, headers = urllib.urlretrieve(query, fn)
-        if not os.path.exists(filename):
-            LOG.error("Failed, headers were: %s", headers)
-            common.show_error(_("RepeaterBook query failed"))
-            self.window.set_cursor(None)
-            return
-
-        try:
-            # Validate CSV
-            radio = repeaterbook.RBRadio(filename)
-            if radio.errors:
-                reporting.report_misc_error("repeaterbook",
-                                            ("query=%s\n" % query) +
-                                            ("\n") +
-                                            ("\n".join(radio.errors)))
-        except errors.InvalidDataError, e:
-            common.show_error(str(e))
-            self.window.set_cursor(None)
-            return
-        except Exception, e:
-            common.log_exception()
-
-        reporting.report_model_usage(radio, "import", True)
-
-        self.window.set_cursor(None)
-        if do_import:
-            eset = self.get_current_editorset()
-            count = eset.do_import(filename)
-        else:
-            self.do_open_live(radio, read_only=True)
-
-    def do_przemienniki_prompt(self):
-        d = inputdialog.FieldDialog(title='przemienniki.net query',
-                                    parent=self)
-        fields = {
-            "Country":
-                (miscwidgets.make_choice(
-                    ['at', 'bg', 'by', 'ch', 'cz', 'de', 'dk', 'es', 'fi',
-                        'fr', 'hu', 'it', 'lt', 'lv', 'no', 'pl', 'ro', 'se',
-                        'sk', 'ua', 'uk'], False),
-                 lambda x: str(x.get_active_text())),
-            "Band":
-                (miscwidgets.make_choice(['10m', '4m', '6m', '2m', '70cm',
-                                          '23cm', '13cm', '3cm'], False, '2m'),
-                 lambda x: str(x.get_active_text())),
-            "Mode":
-                (miscwidgets.make_choice(['fm', 'dv'], False),
-                 lambda x: str(x.get_active_text())),
-            "Only Working":
-                (miscwidgets.make_choice(['', 'yes'], False),
-                 lambda x: str(x.get_active_text())),
-            "Latitude": (gtk.Entry(), lambda x: float(x.get_text())),
-            "Longitude": (gtk.Entry(), lambda x: float(x.get_text())),
-            "Range": (gtk.Entry(), lambda x: int(x.get_text())),
-        }
-        for name in sorted(fields.keys()):
-            value, fn = fields[name]
-            d.add_field(name, value)
-        while d.run() == gtk.RESPONSE_OK:
-            query = "http://przemienniki.net/export/chirp.csv?"
-            args = []
-            for name, (value, fn) in fields.items():
-                if isinstance(value, gtk.Entry):
-                    contents = value.get_text()
-                else:
-                    contents = value.get_active_text()
-                if contents:
-                    try:
-                        _value = fn(value)
-                    except ValueError:
-                        common.show_error(_("Invalid value for %s") % name)
-                        query = None
-                        continue
-
-                    args.append("=".join((name.replace(" ", "").lower(),
-                                          contents)))
-            query += "&".join(args)
-            LOG.debug(query)
-            d.destroy()
-            return query
-
-        d.destroy()
-        return query
-
-    def do_przemienniki(self, do_import):
-        url = self.do_przemienniki_prompt()
-        if not url:
-            return
-
-        fn = tempfile.mktemp(".csv")
-        filename, headers = urllib.urlretrieve(url, fn)
-        if not os.path.exists(filename):
-            LOG.error("Failed, headers were: %s", str(headers))
-            common.show_error(_("Query failed"))
-            return
-
-        class PRRadio(generic_csv.CSVRadio,
-                      chirp_common.NetworkSourceRadio):
-            VENDOR = "przemienniki.net"
-            MODEL = ""
-
-        try:
-            radio = PRRadio(filename)
-        except Exception, e:
-            common.show_error(str(e))
-            return
-
-        if do_import:
-            eset = self.get_current_editorset()
-            count = eset.do_import(filename)
-        else:
-            self.do_open_live(radio, read_only=True)
-
-    def do_rfinder_prompt(self):
-        fields = {"1Email": (gtk.Entry(), lambda x: "@" in x),
-                  "2Password": (gtk.Entry(), lambda x: x),
-                  "3Latitude": (gtk.Entry(),
-                                lambda x: float(x) < 90 and float(x) > -90),
-                  "4Longitude": (gtk.Entry(),
-                                 lambda x: float(x) < 180 and float(x) > -180),
-                  "5Range_in_Miles": (gtk.Entry(),
-                                      lambda x: int(x) > 0 and int(x) < 5000),
-                  }
-
-        d = inputdialog.FieldDialog(title="RFinder Login", parent=self)
-        for k in sorted(fields.keys()):
-            d.add_field(k[1:].replace("_", " "), fields[k][0])
-            fields[k][0].set_text(CONF.get(k[1:], "rfinder") or "")
-            fields[k][0].set_visibility(k != "2Password")
-
-        while d.run() == gtk.RESPONSE_OK:
-            valid = True
-            for k in sorted(fields.keys()):
-                widget, validator = fields[k]
-                try:
-                    if validator(widget.get_text()):
-                        CONF.set(k[1:], widget.get_text(), "rfinder")
-                        continue
-                except Exception:
-                    pass
-                common.show_error("Invalid value for %s" % k[1:])
-                valid = False
-                break
-
-            if valid:
-                d.destroy()
-                return True
-
-        d.destroy()
-        return False
-
-    def do_rfinder(self, do_import):
-        self.window.set_cursor(gtk.gdk.Cursor(gtk.gdk.WATCH))
-        if not self.do_rfinder_prompt():
-            self.window.set_cursor(None)
-            return
-
-        lat = CONF.get_float("Latitude", "rfinder")
-        lon = CONF.get_float("Longitude", "rfinder")
-        passwd = CONF.get("Password", "rfinder")
-        email = CONF.get("Email", "rfinder")
-        miles = CONF.get_int("Range_in_Miles", "rfinder")
-
-        # Do this in case the import process is going to take a while
-        # to make sure we process events leading up to this
-        gtk.gdk.window_process_all_updates()
-        while gtk.events_pending():
-            gtk.main_iteration(False)
-
-        if do_import:
-            eset = self.get_current_editorset()
-            rfstr = "rfinder://%s/%s/%f/%f/%i" % \
-                    (email, passwd, lat, lon, miles)
-            count = eset.do_import(rfstr)
-        else:
-            from chirp.drivers import rfinder
-            radio = rfinder.RFinderRadio(None)
-            radio.set_params((lat, lon), miles, email, passwd)
-            self.do_open_live(radio, read_only=True)
-
-        self.window.set_cursor(None)
-
-    def do_radioreference_prompt(self):
-        fields = {"1Username":  (gtk.Entry(), lambda x: x),
-                  "2Password":  (gtk.Entry(), lambda x: x),
-                  "3Zipcode":   (gtk.Entry(), lambda x: x),
-                  }
-
-        d = inputdialog.FieldDialog(title=_("RadioReference.com Query"),
-                                    parent=self)
-        for k in sorted(fields.keys()):
-            d.add_field(k[1:], fields[k][0])
-            fields[k][0].set_text(CONF.get(k[1:], "radioreference") or "")
-            fields[k][0].set_visibility(k != "2Password")
-
-        while d.run() == gtk.RESPONSE_OK:
-            valid = True
-            for k in sorted(fields.keys()):
-                widget, validator = fields[k]
-                try:
-                    if validator(widget.get_text()):
-                        CONF.set(k[1:], widget.get_text(), "radioreference")
-                        continue
-                except Exception:
-                    pass
-                common.show_error("Invalid value for %s" % k[1:])
-                valid = False
-                break
-
-            if valid:
-                d.destroy()
-                return True
-
-        d.destroy()
-        return False
-
-    def do_radioreference(self, do_import):
-        self.window.set_cursor(gtk.gdk.Cursor(gtk.gdk.WATCH))
-        if not self.do_radioreference_prompt():
-            self.window.set_cursor(None)
-            return
-
-        username = CONF.get("Username", "radioreference")
-        passwd = CONF.get("Password", "radioreference")
-        zipcode = CONF.get("Zipcode", "radioreference")
-
-        # Do this in case the import process is going to take a while
-        # to make sure we process events leading up to this
-        gtk.gdk.window_process_all_updates()
-        while gtk.events_pending():
-            gtk.main_iteration(False)
-
-        if do_import:
-            eset = self.get_current_editorset()
-            rrstr = "radioreference://%s/%s/%s" % (zipcode, username, passwd)
-            count = eset.do_import(rrstr)
-        else:
-            try:
-                from chirp import radioreference
-                radio = radioreference.RadioReferenceRadio(None)
-                radio.set_params(zipcode, username, passwd)
-                self.do_open_live(radio, read_only=True)
-            except errors.RadioError, e:
-                common.show_error(e)
-
-        self.window.set_cursor(None)
-
-    def do_export(self):
-        types = [(_("CSV Files") + " (*.csv)", "csv"),
-                 ]
-
-        eset = self.get_current_editorset()
-
-        if os.path.exists(eset.filename):
-            base = os.path.basename(eset.filename)
-            if "." in base:
-                base = base[:base.rindex(".")]
-            defname = base
-        else:
-            defname = "radio"
-
-        filen = platform.get_platform().gui_save_file(default_name=defname,
-                                                      types=types)
-        if not filen:
-            return
-
-        if os.path.exists(filen):
-            dlg = inputdialog.OverwriteDialog(filen)
-            owrite = dlg.run()
-            dlg.destroy()
-            if owrite != gtk.RESPONSE_OK:
-                return
-            os.remove(filen)
-
-        count = eset.do_export(filen)
-        reporting.report_model_usage(eset.rthread.radio, "export", count > 0)
-
-    def do_about(self):
-        d = gtk.AboutDialog()
-        d.set_transient_for(self)
-        import sys
-        verinfo = "GTK %s\nPyGTK %s\nPython %s\n" % (
-            ".".join([str(x) for x in gtk.gtk_version]),
-            ".".join([str(x) for x in gtk.pygtk_version]),
-            sys.version.split()[0])
-
-        # Set url hook to handle user activating a URL link in the about dialog
-        gtk.about_dialog_set_url_hook(lambda dlg, url: webbrowser.open(url))
-
-        d.set_name("CHIRP")
-        d.set_version(CHIRP_VERSION)
-        d.set_copyright("Copyright 2015 Dan Smith (KK7DS)")
-        d.set_website("http://chirp.danplanet.com")
-        d.set_authors(("Dan Smith KK7DS <dsmith at danplanet.com>",
-                       _("With significant contributions from:"),
-                       "Tom KD7LXL",
-                       "Marco IZ3GME",
-                       "Jim KC9HI"
-                       ))
-        d.set_translator_credits("Polish: Grzegorz SQ2RBY" +
-                                 os.linesep +
-                                 "Italian: Fabio IZ2QDH" +
-                                 os.linesep +
-                                 "Dutch: Michael PD4MT" +
-                                 os.linesep +
-                                 "German: Benjamin HB9EUK" +
-                                 os.linesep +
-                                 "Hungarian: Attila HA5JA" +
-                                 os.linesep +
-                                 "Russian: Dmitry Slukin" +
-                                 os.linesep +
-                                 "Portuguese (BR): Crezivando PP7CJ")
-        d.set_comments(verinfo)
-
-        d.run()
-        d.destroy()
-
-    def do_gethelp(self):
-        webbrowser.open("http://chirp.danplanet.com")
-
-    def do_columns(self):
-        eset = self.get_current_editorset()
-        driver = directory.get_driver(eset.rthread.radio.__class__)
-        radio_name = "%s %s %s" % (eset.rthread.radio.VENDOR,
-                                   eset.rthread.radio.MODEL,
-                                   eset.rthread.radio.VARIANT)
-        d = gtk.Dialog(title=_("Select Columns"),
-                       parent=self,
-                       buttons=(gtk.STOCK_OK, gtk.RESPONSE_OK,
-                                gtk.STOCK_CANCEL, gtk.RESPONSE_CANCEL))
-
-        vbox = gtk.VBox()
-        vbox.show()
-        sw = gtk.ScrolledWindow()
-        sw.set_policy(gtk.POLICY_NEVER, gtk.POLICY_AUTOMATIC)
-        sw.add_with_viewport(vbox)
-        sw.show()
-        d.vbox.pack_start(sw, 1, 1, 1)
-        d.set_size_request(-1, 300)
-        d.set_resizable(False)
-
-        labelstr = _("Visible columns for {radio}").format(radio=radio_name)
-        label = gtk.Label(labelstr)
-        label.show()
-        vbox.pack_start(label)
-
-        fields = []
-        memedit = eset.get_current_editor()  # .editors["memedit"]
-        unsupported = memedit.get_unsupported_columns()
-        for colspec in memedit.cols:
-            if colspec[0].startswith("_"):
-                continue
-            elif colspec[0] in unsupported:
-                continue
-            label = colspec[0]
-            visible = memedit.get_column_visible(memedit.col(label))
-            widget = gtk.CheckButton(label)
-            widget.set_active(visible)
-            fields.append(widget)
-            vbox.pack_start(widget, 1, 1, 1)
-            widget.show()
-
-        res = d.run()
-        selected_columns = []
-        if res == gtk.RESPONSE_OK:
-            for widget in fields:
-                colnum = memedit.col(widget.get_label())
-                memedit.set_column_visible(colnum, widget.get_active())
-                if widget.get_active():
-                    selected_columns.append(widget.get_label())
-
-        d.destroy()
-
-        CONF.set(driver, ",".join(selected_columns), "memedit_columns")
-
-    def do_hide_unused(self, action):
-        eset = self.get_current_editorset()
-        if eset is None:
-            conf = config.get("memedit")
-            conf.set_bool("hide_unused", action.get_active())
-        else:
-            for editortype, editor in eset.editors.iteritems():
-                if "memedit" in editortype:
-                    editor.set_hide_unused(action.get_active())
-
-    def do_clearq(self):
-        eset = self.get_current_editorset()
-        eset.rthread.flush()
-
-    def do_copy(self, cut):
-        eset = self.get_current_editorset()
-        eset.get_current_editor().copy_selection(cut)
-
-    def do_paste(self):
-        eset = self.get_current_editorset()
-        eset.get_current_editor().paste_selection()
-
-    def do_delete(self):
-        eset = self.get_current_editorset()
-        eset.get_current_editor().copy_selection(True)
-
-    def do_toggle_report(self, action):
-        if not action.get_active():
-            d = gtk.MessageDialog(buttons=gtk.BUTTONS_YES_NO, parent=self)
-            markup = "<b><big>" + _("Reporting is disabled") + "</big></b>"
-            d.set_markup(markup)
-            msg = _("The reporting feature of CHIRP is designed to help "
-                    "<u>improve quality</u> by allowing the authors to focus "
-                    "on the radio drivers used most often and errors "
-                    "experienced by the users. The reports contain no "
-                    "identifying information and are used only for "
-                    "statistical purposes by the authors. Your privacy is "
-                    "extremely important, but <u>please consider leaving "
-                    "this feature enabled to help make CHIRP better!</u>\n\n"
-                    "<b>Are you sure you want to disable this feature?</b>")
-            d.format_secondary_markup(msg.replace("\n", "\r\n"))
-            r = d.run()
-            d.destroy()
-            if r == gtk.RESPONSE_NO:
-                action.set_active(not action.get_active())
-
-        conf = config.get()
-        conf.set_bool("no_report", not action.get_active())
-
-    def do_toggle_no_smart_tmode(self, action):
-        CONF.set_bool("no_smart_tmode", not action.get_active(), "memedit")
-
-    def do_toggle_developer(self, action):
-        conf = config.get()
-        conf.set_bool("developer", action.get_active(), "state")
-
-        for name in ["viewdeveloper", "loadmod"]:
-            devaction = self.menu_ag.get_action(name)
-            devaction.set_visible(action.get_active())
-
-    def do_toggle_clone_instructions(self, action):
-        CONF.set_bool("clone_instructions",
-                      not action.get_active(), "noconfirm")
-
-    def do_change_language(self):
-        langs = ["Auto", "English", "Polish", "Italian", "Dutch", "German",
-                 "Hungarian", "Russian", "Portuguese (BR)", "French",
-                 "Spanish"]
-        d = inputdialog.ChoiceDialog(langs, parent=self,
-                                     title="Choose Language")
-        d.label.set_text(_("Choose a language or Auto to use the "
-                           "operating system default. You will need to "
-                           "restart the application before the change "
-                           "will take effect"))
-        d.label.set_line_wrap(True)
-        r = d.run()
-        if r == gtk.RESPONSE_OK:
-            LOG.debug("Chose language %s" % d.choice.get_active_text())
-            conf = config.get()
-            conf.set("language", d.choice.get_active_text(), "state")
-        d.destroy()
-
-    def load_module(self):
-        types = [(_("Python Modules") + "*.py", "*.py")]
-        filen = platform.get_platform().gui_open_file(types=types)
-        if not filen:
-            return
-
-        # We're in development mode, so we need to tell the directory to
-        # allow a loaded module to override an existing driver, against
-        # its normal better judgement
-        directory.enable_reregistrations()
-
-        try:
-            module = file(filen)
-            code = module.read()
-            module.close()
-            pyc = compile(code, filen, 'exec')
-            # See this for why:
-            # http://stackoverflow.com/questions/2904274/globals-and-locals-in-python-exec
-            exec(pyc, globals(), globals())
-        except Exception, e:
-            common.log_exception()
-            common.show_error("Unable to load module: %s" % e)
-
-    def mh(self, _action, *args):
-        action = _action.get_name()
-
-        if action == "quit":
-            gtk.main_quit()
-        elif action == "new":
-            self.do_new()
-        elif action == "open":
-            self.do_open()
-        elif action == "save":
-            self.do_save()
-        elif action == "saveas":
-            self.do_saveas()
-        elif action.startswith("download"):
-            self.do_download(*args)
-        elif action.startswith("upload"):
-            self.do_upload(*args)
-        elif action == "close":
-            self.do_close()
-        elif action == "import":
-            self.do_import()
-        elif action in ["qdmrmarc", "idmrmarc"]:
-            self.do_dmrmarc(action[0] == "i")
-        elif action in ["qrfinder", "irfinder"]:
-            self.do_rfinder(action[0] == "i")
-        elif action in ["qradioreference", "iradioreference"]:
-            self.do_radioreference(action[0] == "i")
-        elif action == "export":
-            self.do_export()
-        elif action in ["qrbookpolitical", "irbookpolitical"]:
-            self.do_repeaterbook_political(action[0] == "i")
-        elif action in ["qrbookproximity", "irbookproximity"]:
-            self.do_repeaterbook_proximity(action[0] == "i")
-        elif action in ["qpr", "ipr"]:
-            self.do_przemienniki(action[0] == "i")
-        elif action == "about":
-            self.do_about()
-        elif action == "gethelp":
-            self.do_gethelp()
-        elif action == "columns":
-            self.do_columns()
-        elif action == "hide_unused":
-            self.do_hide_unused(_action)
-        elif action == "cancelq":
-            self.do_clearq()
-        elif action == "report":
-            self.do_toggle_report(_action)
-        elif action == "channel_defaults":
-            # The memedit thread also has an instance of bandplans.
-            bp = bandplans.BandPlans(CONF)
-            bp.select_bandplan(self)
-        elif action == "no_smart_tmode":
-            self.do_toggle_no_smart_tmode(_action)
-        elif action == "developer":
-            self.do_toggle_developer(_action)
-        elif action == "clone_instructions":
-            self.do_toggle_clone_instructions(_action)
-        elif action in ["cut", "copy", "paste", "delete",
-                        "move_up", "move_dn", "exchange", "all",
-                        "devshowraw", "devdiffraw", "properties"]:
-            self.get_current_editorset().get_current_editor().hotkey(_action)
-        elif action == "devdifftab":
-            self.do_diff_radio()
-        elif action == "language":
-            self.do_change_language()
-        elif action == "loadmod":
-            self.load_module()
-        else:
-            return
-
-        self.ev_tab_switched()
-
-    def make_menubar(self):
-        menu_xml = """
-<ui>
-  <menubar name="MenuBar">
-    <menu action="file">
-      <menuitem action="new"/>
-      <menuitem action="open"/>
-      <menu action="openstock" name="openstock"/>
-      <menu action="recent" name="recent"/>
-      <menuitem action="save"/>
-      <menuitem action="saveas"/>
-      <menuitem action="loadmod"/>
-      <separator/>
-      <menuitem action="import"/>
-      <menuitem action="export"/>
-      <separator/>
-      <menuitem action="close"/>
-      <menuitem action="quit"/>
-    </menu>
-    <menu action="edit">
-      <menuitem action="cut"/>
-      <menuitem action="copy"/>
-      <menuitem action="paste"/>
-      <menuitem action="delete"/>
-      <separator/>
-      <menuitem action="all"/>
-      <separator/>
-      <menuitem action="move_up"/>
-      <menuitem action="move_dn"/>
-      <menuitem action="exchange"/>
-      <separator/>
-      <menuitem action="properties"/>
-    </menu>
-    <menu action="view">
-      <menuitem action="columns"/>
-      <menuitem action="hide_unused"/>
-      <menuitem action="no_smart_tmode"/>
-      <menu action="viewdeveloper">
-        <menuitem action="devshowraw"/>
-        <menuitem action="devdiffraw"/>
-        <menuitem action="devdifftab"/>
-      </menu>
-      <menuitem action="language"/>
-    </menu>
-    <menu action="radio" name="radio">
-      <menuitem action="download"/>
-      <menuitem action="upload"/>
-      <menu action="importsrc" name="importsrc">
-        <menuitem action="idmrmarc"/>
-        <menuitem action="iradioreference"/>
-        <menu action="irbook" name="irbook">
-            <menuitem action="irbookpolitical"/>
-            <menuitem action="irbookproximity"/>
-        </menu>
-        <menuitem action="ipr"/>
-        <menuitem action="irfinder"/>
-      </menu>
-      <menu action="querysrc" name="querysrc">
-        <menuitem action="qdmrmarc"/>
-        <menuitem action="qradioreference"/>
-        <menu action="qrbook" name="qrbook">
-            <menuitem action="qrbookpolitical"/>
-            <menuitem action="qrbookproximity"/>
-        </menu>
-        <menuitem action="qpr"/>
-        <menuitem action="qrfinder"/>
-      </menu>
-      <menu action="stock" name="stock"/>
-      <separator/>
-      <menuitem action="channel_defaults"/>
-      <separator/>
-      <menuitem action="cancelq"/>
-    </menu>
-    <menu action="help">
-      <menuitem action="gethelp"/>
-      <separator/>
-      <menuitem action="report"/>
-      <menuitem action="clone_instructions"/>
-      <menuitem action="developer"/>
-      <separator/>
-      <menuitem action="about"/>
-    </menu>
-  </menubar>
-</ui>
-"""
-        ALT_KEY = "<Alt>"
-        CTRL_KEY = "<Ctrl>"
-        if sys.platform == 'darwin':
-            ALT_KEY = "<Meta>"
-            CTRL_KEY = "<Meta>"
-        actions = [
-            ('file', None, _("_File"), None, None, self.mh),
-            ('new', gtk.STOCK_NEW, None, None, None, self.mh),
-            ('open', gtk.STOCK_OPEN, None, None, None, self.mh),
-            ('openstock', None, _("Open stock config"), None, None, self.mh),
-            ('recent', None, _("_Recent"), None, None, self.mh),
-            ('save', gtk.STOCK_SAVE, None, None, None, self.mh),
-            ('saveas', gtk.STOCK_SAVE_AS, None, None, None, self.mh),
-            ('loadmod', None, _("Load Module"), None, None, self.mh),
-            ('close', gtk.STOCK_CLOSE, None, None, None, self.mh),
-            ('quit', gtk.STOCK_QUIT, None, None, None, self.mh),
-            ('edit', None, _("_Edit"), None, None, self.mh),
-            ('cut', None, _("_Cut"), "%sx" % CTRL_KEY, None, self.mh),
-            ('copy', None, _("_Copy"), "%sc" % CTRL_KEY, None, self.mh),
-            ('paste', None, _("_Paste"),
-             "%sv" % CTRL_KEY, None, self.mh),
-            ('delete', None, _("_Delete"), "Delete", None, self.mh),
-            ('all', None, _("Select _All"), None, None, self.mh),
-            ('move_up', None, _("Move _Up"),
-             "%sUp" % CTRL_KEY, None, self.mh),
-            ('move_dn', None, _("Move Dow_n"),
-             "%sDown" % CTRL_KEY, None, self.mh),
-            ('exchange', None, _("E_xchange"),
-             "%s<Shift>x" % CTRL_KEY, None, self.mh),
-            ('properties', None, _("P_roperties"), None, None, self.mh),
-            ('view', None, _("_View"), None, None, self.mh),
-            ('columns', None, _("Columns"), None, None, self.mh),
-            ('viewdeveloper', None, _("Developer"), None, None, self.mh),
-            ('devshowraw', None, _('Show raw memory'),
-             "%s<Shift>r" % CTRL_KEY, None, self.mh),
-            ('devdiffraw', None, _("Diff raw memories"),
-             "%s<Shift>d" % CTRL_KEY, None, self.mh),
-            ('devdifftab', None, _("Diff tabs"),
-             "%s<Shift>t" % CTRL_KEY, None, self.mh),
-            ('language', None, _("Change language"), None, None, self.mh),
-            ('radio', None, _("_Radio"), None, None, self.mh),
-            ('download', None, _("Download From Radio"),
-             "%sd" % ALT_KEY, None, self.mh),
-            ('upload', None, _("Upload To Radio"),
-             "%su" % ALT_KEY, None, self.mh),
-            ('import', None, _("Import"), "%si" % ALT_KEY, None, self.mh),
-            ('export', None, _("Export"), "%se" % ALT_KEY, None, self.mh),
-            ('importsrc', None, _("Import from data source"),
-             None, None, self.mh),
-            ('idmrmarc', None, _("DMR-MARC Repeaters"), None, None, self.mh),
-            ('iradioreference', None, _("RadioReference.com"),
-             None, None, self.mh),
-            ('irfinder', None, _("RFinder"), None, None, self.mh),
-            ('irbook', None, _("RepeaterBook"), None, None, self.mh),
-            ('irbookpolitical', None, _("RepeaterBook political query"), None,
-             None, self.mh),
-            ('irbookproximity', None, _("RepeaterBook proximity query"), None,
-             None, self.mh),
-            ('ipr', None, _("przemienniki.net"), None, None, self.mh),
-            ('querysrc', None, _("Query data source"), None, None, self.mh),
-            ('qdmrmarc', None, _("DMR-MARC Repeaters"), None, None, self.mh),
-            ('qradioreference', None, _("RadioReference.com"),
-             None, None, self.mh),
-            ('qrfinder', None, _("RFinder"), None, None, self.mh),
-            ('qpr', None, _("przemienniki.net"), None, None, self.mh),
-            ('qrbook', None, _("RepeaterBook"), None, None, self.mh),
-            ('qrbookpolitical', None, _("RepeaterBook political query"), None,
-             None, self.mh),
-            ('qrbookproximity', None, _("RepeaterBook proximity query"), None,
-             None, self.mh),
-            ('export_chirp', None, _("CHIRP Native File"),
-             None, None, self.mh),
-            ('export_csv', None, _("CSV File"), None, None, self.mh),
-            ('stock', None, _("Import from stock config"),
-             None, None, self.mh),
-            ('channel_defaults', None, _("Channel defaults"),
-             None, None, self.mh),
-            ('cancelq', gtk.STOCK_STOP, None, "Escape", None, self.mh),
-            ('help', None, _('Help'), None, None, self.mh),
-            ('about', gtk.STOCK_ABOUT, None, None, None, self.mh),
-            ('gethelp', None, _("Get Help Online..."), None, None, self.mh),
-        ]
-
-        conf = config.get()
-        re = not conf.get_bool("no_report")
-        hu = conf.get_bool("hide_unused", "memedit", default=True)
-        dv = conf.get_bool("developer", "state")
-        ci = not conf.get_bool("clone_instructions", "noconfirm")
-        st = not conf.get_bool("no_smart_tmode", "memedit")
-
-        toggles = [('report', None, _("Report Statistics"),
-                    None, None, self.mh, re),
-                   ('hide_unused', None, _("Hide Unused Fields"),
-                    None, None, self.mh, hu),
-                   ('no_smart_tmode', None, _("Smart Tone Modes"),
-                    None, None, self.mh, st),
-                   ('clone_instructions', None, _("Show Instructions"),
-                    None, None, self.mh, ci),
-                   ('developer', None, _("Enable Developer Functions"),
-                    None, None, self.mh, dv),
-                   ]
-
-        self.menu_uim = gtk.UIManager()
-        self.menu_ag = gtk.ActionGroup("MenuBar")
-        self.menu_ag.add_actions(actions)
-        self.menu_ag.add_toggle_actions(toggles)
-
-        self.menu_uim.insert_action_group(self.menu_ag, 0)
-        self.menu_uim.add_ui_from_string(menu_xml)
-
-        self.add_accel_group(self.menu_uim.get_accel_group())
-
-        self.clonemenu = self.menu_uim.get_widget(
-            "/MenuBar/help/clone_instructions")
-
-        # Initialize
-        self.do_toggle_developer(self.menu_ag.get_action("developer"))
-
-        return self.menu_uim.get_widget("/MenuBar")
-
-    def make_tabs(self):
-        self.tabs = gtk.Notebook()
-        self.tabs.set_scrollable(True)
-
-        return self.tabs
-
-    def close_out(self):
-        num = self.tabs.get_n_pages()
-        while num > 0:
-            num -= 1
-            LOG.debug("Closing %i" % num)
-            try:
-                self.do_close(self.tabs.get_nth_page(num))
-            except ModifiedError:
-                return False
-
-        gtk.main_quit()
-
-        return True
-
-    def make_status_bar(self):
-        box = gtk.HBox(False, 2)
-
-        self.sb_general = gtk.Statusbar()
-        self.sb_general.set_has_resize_grip(False)
-        self.sb_general.show()
-        box.pack_start(self.sb_general, 1, 1, 1)
-
-        self.sb_radio = gtk.Statusbar()
-        self.sb_radio.set_has_resize_grip(True)
-        self.sb_radio.show()
-        box.pack_start(self.sb_radio, 1, 1, 1)
-
-        box.show()
-        return box
-
-    def ev_delete(self, window, event):
-        if not self.close_out():
-            return True  # Don't exit
-
-    def ev_destroy(self, window):
-        if not self.close_out():
-            return True  # Don't exit
-
-    def setup_extra_hotkeys(self):
-        accelg = self.menu_uim.get_accel_group()
-
-        def memedit(a):
-            self.get_current_editorset().editors["memedit"].hotkey(a)
-
-        actions = [
-            # ("action_name", "key", function)
-        ]
-
-        for name, key, fn in actions:
-            a = gtk.Action(name, name, name, "")
-            a.connect("activate", fn)
-            self.menu_ag.add_action_with_accel(a, key)
-            a.set_accel_group(accelg)
-            a.connect_accelerator()
-
-    def _set_icon(self):
-        this_platform = platform.get_platform()
-        path = (this_platform.find_resource("chirp.png") or
-                this_platform.find_resource(os.path.join("pixmaps",
-                                                         "chirp.png")))
-        if os.path.exists(path):
-            self.set_icon_from_file(path)
-        else:
-            LOG.warn("Icon %s not found" % path)
-
-    def _updates(self, version):
-        if not version:
-            return
-
-        if version == CHIRP_VERSION:
-            return
-
-        LOG.info("Server reports version %s is available" % version)
-
-        # Report new updates every three days
-        intv = 3600 * 24 * 3
-
-        if CONF.is_defined("last_update_check", "state") and \
-           (time.time() - CONF.get_int("last_update_check", "state")) < intv:
-            return
-
-        CONF.set_int("last_update_check", int(time.time()), "state")
-        d = gtk.MessageDialog(buttons=gtk.BUTTONS_OK_CANCEL, parent=self,
-                              type=gtk.MESSAGE_INFO)
-        d.label.set_markup(
-            _('A new version of CHIRP is available: ' +
-              '{ver}. '.format(ver=version) +
-              'It is recommended that you upgrade as soon as possible. '
-              'Please go to: \r\n\r\n<a href="http://chirp.danplanet.com">' +
-              'http://chirp.danplanet.com</a>'))
-        response = d.run()
-        d.destroy()
-        if response == gtk.RESPONSE_OK:
-            webbrowser.open('http://chirp.danplanet.com/'
-                            'projects/chirp/wiki/Download')
-
-    def _init_macos(self, menu_bar):
-        macapp = None
-
-        # for KK7DS runtime <= R10
-        try:
-            import gtk_osxapplication
-            macapp = gtk_osxapplication.OSXApplication()
-        except ImportError:
-            pass
-
-        # for gtk-mac-integration >= 2.0.7
-        try:
-            import gtkosx_application
-            macapp = gtkosx_application.Application()
-        except ImportError:
-            pass
-
-        if macapp is None:
-            LOG.error("No MacOS support: %s" % e)
-            return
-
-        this_platform = platform.get_platform()
-        icon = (this_platform.find_resource("chirp.png") or
-                this_platform.find_resource(os.path.join("pixmaps",
-                                                         "chirp.png")))
-        if os.path.exists(icon):
-            icon_pixmap = gtk.gdk.pixbuf_new_from_file(icon)
-            macapp.set_dock_icon_pixbuf(icon_pixmap)
-
-        menu_bar.hide()
-        macapp.set_menu_bar(menu_bar)
-
-        quititem = self.menu_uim.get_widget("/MenuBar/file/quit")
-        quititem.hide()
-
-        aboutitem = self.menu_uim.get_widget("/MenuBar/help/about")
-        macapp.insert_app_menu_item(aboutitem, 0)
-
-        documentationitem = self.menu_uim.get_widget("/MenuBar/help/gethelp")
-        macapp.insert_app_menu_item(documentationitem, 0)
-
-        macapp.set_use_quartz_accelerators(False)
-        macapp.ready()
-
-        LOG.debug("Initialized MacOS support")
-
-    def __init__(self, *args, **kwargs):
-        gtk.Window.__init__(self, *args, **kwargs)
-
-        def expose(window, event):
-            allocation = window.get_allocation()
-            CONF.set_int("window_w", allocation.width, "state")
-            CONF.set_int("window_h", allocation.height, "state")
-        self.connect("expose_event", expose)
-
-        def state_change(window, event):
-            CONF.set_bool(
-                "window_maximized",
-                event.new_window_state == gtk.gdk.WINDOW_STATE_MAXIMIZED,
-                "state")
-        self.connect("window-state-event", state_change)
-
-        d = CONF.get("last_dir", "state")
-        if d and os.path.isdir(d):
-            platform.get_platform().set_last_dir(d)
-
-        vbox = gtk.VBox(False, 2)
-
-        self._recent = []
-
-        self.menu_ag = None
-        mbar = self.make_menubar()
-
-        if os.name != "nt":
-            self._set_icon()  # Windows gets the icon from the exe
-            if os.uname()[0] == "Darwin":
-                self._init_macos(mbar)
-
-        vbox.pack_start(mbar, 0, 0, 0)
-
-        self.tabs = None
-        tabs = self.make_tabs()
-        tabs.connect("switch-page", lambda n, _, p: self.ev_tab_switched(p))
-        tabs.connect("page-removed", lambda *a: self.ev_tab_switched())
-        tabs.show()
-        self.ev_tab_switched()
-        vbox.pack_start(tabs, 1, 1, 1)
-
-        vbox.pack_start(self.make_status_bar(), 0, 0, 0)
-
-        vbox.show()
-
-        self.add(vbox)
-
-        try:
-            width = CONF.get_int("window_w", "state")
-            height = CONF.get_int("window_h", "state")
-        except Exception:
-            width = 800
-            height = 600
-
-        self.set_default_size(width, height)
-        if CONF.get_bool("window_maximized", "state"):
-            self.maximize()
-        self.set_title("CHIRP")
-
-        self.connect("delete_event", self.ev_delete)
-        self.connect("destroy", self.ev_destroy)
-
-        if not CONF.get_bool("warned_about_reporting") and \
-                not CONF.get_bool("no_report"):
-            d = gtk.MessageDialog(buttons=gtk.BUTTONS_OK, parent=self)
-            d.set_markup("<b><big>" +
-                         _("Error reporting is enabled") +
-                         "</big></b>")
-            d.format_secondary_markup(
-                _("If you wish to disable this feature you may do so in "
-                  "the <u>Help</u> menu"))
-            d.run()
-            d.destroy()
-        CONF.set_bool("warned_about_reporting", True)
-
-        self.update_recent_files()
-        try:
-            self.update_stock_configs()
-        except UnicodeDecodeError:
-            LOG.exception('We hit bug #272 while working with unicode paths. '
-                          'Not copying stock configs so we can continue '
-                          'startup.')
-        self.setup_extra_hotkeys()
-
-        def updates_callback(ver):
-            gobject.idle_add(self._updates, ver)
-
-        if not CONF.get_bool("skip_update_check", "state"):
-            reporting.check_for_updates(updates_callback)
+# Copyright 2008 Dan Smith <dsmith at danplanet.com>
+# Copyright 2012 Tom Hayward <tom at tomh.us>
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program.  If not, see <http://www.gnu.org/licenses/>.
+
+from datetime import datetime
+import os
+import tempfile
+import urllib
+import webbrowser
+from glob import glob
+import shutil
+import time
+import logging
+import gtk
+import gobject
+import sys
+
+from chirp.ui import inputdialog, common
+from chirp import platform, directory, util
+from chirp.drivers import generic_xml, generic_csv, repeaterbook
+from chirp.drivers import ic9x, kenwood_live, idrp, vx7, vx5, vx6
+from chirp.drivers import icf, ic9x_icf
+from chirp import CHIRP_VERSION, chirp_common, detect, errors
+from chirp.ui import editorset, clone, miscwidgets, config, reporting, fips
+from chirp.ui import bandplans
+
+gobject.threads_init()
+
+LOG = logging.getLogger(__name__)
+
+if __name__ == "__main__":
+    sys.path.insert(0, "..")
+
+try:
+    import serial
+except ImportError, e:
+    common.log_exception()
+    common.show_error("\nThe Pyserial module is not installed!")
+
+
+CONF = config.get()
+
+KEEP_RECENT = 8
+
+RB_BANDS = {
+    "--All--":                  0,
+    "10 meters (29MHz)":        29,
+    "6 meters (54MHz)":         5,
+    "2 meters (144MHz)":        14,
+    "1.25 meters (220MHz)":     22,
+    "70 centimeters (440MHz)":  4,
+    "33 centimeters (900MHz)":  9,
+    "23 centimeters (1.2GHz)":  12,
+}
+
+
+def key_bands(band):
+    if band.startswith("-"):
+        return -1
+
+    amount, units, mhz = band.split(" ")
+    scale = units == "meters" and 100 or 1
+
+    return 100000 - (float(amount) * scale)
+
+
+class ModifiedError(Exception):
+    pass
+
+
+class ChirpMain(gtk.Window):
+
+    def get_current_editorset(self):
+        page = self.tabs.get_current_page()
+        if page is not None:
+            return self.tabs.get_nth_page(page)
+        else:
+            return None
+
+    def ev_tab_switched(self, pagenum=None):
+        def set_action_sensitive(action, sensitive):
+            self.menu_ag.get_action(action).set_sensitive(sensitive)
+
+        if pagenum is not None:
+            eset = self.tabs.get_nth_page(pagenum)
+        else:
+            eset = self.get_current_editorset()
+
+        upload_sens = bool(eset and
+                           isinstance(eset.radio, chirp_common.CloneModeRadio))
+
+        if not eset or isinstance(eset.radio, chirp_common.LiveRadio):
+            save_sens = False
+        elif isinstance(eset.radio, chirp_common.NetworkSourceRadio):
+            save_sens = False
+        else:
+            save_sens = True
+
+        for i in ["import", "importsrc", "stock"]:
+            set_action_sensitive(i,
+                                 eset is not None and not eset.get_read_only())
+
+        for i in ["save", "saveas"]:
+            set_action_sensitive(i, save_sens)
+
+        for i in ["upload"]:
+            set_action_sensitive(i, upload_sens)
+
+        for i in ["cancelq"]:
+            set_action_sensitive(i, eset is not None and not save_sens)
+
+        for i in ["export", "close", "columns", "irbook", "irfinder",
+                  "move_up", "move_dn", "exchange", "iradioreference",
+                  "cut", "copy", "paste", "delete", "viewdeveloper",
+                  "all", "properties"]:
+            set_action_sensitive(i, eset is not None)
+
+    def ev_status(self, editorset, msg):
+        self.sb_radio.pop(0)
+        self.sb_radio.push(0, msg)
+
+    def ev_usermsg(self, editorset, msg):
+        self.sb_general.pop(0)
+        self.sb_general.push(0, msg)
+
+    def ev_editor_selected(self, editorset, editortype):
+        mappings = {
+            "memedit": ["view", "edit"],
+        }
+
+        for _editortype, actions in mappings.items():
+            for _action in actions:
+                action = self.menu_ag.get_action(_action)
+                action.set_sensitive(editortype.startswith(_editortype))
+
+    def _connect_editorset(self, eset):
+        eset.connect("want-close", self.do_close)
+        eset.connect("status", self.ev_status)
+        eset.connect("usermsg", self.ev_usermsg)
+        eset.connect("editor-selected", self.ev_editor_selected)
+
+    def do_diff_radio(self):
+        if self.tabs.get_n_pages() < 2:
+            common.show_error("Diff tabs requires at least two open tabs!")
+            return
+
+        esets = []
+        for i in range(0, self.tabs.get_n_pages()):
+            esets.append(self.tabs.get_nth_page(i))
+
+        d = gtk.Dialog(title="Diff Radios",
+                       buttons=(gtk.STOCK_OK, gtk.RESPONSE_OK,
+                                gtk.STOCK_CANCEL, gtk.RESPONSE_CANCEL),
+                       parent=self)
+
+        label = gtk.Label("")
+        label.set_markup("<b>-1</b> for either Mem # does a full-file hex " +
+                         "dump with diffs highlighted.\n" +
+                         "<b>-2</b> for first Mem # shows " +
+                         "<b>only</b> the diffs.")
+        d.vbox.pack_start(label, True, True, 0)
+        label.show()
+
+        choices = []
+        for eset in esets:
+            choices.append("%s %s (%s)" % (eset.rthread.radio.VENDOR,
+                                           eset.rthread.radio.MODEL,
+                                           eset.filename))
+        choice_a = miscwidgets.make_choice(choices, False, choices[0])
+        choice_a.show()
+        chan_a = gtk.SpinButton()
+        chan_a.get_adjustment().set_all(1, -2, 999, 1, 10, 0)
+        chan_a.show()
+        hbox = gtk.HBox(False, 3)
+        hbox.pack_start(choice_a, 1, 1, 1)
+        hbox.pack_start(chan_a, 0, 0, 0)
+        hbox.show()
+        d.vbox.pack_start(hbox, 0, 0, 0)
+
+        choice_b = miscwidgets.make_choice(choices, False, choices[1])
+        choice_b.show()
+        chan_b = gtk.SpinButton()
+        chan_b.get_adjustment().set_all(1, -1, 999, 1, 10, 0)
+        chan_b.show()
+        hbox = gtk.HBox(False, 3)
+        hbox.pack_start(choice_b, 1, 1, 1)
+        hbox.pack_start(chan_b, 0, 0, 0)
+        hbox.show()
+        d.vbox.pack_start(hbox, 0, 0, 0)
+
+        r = d.run()
+        sel_a = choice_a.get_active_text()
+        sel_chan_a = chan_a.get_value()
+        sel_b = choice_b.get_active_text()
+        sel_chan_b = chan_b.get_value()
+        d.destroy()
+        if r == gtk.RESPONSE_CANCEL:
+            return
+
+        if sel_a == sel_b:
+            common.show_error("Can't diff the same tab!")
+            return
+
+        LOG.debug("Selected %s@%i and %s@%i" %
+                  (sel_a, sel_chan_a, sel_b, sel_chan_b))
+        name_a = os.path.basename(sel_a)
+        name_a = name_a[:name_a.rindex(")")]
+        name_b = os.path.basename(sel_b)
+        name_b = name_b[:name_b.rindex(")")]
+        diffwintitle = "%s@%i  diff  %s@%i" % (
+            name_a, sel_chan_a, name_b, sel_chan_b)
+
+        eset_a = esets[choices.index(sel_a)]
+        eset_b = esets[choices.index(sel_b)]
+
+        def _show_diff(mem_b, mem_a):
+            # Step 3: Show the diff
+            diff = common.simple_diff(mem_a, mem_b)
+            common.show_diff_blob(diffwintitle, diff)
+
+        def _get_mem_b(mem_a):
+            # Step 2: Get memory b
+            job = common.RadioJob(_show_diff, "get_raw_memory",
+                                  int(sel_chan_b))
+            job.set_cb_args(mem_a)
+            eset_b.rthread.submit(job)
+
+        if sel_chan_a >= 0 and sel_chan_b >= 0:
+            # Diff numbered memory
+            # Step 1: Get memory a
+            job = common.RadioJob(_get_mem_b, "get_raw_memory",
+                                  int(sel_chan_a))
+            eset_a.rthread.submit(job)
+        elif isinstance(eset_a.rthread.radio, chirp_common.CloneModeRadio) and\
+                isinstance(eset_b.rthread.radio, chirp_common.CloneModeRadio):
+            # Diff whole (can do this without a job, since both are clone-mode)
+            try:
+                addrfmt = CONF.get('hexdump_addrfmt', section='developer',
+                                   raw=True)
+            except:
+                pass
+            a = util.hexprint(eset_a.rthread.radio._mmap.get_packed(),
+                              addrfmt=addrfmt)
+            b = util.hexprint(eset_b.rthread.radio._mmap.get_packed(),
+                              addrfmt=addrfmt)
+            if sel_chan_a == -2:
+                diffsonly = True
+            else:
+                diffsonly = False
+            common.show_diff_blob(diffwintitle,
+                                  common.simple_diff(a, b, diffsonly))
+        else:
+            common.show_error("Cannot diff whole live-mode radios!")
+
+    def do_new(self):
+        eset = editorset.EditorSet(_("Untitled") + ".csv", self)
+        self._connect_editorset(eset)
+        eset.prime()
+        eset.show()
+
+        tab = self.tabs.append_page(eset, eset.get_tab_label())
+        self.tabs.set_current_page(tab)
+
+    def _do_manual_select(self, filename):
+        radiolist = {}
+        for drv, radio in directory.DRV_TO_RADIO.items():
+            if not issubclass(radio, chirp_common.CloneModeRadio):
+                continue
+            radiolist["%s %s" % (radio.VENDOR, radio.MODEL)] = drv
+
+        lab = gtk.Label("""<b><big>Unable to detect model!</big></b>
+
+If you think that it is valid, you can select a radio model below to
+force an open attempt. If selecting the model manually works, please
+file a bug on the website and attach your image. If selecting the model
+does not work, it is likely that you are trying to open some other type
+of file.
+""")
+
+        lab.set_justify(gtk.JUSTIFY_FILL)
+        lab.set_line_wrap(True)
+        lab.set_use_markup(True)
+        lab.show()
+        choice = miscwidgets.make_choice(sorted(radiolist.keys()), False,
+                                         sorted(radiolist.keys())[0])
+        d = gtk.Dialog(title="Detection Failed",
+                       buttons=(gtk.STOCK_OK, gtk.RESPONSE_OK,
+                                gtk.STOCK_CANCEL, gtk.RESPONSE_CANCEL))
+        d.vbox.pack_start(lab, 0, 0, 0)
+        d.vbox.pack_start(choice, 0, 0, 0)
+        d.vbox.set_spacing(5)
+        choice.show()
+        d.set_default_size(400, 200)
+        # d.set_resizable(False)
+        r = d.run()
+        d.destroy()
+        if r != gtk.RESPONSE_OK:
+            return
+        try:
+            rc = directory.DRV_TO_RADIO[radiolist[choice.get_active_text()]]
+            return rc(filename)
+        except:
+            return
+
+    def do_open(self, fname=None, tempname=None):
+        if not fname:
+            types = [(_("All files") + " (*.*)", "*"),
+                     (_("CHIRP Radio Images") + " (*.img)", "*.img"),
+                     (_("CHIRP Files") + " (*.chirp)", "*.chirp"),
+                     (_("CSV Files") + " (*.csv)", "*.csv"),
+                     (_("DAT Files") + " (*.dat)", "*.dat"),
+                     (_("EVE Files (VX5)") + " (*.eve)", "*.eve"),
+                     (_("ICF Files") + " (*.icf)", "*.icf"),
+                     (_("VX5 Commander Files") + " (*.vx5)", "*.vx5"),
+                     (_("VX6 Commander Files") + " (*.vx6)", "*.vx6"),
+                     (_("VX7 Commander Files") + " (*.vx7)", "*.vx7"),
+                     ]
+            fname = platform.get_platform().gui_open_file(types=types)
+            if not fname:
+                return
+
+        self.record_recent_file(fname)
+
+        if icf.is_icf_file(fname):
+            a = common.ask_yesno_question(
+                _("ICF files cannot be edited, only displayed or imported "
+                  "into another file. Open in read-only mode?"),
+                self)
+            if not a:
+                return
+            read_only = True
+        else:
+            read_only = False
+
+        if icf.is_9x_icf(fname):
+            # We have to actually instantiate the IC9xICFRadio to get its
+            # sub-devices
+            radio = ic9x_icf.IC9xICFRadio(fname)
+        else:
+            try:
+                radio = directory.get_radio_by_image(fname)
+            except errors.ImageDetectFailed:
+                radio = self._do_manual_select(fname)
+                if not radio:
+                    return
+                LOG.debug("Manually selected %s" % radio)
+            except Exception, e:
+                common.log_exception()
+                common.show_error(os.path.basename(fname) + ": " + str(e))
+                return
+
+        first_tab = False
+        try:
+            eset = editorset.EditorSet(radio, self,
+                                       filename=fname,
+                                       tempname=tempname)
+        except Exception, e:
+            common.log_exception()
+            common.show_error(
+                _("There was an error opening {fname}: {error}").format(
+                    fname=fname,
+                    error=e))
+            return
+
+        eset.set_read_only(read_only)
+        self._connect_editorset(eset)
+        eset.show()
+        self.tabs.append_page(eset, eset.get_tab_label())
+
+        if hasattr(eset.rthread.radio, "errors") and \
+                eset.rthread.radio.errors:
+            msg = _("{num} errors during open:").format(
+                num=len(eset.rthread.radio.errors))
+            common.show_error_text(msg,
+                                   "\r\n".join(eset.rthread.radio.errors))
+
+    def do_live_warning(self, radio):
+        d = gtk.MessageDialog(parent=self, buttons=gtk.BUTTONS_OK)
+        d.set_markup("<big><b>" + _("Note:") + "</b></big>")
+        msg = _("The {vendor} {model} operates in <b>live mode</b>. "
+                "This means that any changes you make are immediately sent "
+                "to the radio. Because of this, you cannot perform the "
+                "<u>Save</u> or <u>Upload</u> operations. If you wish to "
+                "edit the contents offline, please <u>Export</u> to a CSV "
+                "file, using the <b>File menu</b>.")
+        msg = msg.format(vendor=radio.VENDOR, model=radio.MODEL)
+        d.format_secondary_markup(msg)
+
+        again = gtk.CheckButton(_("Don't show this again"))
+        again.show()
+        d.vbox.pack_start(again, 0, 0, 0)
+        d.run()
+        CONF.set_bool("live_mode", again.get_active(), "noconfirm")
+        d.destroy()
+
+    def do_open_live(self, radio, tempname=None, read_only=False):
+        eset = editorset.EditorSet(radio, self, tempname=tempname)
+        eset.connect("want-close", self.do_close)
+        eset.connect("status", self.ev_status)
+        eset.set_read_only(read_only)
+        eset.show()
+        self.tabs.append_page(eset, eset.get_tab_label())
+
+        if isinstance(radio, chirp_common.LiveRadio):
+            reporting.report_model_usage(radio, "live", True)
+            if not CONF.get_bool("live_mode", "noconfirm"):
+                self.do_live_warning(radio)
+
+    def do_save(self, eset=None):
+        if not eset:
+            eset = self.get_current_editorset()
+
+        # For usability, allow Ctrl-S to short-circuit to Save-As if
+        # we are working on a yet-to-be-saved image
+        if not os.path.exists(eset.filename):
+            return self.do_saveas()
+
+        eset.save()
+
+    def do_saveas(self):
+        eset = self.get_current_editorset()
+
+        label = _("{vendor} {model} image file").format(
+            vendor=eset.radio.VENDOR,
+            model=eset.radio.MODEL)
+
+        defname_format = CONF.get("default_filename", "global") or \
+            "{vendor}_{model}_{date}"
+        defname = defname_format.format(
+            vendor=eset.radio.VENDOR,
+            model=eset.radio.MODEL,
+            date=datetime.now().strftime('%Y%m%d')
+        ).replace('/', '_')
+
+        types = [(label + " (*.%s)" % eset.radio.FILE_EXTENSION,
+                  eset.radio.FILE_EXTENSION)]
+
+        if isinstance(eset.radio, vx7.VX7Radio):
+            types += [(_("VX7 Commander") + " (*.vx7)", "vx7")]
+        elif isinstance(eset.radio, vx6.VX6Radio):
+            types += [(_("VX6 Commander") + " (*.vx6)", "vx6")]
+        elif isinstance(eset.radio, vx5.VX5Radio):
+            types += [(_("EVE") + " (*.eve)", "eve")]
+            types += [(_("VX5 Commander") + " (*.vx5)", "vx5")]
+
+        while True:
+            fname = platform.get_platform().gui_save_file(default_name=defname,
+                                                          types=types)
+            if not fname:
+                return
+
+            if os.path.exists(fname):
+                dlg = inputdialog.OverwriteDialog(fname)
+                owrite = dlg.run()
+                dlg.destroy()
+                if owrite == gtk.RESPONSE_OK:
+                    break
+            else:
+                break
+
+        try:
+            eset.save(fname)
+        except Exception, e:
+            d = inputdialog.ExceptionDialog(e)
+            d.run()
+            d.destroy()
+
+    def cb_clonein(self, radio, emsg=None):
+        radio.pipe.close()
+        reporting.report_model_usage(radio, "download", bool(emsg))
+        if not emsg:
+            self.do_open_live(radio, tempname="(" + _("Untitled") + ")")
+        else:
+            d = inputdialog.ExceptionDialog(emsg)
+            d.run()
+            d.destroy()
+
+    def cb_cloneout(self, radio, emsg=None):
+        radio.pipe.close()
+        reporting.report_model_usage(radio, "upload", True)
+        if emsg:
+            d = inputdialog.ExceptionDialog(emsg)
+            d.run()
+            d.destroy()
+
+    def _get_recent_list(self):
+        recent = []
+        for i in range(0, KEEP_RECENT):
+            fn = CONF.get("recent%i" % i, "state")
+            if fn:
+                recent.append(fn)
+        return recent
+
+    def _set_recent_list(self, recent):
+        for fn in recent:
+            CONF.set("recent%i" % recent.index(fn), fn, "state")
+
+    def update_recent_files(self):
+        i = 0
+        for fname in self._get_recent_list():
+            action_name = "recent%i" % i
+            path = "/MenuBar/file/recent"
+
+            old_action = self.menu_ag.get_action(action_name)
+            if old_action:
+                self.menu_ag.remove_action(old_action)
+
+            file_basename = os.path.basename(fname).replace("_", "__")
+            action = gtk.Action(
+                action_name, "_%i. %s" % (i + 1, file_basename),
+                _("Open recent file {name}").format(name=fname), "")
+            action.connect("activate", lambda a, f: self.do_open(f), fname)
+            mid = self.menu_uim.new_merge_id()
+            self.menu_uim.add_ui(mid, path,
+                                 action_name, action_name,
+                                 gtk.UI_MANAGER_MENUITEM, False)
+            self.menu_ag.add_action(action)
+            i += 1
+
+    def record_recent_file(self, filename):
+
+        recent_files = self._get_recent_list()
+        if filename not in recent_files:
+            if len(recent_files) == KEEP_RECENT:
+                del recent_files[-1]
+            recent_files.insert(0, filename)
+            self._set_recent_list(recent_files)
+
+        self.update_recent_files()
+
+    def import_stock_config(self, action, config):
+        eset = self.get_current_editorset()
+        count = eset.do_import(config)
+
+    def copy_shipped_stock_configs(self, stock_dir):
+        basepath = platform.get_platform().find_resource("stock_configs")
+
+        files = glob(os.path.join(basepath, "*.csv"))
+        for fn in files:
+            if os.path.exists(os.path.join(stock_dir, os.path.basename(fn))):
+                LOG.info("Skipping existing stock config")
+                continue
+            try:
+                shutil.copy(fn, stock_dir)
+                LOG.debug("Copying %s -> %s" % (fn, stock_dir))
+            except Exception, e:
+                LOG.error("Unable to copy %s to %s: %s" % (fn, stock_dir, e))
+                return False
+        return True
+
+    def update_stock_configs(self):
+        stock_dir = platform.get_platform().config_file("stock_configs")
+        if not os.path.isdir(stock_dir):
+            try:
+                os.mkdir(stock_dir)
+            except Exception, e:
+                LOG.error("Unable to create directory: %s" % stock_dir)
+                return
+        if not self.copy_shipped_stock_configs(stock_dir):
+            return
+
+        def _do_import_action(config):
+            name = os.path.splitext(os.path.basename(config))[0]
+            action_name = "stock-%i" % configs.index(config)
+            path = "/MenuBar/radio/stock"
+            action = gtk.Action(action_name,
+                                name,
+                                _("Import stock "
+                                  "configuration {name}").format(name=name),
+                                "")
+            action.connect("activate", self.import_stock_config, config)
+            mid = self.menu_uim.new_merge_id()
+            mid = self.menu_uim.add_ui(mid, path,
+                                       action_name, action_name,
+                                       gtk.UI_MANAGER_MENUITEM, False)
+            self.menu_ag.add_action(action)
+
+        def _do_open_action(config):
+            name = os.path.splitext(os.path.basename(config))[0]
+            action_name = "openstock-%i" % configs.index(config)
+            path = "/MenuBar/file/openstock"
+            action = gtk.Action(action_name,
+                                name,
+                                _("Open stock "
+                                  "configuration {name}").format(name=name),
+                                "")
+            action.connect("activate", lambda a, c: self.do_open(c), config)
+            mid = self.menu_uim.new_merge_id()
+            mid = self.menu_uim.add_ui(mid, path,
+                                       action_name, action_name,
+                                       gtk.UI_MANAGER_MENUITEM, False)
+            self.menu_ag.add_action(action)
+
+        configs = glob(os.path.join(stock_dir, "*.csv"))
+        for config in configs:
+            _do_import_action(config)
+            _do_open_action(config)
+
+    def _confirm_experimental(self, rclass):
+        sql_key = "warn_experimental_%s" % directory.radio_class_id(rclass)
+        if CONF.is_defined(sql_key, "state") and \
+                not CONF.get_bool(sql_key, "state"):
+            return True
+
+        title = _("Proceed with experimental driver?")
+        text = rclass.get_prompts().experimental
+        msg = _("This radio's driver is experimental. "
+                "Do you want to proceed?")
+        resp, squelch = common.show_warning(msg, text,
+                                            title=title,
+                                            buttons=gtk.BUTTONS_YES_NO,
+                                            can_squelch=True)
+        if resp == gtk.RESPONSE_YES:
+            CONF.set_bool(sql_key, not squelch, "state")
+        return resp == gtk.RESPONSE_YES
+
+    def _show_information(self, radio, message):
+        if message is None:
+            return
+
+        if CONF.get_bool("clone_information", "noconfirm"):
+            return
+
+        d = gtk.MessageDialog(parent=self, buttons=gtk.BUTTONS_OK)
+        d.set_markup("<big><b>" + _("{name} Information").format(
+                 name=radio.get_name()) + "</b></big>")
+        msg = _("{information}").format(information=message)
+        _again_msg = "Don't show information for any radio again"
+
+        d.format_secondary_markup(msg)
+
+        again = gtk.CheckButton(_(_again_msg))
+        again.show()
+        again.connect("toggled", lambda action:
+                      self.infomenu.set_active(not action.get_active()))
+        d.vbox.pack_start(again, 0, 0, 0)
+        h_button_box = d.vbox.get_children()[2]
+        try:
+            ok_button = h_button_box.get_children()[0]
+            ok_button.grab_default()
+            ok_button.grab_focus()
+        except AttributeError:
+            # don't grab focus on GTK+ 2.0
+            pass
+        d.run()
+        d.destroy()
+
+    def _show_instructions(self, radio, message):
+        if message is None:
+            return
+
+        if CONF.get_bool("clone_instructions", "noconfirm"):
+            return
+
+        d = gtk.MessageDialog(parent=self, buttons=gtk.BUTTONS_OK)
+        d.set_markup("<big><b>" + _("{name} Instructions").format(
+                 name=radio.get_name()) + "</b></big>")
+        msg = _("{instructions}").format(instructions=message)
+        _again_msg = "Don't show instructions for any radio again"
+
+        d.format_secondary_markup(msg)
+
+        again = gtk.CheckButton(_(_again_msg))
+        again.show()
+        again.connect("toggled", lambda action:
+                      self.clonemenu.set_active(not action.get_active()))
+        d.vbox.pack_start(again, 0, 0, 0)
+        h_button_box = d.vbox.get_children()[2]
+        try:
+            ok_button = h_button_box.get_children()[0]
+            ok_button.grab_default()
+            ok_button.grab_focus()
+        except AttributeError:
+            # don't grab focus on GTK+ 2.0
+            pass
+        d.run()
+        d.destroy()
+
+    def do_download(self, port=None, rtype=None):
+        d = clone.CloneSettingsDialog(parent=self)
+        settings = d.run()
+        d.destroy()
+        if not settings:
+            return
+
+        rclass = settings.radio_class
+        if issubclass(rclass, chirp_common.ExperimentalRadio) and \
+                not self._confirm_experimental(rclass):
+            # User does not want to proceed with experimental driver
+            return
+
+        if rclass.get_prompts().display_info is True:
+            self._show_information(rclass, rclass.get_prompts().info)
+
+        self._show_instructions(rclass, rclass.get_prompts().pre_download)
+
+        LOG.debug("User selected %s %s on port %s" %
+                  (rclass.VENDOR, rclass.MODEL, settings.port))
+
+        try:
+            ser = serial.Serial(port=settings.port,
+                                baudrate=rclass.BAUD_RATE,
+                                rtscts=rclass.HARDWARE_FLOW,
+                                timeout=0.25)
+            ser.flushInput()
+        except serial.SerialException, e:
+            d = inputdialog.ExceptionDialog(e)
+            d.run()
+            d.destroy()
+            return
+
+        radio = settings.radio_class(ser)
+
+        fn = tempfile.mktemp()
+        if isinstance(radio, chirp_common.CloneModeRadio):
+            ct = clone.CloneThread(radio, "in", cb=self.cb_clonein,
+                                   parent=self)
+            ct.start()
+        else:
+            self.do_open_live(radio)
+
+    def do_upload(self, port=None, rtype=None):
+        eset = self.get_current_editorset()
+        radio = eset.radio
+
+        settings = clone.CloneSettings()
+        settings.radio_class = radio.__class__
+
+        d = clone.CloneSettingsDialog(settings, parent=self)
+        settings = d.run()
+        d.destroy()
+        if not settings:
+            return
+        prompts = radio.get_prompts()
+
+        if prompts.display_pre_upload_prompt_before_opening_port is True:
+            LOG.debug("Opening port after pre_upload prompt.")
+            self._show_instructions(radio, prompts.pre_upload)
+
+        if isinstance(radio, chirp_common.ExperimentalRadio) and \
+                not self._confirm_experimental(radio.__class__):
+            # User does not want to proceed with experimental driver
+            return
+
+        try:
+            ser = serial.Serial(port=settings.port,
+                                baudrate=radio.BAUD_RATE,
+                                rtscts=radio.HARDWARE_FLOW,
+                                timeout=0.25)
+            ser.flushInput()
+        except serial.SerialException, e:
+            d = inputdialog.ExceptionDialog(e)
+            d.run()
+            d.destroy()
+            return
+
+        if prompts.display_pre_upload_prompt_before_opening_port is False:
+            LOG.debug("Opening port before pre_upload prompt.")
+            self._show_instructions(radio, prompts.pre_upload)
+
+        radio.set_pipe(ser)
+
+        ct = clone.CloneThread(radio, "out", cb=self.cb_cloneout, parent=self)
+        ct.start()
+
+    def do_close(self, tab_child=None):
+        if tab_child:
+            eset = tab_child
+        else:
+            eset = self.get_current_editorset()
+
+        if not eset:
+            return False
+
+        if eset.is_modified():
+            dlg = miscwidgets.YesNoDialog(
+                title=_("Save Changes?"), parent=self,
+                buttons=(gtk.STOCK_YES, gtk.RESPONSE_YES,
+                         gtk.STOCK_NO, gtk.RESPONSE_NO,
+                         gtk.STOCK_CANCEL, gtk.RESPONSE_CANCEL))
+            dlg.set_text(_("File is modified, save changes before closing?"))
+            res = dlg.run()
+            dlg.destroy()
+
+            if res == gtk.RESPONSE_YES:
+                self.do_save(eset)
+            elif res != gtk.RESPONSE_NO:
+                raise ModifiedError()
+
+        eset.rthread.stop()
+        eset.rthread.join()
+
+        eset.prepare_close()
+
+        if eset.radio.pipe:
+            eset.radio.pipe.close()
+
+        if isinstance(eset.radio, chirp_common.LiveRadio):
+            action = self.menu_ag.get_action("openlive")
+            if action:
+                action.set_sensitive(True)
+
+        page = self.tabs.page_num(eset)
+        if page is not None:
+            self.tabs.remove_page(page)
+
+        return True
+
+    def do_import(self):
+        types = [(_("All files") + " (*.*)", "*"),
+                 (_("CHIRP Files") + " (*.chirp)", "*.chirp"),
+                 (_("CHIRP Radio Images") + " (*.img)", "*.img"),
+                 (_("CSV Files") + " (*.csv)", "*.csv"),
+                 (_("DAT Files") + " (*.dat)", "*.dat"),
+                 (_("EVE Files (VX5)") + " (*.eve)", "*.eve"),
+                 (_("ICF Files") + " (*.icf)", "*.icf"),
+                 (_("Kenwood HMK Files") + " (*.hmk)", "*.hmk"),
+                 (_("Kenwood ITM Files") + " (*.itm)", "*.itm"),
+                 (_("Travel Plus Files") + " (*.tpe)", "*.tpe"),
+                 (_("VX5 Commander Files") + " (*.vx5)", "*.vx5"),
+                 (_("VX6 Commander Files") + " (*.vx6)", "*.vx6"),
+                 (_("VX7 Commander Files") + " (*.vx7)", "*.vx7")]
+        filen = platform.get_platform().gui_open_file(types=types)
+        if not filen:
+            return
+
+        eset = self.get_current_editorset()
+        count = eset.do_import(filen)
+        reporting.report_model_usage(eset.rthread.radio, "import", count > 0)
+
+    def do_dmrmarc_prompt(self):
+        fields = {"1City":      (gtk.Entry(), lambda x: x),
+                  "2State":     (gtk.Entry(), lambda x: x),
+                  "3Country":   (gtk.Entry(), lambda x: x),
+                  }
+
+        d = inputdialog.FieldDialog(title=_("DMR-MARC Repeater Database Dump"),
+                                    parent=self)
+        for k in sorted(fields.keys()):
+            d.add_field(k[1:], fields[k][0])
+            fields[k][0].set_text(CONF.get(k[1:], "dmrmarc") or "")
+
+        while d.run() == gtk.RESPONSE_OK:
+            for k in sorted(fields.keys()):
+                widget, validator = fields[k]
+                try:
+                    if validator(widget.get_text()):
+                        CONF.set(k[1:], widget.get_text(), "dmrmarc")
+                        continue
+                except Exception:
+                    pass
+
+            d.destroy()
+            return True
+
+        d.destroy()
+        return False
+
+    def do_dmrmarc(self, do_import):
+        self.window.set_cursor(gtk.gdk.Cursor(gtk.gdk.WATCH))
+        if not self.do_dmrmarc_prompt():
+            self.window.set_cursor(None)
+            return
+
+        city = CONF.get("city", "dmrmarc")
+        state = CONF.get("state", "dmrmarc")
+        country = CONF.get("country", "dmrmarc")
+
+        # Do this in case the import process is going to take a while
+        # to make sure we process events leading up to this
+        gtk.gdk.window_process_all_updates()
+        while gtk.events_pending():
+            gtk.main_iteration(False)
+
+        if do_import:
+            eset = self.get_current_editorset()
+            dmrmarcstr = "dmrmarc://%s/%s/%s" % (city, state, country)
+            eset.do_import(dmrmarcstr)
+        else:
+            try:
+                from chirp import dmrmarc
+                radio = dmrmarc.DMRMARCRadio(None)
+                radio.set_params(city, state, country)
+                self.do_open_live(radio, read_only=True)
+            except errors.RadioError, e:
+                common.show_error(e)
+
+        self.window.set_cursor(None)
+
+    def do_repeaterbook_political_prompt(self):
+        if not CONF.get_bool("has_seen_credit", "repeaterbook"):
+            d = gtk.MessageDialog(parent=self, buttons=gtk.BUTTONS_OK)
+            d.set_markup("<big><big><b>RepeaterBook</b></big>\r\n" +
+                         "<i>North American Repeater Directory</i></big>")
+            d.format_secondary_markup("For more information about this " +
+                                      "free service, please go to\r\n" +
+                                      "http://www.repeaterbook.com")
+            d.run()
+            d.destroy()
+            CONF.set_bool("has_seen_credit", True, "repeaterbook")
+
+        default_state = "Oregon"
+        default_county = "--All--"
+        default_band = "--All--"
+        try:
+            try:
+                code = int(CONF.get("state", "repeaterbook"))
+            except:
+                code = CONF.get("state", "repeaterbook")
+            for k, v in fips.FIPS_STATES.items():
+                if code == v:
+                    default_state = k
+                    break
+
+            code = CONF.get("county", "repeaterbook")
+            items = fips.FIPS_COUNTIES[fips.FIPS_STATES[default_state]].items()
+            for k, v in items:
+                if code == v:
+                    default_county = k
+                    break
+
+            code = int(CONF.get("band", "repeaterbook"))
+            for k, v in RB_BANDS.items():
+                if code == v:
+                    default_band = k
+                    break
+        except:
+            pass
+
+        state = miscwidgets.make_choice(sorted(fips.FIPS_STATES.keys()),
+                                        False, default_state)
+        county = miscwidgets.make_choice(
+            sorted(fips.FIPS_COUNTIES[fips.FIPS_STATES[default_state]].keys()),
+            False, default_county)
+        band = miscwidgets.make_choice(sorted(RB_BANDS.keys(), key=key_bands),
+                                       False, default_band)
+
+        def _changed(box, county):
+            state = fips.FIPS_STATES[box.get_active_text()]
+            county.get_model().clear()
+            for fips_county in sorted(fips.FIPS_COUNTIES[state].keys()):
+                county.append_text(fips_county)
+            county.set_active(0)
+
+        state.connect("changed", _changed, county)
+
+        d = inputdialog.FieldDialog(title=_("RepeaterBook Query"), parent=self)
+        d.add_field("State", state)
+        d.add_field("County", county)
+        d.add_field("Band", band)
+
+        r = d.run()
+        d.destroy()
+        if r != gtk.RESPONSE_OK:
+            return False
+
+        code = fips.FIPS_STATES[state.get_active_text()]
+        county_id = fips.FIPS_COUNTIES[code][county.get_active_text()]
+        freq = RB_BANDS[band.get_active_text()]
+        CONF.set("state", str(code), "repeaterbook")
+        CONF.set("county", str(county_id), "repeaterbook")
+        CONF.set("band", str(freq), "repeaterbook")
+
+        return True
+
+    def do_repeaterbook_political(self, do_import):
+        self.window.set_cursor(gtk.gdk.Cursor(gtk.gdk.WATCH))
+        if not self.do_repeaterbook_political_prompt():
+            self.window.set_cursor(None)
+            return
+
+        try:
+            code = "%02i" % int(CONF.get("state", "repeaterbook"))
+        except:
+            try:
+                code = CONF.get("state", "repeaterbook")
+            except:
+                code = '41'  # Oregon default
+
+        try:
+            county = CONF.get("county", "repeaterbook")
+        except:
+            county = '%'  # --All-- default
+
+        try:
+            band = int(CONF.get("band", "repeaterbook"))
+        except:
+            band = 14  # 2m default
+
+        query = "http://www.repeaterbook.com/repeaters/downloads/chirp.php" + \
+            "?func=default&state_id=%s&band=%s&freq=%%&band6=%%&loc=%%" + \
+            "&county_id=%s&status_id=%%&features=%%&coverage=%%&use=%%"
+        query = query % (code,
+                         band and band or "%%",
+                         county and county or "%%")
+        print query
+
+        # Do this in case the import process is going to take a while
+        # to make sure we process events leading up to this
+        gtk.gdk.window_process_all_updates()
+        while gtk.events_pending():
+            gtk.main_iteration(False)
+
+        fn = tempfile.mktemp(".csv")
+        filename, headers = urllib.urlretrieve(query, fn)
+        if not os.path.exists(filename):
+            LOG.error("Failed, headers were: %s", headers)
+            common.show_error(_("RepeaterBook query failed"))
+            self.window.set_cursor(None)
+            return
+
+        try:
+            # Validate CSV
+            radio = repeaterbook.RBRadio(filename)
+            if radio.errors:
+                reporting.report_misc_error("repeaterbook",
+                                            ("query=%s\n" % query) +
+                                            ("\n") +
+                                            ("\n".join(radio.errors)))
+        except errors.InvalidDataError, e:
+            common.show_error(str(e))
+            self.window.set_cursor(None)
+            return
+        except Exception, e:
+            common.log_exception()
+
+        reporting.report_model_usage(radio, "import", True)
+
+        self.window.set_cursor(None)
+        if do_import:
+            eset = self.get_current_editorset()
+            count = eset.do_import(filename)
+        else:
+            self.do_open_live(radio, read_only=True)
+
+    def do_repeaterbook_proximity_prompt(self):
+        default_band = "--All--"
+        try:
+            code = int(CONF.get("band", "repeaterbook"))
+            for k, v in RB_BANDS.items():
+                if code == v:
+                    default_band = k
+                    break
+        except:
+            pass
+        fields = {"1Location":  (gtk.Entry(), lambda x: x.get_text()),
+                  "2Distance":  (gtk.Entry(), lambda x: x.get_text()),
+                  "3Band":      (miscwidgets.make_choice(
+                                sorted(RB_BANDS.keys(), key=key_bands),
+                                False, default_band),
+                                lambda x: RB_BANDS[x.get_active_text()]),
+                  }
+
+        d = inputdialog.FieldDialog(title=_("RepeaterBook Query"),
+                                    parent=self)
+        for k in sorted(fields.keys()):
+            d.add_field(k[1:], fields[k][0])
+            if isinstance(fields[k][0], gtk.Entry):
+                fields[k][0].set_text(
+                    CONF.get(k[1:].lower(), "repeaterbook") or "")
+
+        while d.run() == gtk.RESPONSE_OK:
+            valid = True
+            for k, (widget, fn) in fields.items():
+                try:
+                    CONF.set(k[1:].lower(), str(fn(widget)), "repeaterbook")
+                    continue
+                except:
+                    pass
+                common.show_error("Invalid value for %s" % k[1:])
+                valid = False
+                break
+
+            if valid:
+                d.destroy()
+                return True
+
+        d.destroy()
+        return False
+
+    def do_repeaterbook_proximity(self, do_import):
+        self.window.set_cursor(gtk.gdk.Cursor(gtk.gdk.WATCH))
+        if not self.do_repeaterbook_proximity_prompt():
+            self.window.set_cursor(None)
+            return
+
+        loc = CONF.get("location", "repeaterbook")
+
+        try:
+            dist = int(CONF.get("distance", "repeaterbook"))
+        except:
+            dist = 20
+
+        try:
+            band = int(CONF.get("band", "repeaterbook")) or '%'
+            band = str(band)
+        except:
+            band = '%'
+
+        query = "https://www.repeaterbook.com/repeaters/downloads/CHIRP/" \
+                "app_direct.php?loc=%s&band=%s&dist=%s" % (loc, band, dist)
+        print query
+
+        # Do this in case the import process is going to take a while
+        # to make sure we process events leading up to this
+        gtk.gdk.window_process_all_updates()
+        while gtk.events_pending():
+            gtk.main_iteration(False)
+
+        fn = tempfile.mktemp(".csv")
+        filename, headers = urllib.urlretrieve(query, fn)
+        if not os.path.exists(filename):
+            LOG.error("Failed, headers were: %s", headers)
+            common.show_error(_("RepeaterBook query failed"))
+            self.window.set_cursor(None)
+            return
+
+        try:
+            # Validate CSV
+            radio = repeaterbook.RBRadio(filename)
+            if radio.errors:
+                reporting.report_misc_error("repeaterbook",
+                                            ("query=%s\n" % query) +
+                                            ("\n") +
+                                            ("\n".join(radio.errors)))
+        except errors.InvalidDataError, e:
+            common.show_error(str(e))
+            self.window.set_cursor(None)
+            return
+        except Exception, e:
+            common.log_exception()
+
+        reporting.report_model_usage(radio, "import", True)
+
+        self.window.set_cursor(None)
+        if do_import:
+            eset = self.get_current_editorset()
+            count = eset.do_import(filename)
+        else:
+            self.do_open_live(radio, read_only=True)
+
+    def do_przemienniki_prompt(self):
+        d = inputdialog.FieldDialog(title='przemienniki.net query',
+                                    parent=self)
+        fields = {
+            "Country":
+                (miscwidgets.make_choice(
+                    ['at', 'bg', 'by', 'ch', 'cz', 'de', 'dk', 'es', 'fi',
+                        'fr', 'hu', 'it', 'lt', 'lv', 'no', 'pl', 'ro', 'se',
+                        'sk', 'ua', 'uk'], False),
+                 lambda x: str(x.get_active_text())),
+            "Band":
+                (miscwidgets.make_choice(['10m', '4m', '6m', '2m', '70cm',
+                                          '23cm', '13cm', '3cm'], False, '2m'),
+                 lambda x: str(x.get_active_text())),
+            "Mode":
+                (miscwidgets.make_choice(['fm', 'dv'], False),
+                 lambda x: str(x.get_active_text())),
+            "Only Working":
+                (miscwidgets.make_choice(['', 'yes'], False),
+                 lambda x: str(x.get_active_text())),
+            "Latitude": (gtk.Entry(), lambda x: float(x.get_text())),
+            "Longitude": (gtk.Entry(), lambda x: float(x.get_text())),
+            "Range": (gtk.Entry(), lambda x: int(x.get_text())),
+        }
+        for name in sorted(fields.keys()):
+            value, fn = fields[name]
+            d.add_field(name, value)
+        while d.run() == gtk.RESPONSE_OK:
+            query = "http://przemienniki.net/export/chirp.csv?"
+            args = []
+            for name, (value, fn) in fields.items():
+                if isinstance(value, gtk.Entry):
+                    contents = value.get_text()
+                else:
+                    contents = value.get_active_text()
+                if contents:
+                    try:
+                        _value = fn(value)
+                    except ValueError:
+                        common.show_error(_("Invalid value for %s") % name)
+                        query = None
+                        continue
+
+                    args.append("=".join((name.replace(" ", "").lower(),
+                                          contents)))
+            query += "&".join(args)
+            LOG.debug(query)
+            d.destroy()
+            return query
+
+        d.destroy()
+        return query
+
+    def do_przemienniki(self, do_import):
+        url = self.do_przemienniki_prompt()
+        if not url:
+            return
+
+        fn = tempfile.mktemp(".csv")
+        filename, headers = urllib.urlretrieve(url, fn)
+        if not os.path.exists(filename):
+            LOG.error("Failed, headers were: %s", str(headers))
+            common.show_error(_("Query failed"))
+            return
+
+        class PRRadio(generic_csv.CSVRadio,
+                      chirp_common.NetworkSourceRadio):
+            VENDOR = "przemienniki.net"
+            MODEL = ""
+
+        try:
+            radio = PRRadio(filename)
+        except Exception, e:
+            common.show_error(str(e))
+            return
+
+        if do_import:
+            eset = self.get_current_editorset()
+            count = eset.do_import(filename)
+        else:
+            self.do_open_live(radio, read_only=True)
+
+    def do_rfinder_prompt(self):
+        fields = {"1Email": (gtk.Entry(), lambda x: "@" in x),
+                  "2Password": (gtk.Entry(), lambda x: x),
+                  "3Latitude": (gtk.Entry(),
+                                lambda x: float(x) < 90 and float(x) > -90),
+                  "4Longitude": (gtk.Entry(),
+                                 lambda x: float(x) < 180 and float(x) > -180),
+                  "5Range_in_Miles": (gtk.Entry(),
+                                      lambda x: int(x) > 0 and int(x) < 5000),
+                  }
+
+        d = inputdialog.FieldDialog(title="RFinder Login", parent=self)
+        for k in sorted(fields.keys()):
+            d.add_field(k[1:].replace("_", " "), fields[k][0])
+            fields[k][0].set_text(CONF.get(k[1:], "rfinder") or "")
+            fields[k][0].set_visibility(k != "2Password")
+
+        while d.run() == gtk.RESPONSE_OK:
+            valid = True
+            for k in sorted(fields.keys()):
+                widget, validator = fields[k]
+                try:
+                    if validator(widget.get_text()):
+                        CONF.set(k[1:], widget.get_text(), "rfinder")
+                        continue
+                except Exception:
+                    pass
+                common.show_error("Invalid value for %s" % k[1:])
+                valid = False
+                break
+
+            if valid:
+                d.destroy()
+                return True
+
+        d.destroy()
+        return False
+
+    def do_rfinder(self, do_import):
+        self.window.set_cursor(gtk.gdk.Cursor(gtk.gdk.WATCH))
+        if not self.do_rfinder_prompt():
+            self.window.set_cursor(None)
+            return
+
+        lat = CONF.get_float("Latitude", "rfinder")
+        lon = CONF.get_float("Longitude", "rfinder")
+        passwd = CONF.get("Password", "rfinder")
+        email = CONF.get("Email", "rfinder")
+        miles = CONF.get_int("Range_in_Miles", "rfinder")
+
+        # Do this in case the import process is going to take a while
+        # to make sure we process events leading up to this
+        gtk.gdk.window_process_all_updates()
+        while gtk.events_pending():
+            gtk.main_iteration(False)
+
+        if do_import:
+            eset = self.get_current_editorset()
+            rfstr = "rfinder://%s/%s/%f/%f/%i" % \
+                    (email, passwd, lat, lon, miles)
+            count = eset.do_import(rfstr)
+        else:
+            from chirp.drivers import rfinder
+            radio = rfinder.RFinderRadio(None)
+            radio.set_params((lat, lon), miles, email, passwd)
+            self.do_open_live(radio, read_only=True)
+
+        self.window.set_cursor(None)
+
+    def do_radioreference_prompt(self):
+        fields = {"1Username":  (gtk.Entry(), lambda x: x),
+                  "2Password":  (gtk.Entry(), lambda x: x),
+                  "3Zipcode":   (gtk.Entry(), lambda x: x),
+                  }
+
+        d = inputdialog.FieldDialog(title=_("RadioReference.com Query"),
+                                    parent=self)
+        for k in sorted(fields.keys()):
+            d.add_field(k[1:], fields[k][0])
+            fields[k][0].set_text(CONF.get(k[1:], "radioreference") or "")
+            fields[k][0].set_visibility(k != "2Password")
+
+        while d.run() == gtk.RESPONSE_OK:
+            valid = True
+            for k in sorted(fields.keys()):
+                widget, validator = fields[k]
+                try:
+                    if validator(widget.get_text()):
+                        CONF.set(k[1:], widget.get_text(), "radioreference")
+                        continue
+                except Exception:
+                    pass
+                common.show_error("Invalid value for %s" % k[1:])
+                valid = False
+                break
+
+            if valid:
+                d.destroy()
+                return True
+
+        d.destroy()
+        return False
+
+    def do_radioreference(self, do_import):
+        self.window.set_cursor(gtk.gdk.Cursor(gtk.gdk.WATCH))
+        if not self.do_radioreference_prompt():
+            self.window.set_cursor(None)
+            return
+
+        username = CONF.get("Username", "radioreference")
+        passwd = CONF.get("Password", "radioreference")
+        zipcode = CONF.get("Zipcode", "radioreference")
+
+        # Do this in case the import process is going to take a while
+        # to make sure we process events leading up to this
+        gtk.gdk.window_process_all_updates()
+        while gtk.events_pending():
+            gtk.main_iteration(False)
+
+        if do_import:
+            eset = self.get_current_editorset()
+            rrstr = "radioreference://%s/%s/%s" % (zipcode, username, passwd)
+            count = eset.do_import(rrstr)
+        else:
+            try:
+                from chirp import radioreference
+                radio = radioreference.RadioReferenceRadio(None)
+                radio.set_params(zipcode, username, passwd)
+                self.do_open_live(radio, read_only=True)
+            except errors.RadioError, e:
+                common.show_error(e)
+
+        self.window.set_cursor(None)
+
+    def do_export(self):
+        types = [(_("CSV Files") + " (*.csv)", "csv"),
+                 ]
+
+        eset = self.get_current_editorset()
+
+        if os.path.exists(eset.filename):
+            base = os.path.basename(eset.filename)
+            if "." in base:
+                base = base[:base.rindex(".")]
+            defname = base
+        else:
+            defname = "radio"
+
+        filen = platform.get_platform().gui_save_file(default_name=defname,
+                                                      types=types)
+        if not filen:
+            return
+
+        if os.path.exists(filen):
+            dlg = inputdialog.OverwriteDialog(filen)
+            owrite = dlg.run()
+            dlg.destroy()
+            if owrite != gtk.RESPONSE_OK:
+                return
+            os.remove(filen)
+
+        count = eset.do_export(filen)
+        reporting.report_model_usage(eset.rthread.radio, "export", count > 0)
+
+    def do_about(self):
+        d = gtk.AboutDialog()
+        d.set_transient_for(self)
+        import sys
+        verinfo = "GTK %s\nPyGTK %s\nPython %s\n" % (
+            ".".join([str(x) for x in gtk.gtk_version]),
+            ".".join([str(x) for x in gtk.pygtk_version]),
+            sys.version.split()[0])
+
+        # Set url hook to handle user activating a URL link in the about dialog
+        gtk.about_dialog_set_url_hook(lambda dlg, url: webbrowser.open(url))
+
+        d.set_name("CHIRP")
+        d.set_version(CHIRP_VERSION)
+        d.set_copyright("Copyright 2015 Dan Smith (KK7DS)")
+        d.set_website("http://chirp.danplanet.com")
+        d.set_authors(("Dan Smith KK7DS <dsmith at danplanet.com>",
+                       _("With significant contributions from:"),
+                       "Tom KD7LXL",
+                       "Marco IZ3GME",
+                       "Jim KC9HI"
+                       ))
+        d.set_translator_credits("Polish: Grzegorz SQ2RBY" +
+                                 os.linesep +
+                                 "Italian: Fabio IZ2QDH" +
+                                 os.linesep +
+                                 "Dutch: Michael PD4MT" +
+                                 os.linesep +
+                                 "German: Benjamin HB9EUK" +
+                                 os.linesep +
+                                 "Hungarian: Attila HA5JA" +
+                                 os.linesep +
+                                 "Russian: Dmitry Slukin" +
+                                 os.linesep +
+                                 "Portuguese (BR): Crezivando PP7CJ")
+        d.set_comments(verinfo)
+
+        d.run()
+        d.destroy()
+
+    def do_gethelp(self):
+        webbrowser.open("http://chirp.danplanet.com")
+
+    def do_columns(self):
+        eset = self.get_current_editorset()
+        driver = directory.get_driver(eset.rthread.radio.__class__)
+        radio_name = "%s %s %s" % (eset.rthread.radio.VENDOR,
+                                   eset.rthread.radio.MODEL,
+                                   eset.rthread.radio.VARIANT)
+        d = gtk.Dialog(title=_("Select Columns"),
+                       parent=self,
+                       buttons=(gtk.STOCK_OK, gtk.RESPONSE_OK,
+                                gtk.STOCK_CANCEL, gtk.RESPONSE_CANCEL))
+
+        vbox = gtk.VBox()
+        vbox.show()
+        sw = gtk.ScrolledWindow()
+        sw.set_policy(gtk.POLICY_NEVER, gtk.POLICY_AUTOMATIC)
+        sw.add_with_viewport(vbox)
+        sw.show()
+        d.vbox.pack_start(sw, 1, 1, 1)
+        d.set_size_request(-1, 300)
+        d.set_resizable(False)
+
+        labelstr = _("Visible columns for {radio}").format(radio=radio_name)
+        label = gtk.Label(labelstr)
+        label.show()
+        vbox.pack_start(label)
+
+        fields = []
+        memedit = eset.get_current_editor()  # .editors["memedit"]
+        unsupported = memedit.get_unsupported_columns()
+        for colspec in memedit.cols:
+            if colspec[0].startswith("_"):
+                continue
+            elif colspec[0] in unsupported:
+                continue
+            label = colspec[0]
+            visible = memedit.get_column_visible(memedit.col(label))
+            widget = gtk.CheckButton(label)
+            widget.set_active(visible)
+            fields.append(widget)
+            vbox.pack_start(widget, 1, 1, 1)
+            widget.show()
+
+        res = d.run()
+        selected_columns = []
+        if res == gtk.RESPONSE_OK:
+            for widget in fields:
+                colnum = memedit.col(widget.get_label())
+                memedit.set_column_visible(colnum, widget.get_active())
+                if widget.get_active():
+                    selected_columns.append(widget.get_label())
+
+        d.destroy()
+
+        CONF.set(driver, ",".join(selected_columns), "memedit_columns")
+
+    def do_hide_unused(self, action):
+        eset = self.get_current_editorset()
+        if eset is None:
+            conf = config.get("memedit")
+            conf.set_bool("hide_unused", action.get_active())
+        else:
+            for editortype, editor in eset.editors.iteritems():
+                if "memedit" in editortype:
+                    editor.set_hide_unused(action.get_active())
+
+    def do_clearq(self):
+        eset = self.get_current_editorset()
+        eset.rthread.flush()
+
+    def do_copy(self, cut):
+        eset = self.get_current_editorset()
+        eset.get_current_editor().copy_selection(cut)
+
+    def do_paste(self):
+        eset = self.get_current_editorset()
+        eset.get_current_editor().paste_selection()
+
+    def do_delete(self):
+        eset = self.get_current_editorset()
+        eset.get_current_editor().copy_selection(True)
+
+    def do_toggle_report(self, action):
+        if not action.get_active():
+            d = gtk.MessageDialog(buttons=gtk.BUTTONS_YES_NO, parent=self)
+            markup = "<b><big>" + _("Reporting is disabled") + "</big></b>"
+            d.set_markup(markup)
+            msg = _("The reporting feature of CHIRP is designed to help "
+                    "<u>improve quality</u> by allowing the authors to focus "
+                    "on the radio drivers used most often and errors "
+                    "experienced by the users. The reports contain no "
+                    "identifying information and are used only for "
+                    "statistical purposes by the authors. Your privacy is "
+                    "extremely important, but <u>please consider leaving "
+                    "this feature enabled to help make CHIRP better!</u>\n\n"
+                    "<b>Are you sure you want to disable this feature?</b>")
+            d.format_secondary_markup(msg.replace("\n", "\r\n"))
+            r = d.run()
+            d.destroy()
+            if r == gtk.RESPONSE_NO:
+                action.set_active(not action.get_active())
+
+        conf = config.get()
+        conf.set_bool("no_report", not action.get_active())
+
+    def do_toggle_no_smart_tmode(self, action):
+        CONF.set_bool("no_smart_tmode", not action.get_active(), "memedit")
+
+    def do_toggle_developer(self, action):
+        conf = config.get()
+        conf.set_bool("developer", action.get_active(), "state")
+
+        for name in ["viewdeveloper", "loadmod"]:
+            devaction = self.menu_ag.get_action(name)
+            devaction.set_visible(action.get_active())
+
+    def do_toggle_clone_information(self, action):
+        CONF.set_bool("clone_information",
+                      not action.get_active(), "noconfirm")
+
+    def do_toggle_clone_instructions(self, action):
+        CONF.set_bool("clone_instructions",
+                      not action.get_active(), "noconfirm")
+
+    def do_change_language(self):
+        langs = ["Auto", "English", "Polish", "Italian", "Dutch", "German",
+                 "Hungarian", "Russian", "Portuguese (BR)", "French",
+                 "Spanish"]
+        d = inputdialog.ChoiceDialog(langs, parent=self,
+                                     title="Choose Language")
+        d.label.set_text(_("Choose a language or Auto to use the "
+                           "operating system default. You will need to "
+                           "restart the application before the change "
+                           "will take effect"))
+        d.label.set_line_wrap(True)
+        r = d.run()
+        if r == gtk.RESPONSE_OK:
+            LOG.debug("Chose language %s" % d.choice.get_active_text())
+            conf = config.get()
+            conf.set("language", d.choice.get_active_text(), "state")
+        d.destroy()
+
+    def load_module(self):
+        types = [(_("Python Modules") + "*.py", "*.py")]
+        filen = platform.get_platform().gui_open_file(types=types)
+        if not filen:
+            return
+
+        # We're in development mode, so we need to tell the directory to
+        # allow a loaded module to override an existing driver, against
+        # its normal better judgement
+        directory.enable_reregistrations()
+
+        try:
+            module = file(filen)
+            code = module.read()
+            module.close()
+            pyc = compile(code, filen, 'exec')
+            # See this for why:
+            # http://stackoverflow.com/questions/2904274/globals-and-locals-in-python-exec
+            exec(pyc, globals(), globals())
+        except Exception, e:
+            common.log_exception()
+            common.show_error("Unable to load module: %s" % e)
+
+    def mh(self, _action, *args):
+        action = _action.get_name()
+
+        if action == "quit":
+            gtk.main_quit()
+        elif action == "new":
+            self.do_new()
+        elif action == "open":
+            self.do_open()
+        elif action == "save":
+            self.do_save()
+        elif action == "saveas":
+            self.do_saveas()
+        elif action.startswith("download"):
+            self.do_download(*args)
+        elif action.startswith("upload"):
+            self.do_upload(*args)
+        elif action == "close":
+            self.do_close()
+        elif action == "import":
+            self.do_import()
+        elif action in ["qdmrmarc", "idmrmarc"]:
+            self.do_dmrmarc(action[0] == "i")
+        elif action in ["qrfinder", "irfinder"]:
+            self.do_rfinder(action[0] == "i")
+        elif action in ["qradioreference", "iradioreference"]:
+            self.do_radioreference(action[0] == "i")
+        elif action == "export":
+            self.do_export()
+        elif action in ["qrbookpolitical", "irbookpolitical"]:
+            self.do_repeaterbook_political(action[0] == "i")
+        elif action in ["qrbookproximity", "irbookproximity"]:
+            self.do_repeaterbook_proximity(action[0] == "i")
+        elif action in ["qpr", "ipr"]:
+            self.do_przemienniki(action[0] == "i")
+        elif action == "about":
+            self.do_about()
+        elif action == "gethelp":
+            self.do_gethelp()
+        elif action == "columns":
+            self.do_columns()
+        elif action == "hide_unused":
+            self.do_hide_unused(_action)
+        elif action == "cancelq":
+            self.do_clearq()
+        elif action == "report":
+            self.do_toggle_report(_action)
+        elif action == "channel_defaults":
+            # The memedit thread also has an instance of bandplans.
+            bp = bandplans.BandPlans(CONF)
+            bp.select_bandplan(self)
+        elif action == "no_smart_tmode":
+            self.do_toggle_no_smart_tmode(_action)
+        elif action == "developer":
+            self.do_toggle_developer(_action)
+        elif action == "clone_information":
+            self.do_toggle_clone_information(_action)
+        elif action == "clone_instructions":
+            self.do_toggle_clone_instructions(_action)
+        elif action in ["cut", "copy", "paste", "delete",
+                        "move_up", "move_dn", "exchange", "all",
+                        "devshowraw", "devdiffraw", "properties"]:
+            self.get_current_editorset().get_current_editor().hotkey(_action)
+        elif action == "devdifftab":
+            self.do_diff_radio()
+        elif action == "language":
+            self.do_change_language()
+        elif action == "loadmod":
+            self.load_module()
+        else:
+            return
+
+        self.ev_tab_switched()
+
+    def make_menubar(self):
+        menu_xml = """
+<ui>
+  <menubar name="MenuBar">
+    <menu action="file">
+      <menuitem action="new"/>
+      <menuitem action="open"/>
+      <menu action="openstock" name="openstock"/>
+      <menu action="recent" name="recent"/>
+      <menuitem action="save"/>
+      <menuitem action="saveas"/>
+      <menuitem action="loadmod"/>
+      <separator/>
+      <menuitem action="import"/>
+      <menuitem action="export"/>
+      <separator/>
+      <menuitem action="close"/>
+      <menuitem action="quit"/>
+    </menu>
+    <menu action="edit">
+      <menuitem action="cut"/>
+      <menuitem action="copy"/>
+      <menuitem action="paste"/>
+      <menuitem action="delete"/>
+      <separator/>
+      <menuitem action="all"/>
+      <separator/>
+      <menuitem action="move_up"/>
+      <menuitem action="move_dn"/>
+      <menuitem action="exchange"/>
+      <separator/>
+      <menuitem action="properties"/>
+    </menu>
+    <menu action="view">
+      <menuitem action="columns"/>
+      <menuitem action="hide_unused"/>
+      <menuitem action="no_smart_tmode"/>
+      <menu action="viewdeveloper">
+        <menuitem action="devshowraw"/>
+        <menuitem action="devdiffraw"/>
+        <menuitem action="devdifftab"/>
+      </menu>
+      <menuitem action="language"/>
+    </menu>
+    <menu action="radio" name="radio">
+      <menuitem action="download"/>
+      <menuitem action="upload"/>
+      <menu action="importsrc" name="importsrc">
+        <menuitem action="idmrmarc"/>
+        <menuitem action="iradioreference"/>
+        <menu action="irbook" name="irbook">
+            <menuitem action="irbookpolitical"/>
+            <menuitem action="irbookproximity"/>
+        </menu>
+        <menuitem action="ipr"/>
+        <menuitem action="irfinder"/>
+      </menu>
+      <menu action="querysrc" name="querysrc">
+        <menuitem action="qdmrmarc"/>
+        <menuitem action="qradioreference"/>
+        <menu action="qrbook" name="qrbook">
+            <menuitem action="qrbookpolitical"/>
+            <menuitem action="qrbookproximity"/>
+        </menu>
+        <menuitem action="qpr"/>
+        <menuitem action="qrfinder"/>
+      </menu>
+      <menu action="stock" name="stock"/>
+      <separator/>
+      <menuitem action="channel_defaults"/>
+      <separator/>
+      <menuitem action="cancelq"/>
+    </menu>
+    <menu action="help">
+      <menuitem action="gethelp"/>
+      <separator/>
+      <menuitem action="report"/>
+      <menuitem action="clone_information"/>
+      <menuitem action="clone_instructions"/>
+      <menuitem action="developer"/>
+      <separator/>
+      <menuitem action="about"/>
+    </menu>
+  </menubar>
+</ui>
+"""
+        ALT_KEY = "<Alt>"
+        CTRL_KEY = "<Ctrl>"
+        if sys.platform == 'darwin':
+            ALT_KEY = "<Meta>"
+            CTRL_KEY = "<Meta>"
+        actions = [
+            ('file', None, _("_File"), None, None, self.mh),
+            ('new', gtk.STOCK_NEW, None, None, None, self.mh),
+            ('open', gtk.STOCK_OPEN, None, None, None, self.mh),
+            ('openstock', None, _("Open stock config"), None, None, self.mh),
+            ('recent', None, _("_Recent"), None, None, self.mh),
+            ('save', gtk.STOCK_SAVE, None, None, None, self.mh),
+            ('saveas', gtk.STOCK_SAVE_AS, None, None, None, self.mh),
+            ('loadmod', None, _("Load Module"), None, None, self.mh),
+            ('close', gtk.STOCK_CLOSE, None, None, None, self.mh),
+            ('quit', gtk.STOCK_QUIT, None, None, None, self.mh),
+            ('edit', None, _("_Edit"), None, None, self.mh),
+            ('cut', None, _("_Cut"), "%sx" % CTRL_KEY, None, self.mh),
+            ('copy', None, _("_Copy"), "%sc" % CTRL_KEY, None, self.mh),
+            ('paste', None, _("_Paste"),
+             "%sv" % CTRL_KEY, None, self.mh),
+            ('delete', None, _("_Delete"), "Delete", None, self.mh),
+            ('all', None, _("Select _All"), None, None, self.mh),
+            ('move_up', None, _("Move _Up"),
+             "%sUp" % CTRL_KEY, None, self.mh),
+            ('move_dn', None, _("Move Dow_n"),
+             "%sDown" % CTRL_KEY, None, self.mh),
+            ('exchange', None, _("E_xchange"),
+             "%s<Shift>x" % CTRL_KEY, None, self.mh),
+            ('properties', None, _("P_roperties"), None, None, self.mh),
+            ('view', None, _("_View"), None, None, self.mh),
+            ('columns', None, _("Columns"), None, None, self.mh),
+            ('viewdeveloper', None, _("Developer"), None, None, self.mh),
+            ('devshowraw', None, _('Show raw memory'),
+             "%s<Shift>r" % CTRL_KEY, None, self.mh),
+            ('devdiffraw', None, _("Diff raw memories"),
+             "%s<Shift>d" % CTRL_KEY, None, self.mh),
+            ('devdifftab', None, _("Diff tabs"),
+             "%s<Shift>t" % CTRL_KEY, None, self.mh),
+            ('language', None, _("Change language"), None, None, self.mh),
+            ('radio', None, _("_Radio"), None, None, self.mh),
+            ('download', None, _("Download From Radio"),
+             "%sd" % ALT_KEY, None, self.mh),
+            ('upload', None, _("Upload To Radio"),
+             "%su" % ALT_KEY, None, self.mh),
+            ('import', None, _("Import"), "%si" % ALT_KEY, None, self.mh),
+            ('export', None, _("Export"), "%se" % ALT_KEY, None, self.mh),
+            ('importsrc', None, _("Import from data source"),
+             None, None, self.mh),
+            ('idmrmarc', None, _("DMR-MARC Repeaters"), None, None, self.mh),
+            ('iradioreference', None, _("RadioReference.com"),
+             None, None, self.mh),
+            ('irfinder', None, _("RFinder"), None, None, self.mh),
+            ('irbook', None, _("RepeaterBook"), None, None, self.mh),
+            ('irbookpolitical', None, _("RepeaterBook political query"), None,
+             None, self.mh),
+            ('irbookproximity', None, _("RepeaterBook proximity query"), None,
+             None, self.mh),
+            ('ipr', None, _("przemienniki.net"), None, None, self.mh),
+            ('querysrc', None, _("Query data source"), None, None, self.mh),
+            ('qdmrmarc', None, _("DMR-MARC Repeaters"), None, None, self.mh),
+            ('qradioreference', None, _("RadioReference.com"),
+             None, None, self.mh),
+            ('qrfinder', None, _("RFinder"), None, None, self.mh),
+            ('qpr', None, _("przemienniki.net"), None, None, self.mh),
+            ('qrbook', None, _("RepeaterBook"), None, None, self.mh),
+            ('qrbookpolitical', None, _("RepeaterBook political query"), None,
+             None, self.mh),
+            ('qrbookproximity', None, _("RepeaterBook proximity query"), None,
+             None, self.mh),
+            ('export_chirp', None, _("CHIRP Native File"),
+             None, None, self.mh),
+            ('export_csv', None, _("CSV File"), None, None, self.mh),
+            ('stock', None, _("Import from stock config"),
+             None, None, self.mh),
+            ('channel_defaults', None, _("Channel defaults"),
+             None, None, self.mh),
+            ('cancelq', gtk.STOCK_STOP, None, "Escape", None, self.mh),
+            ('help', None, _('Help'), None, None, self.mh),
+            ('about', gtk.STOCK_ABOUT, None, None, None, self.mh),
+            ('gethelp', None, _("Get Help Online..."), None, None, self.mh),
+        ]
+
+        conf = config.get()
+        re = not conf.get_bool("no_report")
+        hu = conf.get_bool("hide_unused", "memedit", default=True)
+        dv = conf.get_bool("developer", "state")
+        cf = not conf.get_bool("clone_information", "noconfirm")
+        ci = not conf.get_bool("clone_instructions", "noconfirm")
+        st = not conf.get_bool("no_smart_tmode", "memedit")
+
+        toggles = [('report', None, _("Report Statistics"),
+                    None, None, self.mh, re),
+                   ('hide_unused', None, _("Hide Unused Fields"),
+                    None, None, self.mh, hu),
+                   ('no_smart_tmode', None, _("Smart Tone Modes"),
+                    None, None, self.mh, st),
+                   ('clone_information', None, _("Show Information"),
+                    None, None, self.mh, cf),
+                   ('clone_instructions', None, _("Show Instructions"),
+                    None, None, self.mh, ci),
+                   ('developer', None, _("Enable Developer Functions"),
+                    None, None, self.mh, dv),
+                   ]
+
+        self.menu_uim = gtk.UIManager()
+        self.menu_ag = gtk.ActionGroup("MenuBar")
+        self.menu_ag.add_actions(actions)
+        self.menu_ag.add_toggle_actions(toggles)
+
+        self.menu_uim.insert_action_group(self.menu_ag, 0)
+        self.menu_uim.add_ui_from_string(menu_xml)
+
+        self.add_accel_group(self.menu_uim.get_accel_group())
+
+        self.infomenu = self.menu_uim.get_widget(
+            "/MenuBar/help/clone_information")
+
+        self.clonemenu = self.menu_uim.get_widget(
+            "/MenuBar/help/clone_instructions")
+
+        # Initialize
+        self.do_toggle_developer(self.menu_ag.get_action("developer"))
+
+        return self.menu_uim.get_widget("/MenuBar")
+
+    def make_tabs(self):
+        self.tabs = gtk.Notebook()
+        self.tabs.set_scrollable(True)
+
+        return self.tabs
+
+    def close_out(self):
+        num = self.tabs.get_n_pages()
+        while num > 0:
+            num -= 1
+            LOG.debug("Closing %i" % num)
+            try:
+                self.do_close(self.tabs.get_nth_page(num))
+            except ModifiedError:
+                return False
+
+        gtk.main_quit()
+
+        return True
+
+    def make_status_bar(self):
+        box = gtk.HBox(False, 2)
+
+        self.sb_general = gtk.Statusbar()
+        self.sb_general.set_has_resize_grip(False)
+        self.sb_general.show()
+        box.pack_start(self.sb_general, 1, 1, 1)
+
+        self.sb_radio = gtk.Statusbar()
+        self.sb_radio.set_has_resize_grip(True)
+        self.sb_radio.show()
+        box.pack_start(self.sb_radio, 1, 1, 1)
+
+        box.show()
+        return box
+
+    def ev_delete(self, window, event):
+        if not self.close_out():
+            return True  # Don't exit
+
+    def ev_destroy(self, window):
+        if not self.close_out():
+            return True  # Don't exit
+
+    def setup_extra_hotkeys(self):
+        accelg = self.menu_uim.get_accel_group()
+
+        def memedit(a):
+            self.get_current_editorset().editors["memedit"].hotkey(a)
+
+        actions = [
+            # ("action_name", "key", function)
+        ]
+
+        for name, key, fn in actions:
+            a = gtk.Action(name, name, name, "")
+            a.connect("activate", fn)
+            self.menu_ag.add_action_with_accel(a, key)
+            a.set_accel_group(accelg)
+            a.connect_accelerator()
+
+    def _set_icon(self):
+        this_platform = platform.get_platform()
+        path = (this_platform.find_resource("chirp.png") or
+                this_platform.find_resource(os.path.join("pixmaps",
+                                                         "chirp.png")))
+        if os.path.exists(path):
+            self.set_icon_from_file(path)
+        else:
+            LOG.warn("Icon %s not found" % path)
+
+    def _updates(self, version):
+        if not version:
+            return
+
+        if version == CHIRP_VERSION:
+            return
+
+        LOG.info("Server reports version %s is available" % version)
+
+        # Report new updates every three days
+        intv = 3600 * 24 * 3
+
+        if CONF.is_defined("last_update_check", "state") and \
+           (time.time() - CONF.get_int("last_update_check", "state")) < intv:
+            return
+
+        CONF.set_int("last_update_check", int(time.time()), "state")
+        d = gtk.MessageDialog(buttons=gtk.BUTTONS_OK_CANCEL, parent=self,
+                              type=gtk.MESSAGE_INFO)
+        d.label.set_markup(
+            _('A new version of CHIRP is available: ' +
+              '{ver}. '.format(ver=version) +
+              'It is recommended that you upgrade as soon as possible. '
+              'Please go to: \r\n\r\n<a href="http://chirp.danplanet.com">' +
+              'http://chirp.danplanet.com</a>'))
+        response = d.run()
+        d.destroy()
+        if response == gtk.RESPONSE_OK:
+            webbrowser.open('http://chirp.danplanet.com/'
+                            'projects/chirp/wiki/Download')
+
+    def _init_macos(self, menu_bar):
+        macapp = None
+
+        # for KK7DS runtime <= R10
+        try:
+            import gtk_osxapplication
+            macapp = gtk_osxapplication.OSXApplication()
+        except ImportError:
+            pass
+
+        # for gtk-mac-integration >= 2.0.7
+        try:
+            import gtkosx_application
+            macapp = gtkosx_application.Application()
+        except ImportError:
+            pass
+
+        if macapp is None:
+            LOG.error("No MacOS support: %s" % e)
+            return
+
+        this_platform = platform.get_platform()
+        icon = (this_platform.find_resource("chirp.png") or
+                this_platform.find_resource(os.path.join("pixmaps",
+                                                         "chirp.png")))
+        if os.path.exists(icon):
+            icon_pixmap = gtk.gdk.pixbuf_new_from_file(icon)
+            macapp.set_dock_icon_pixbuf(icon_pixmap)
+
+        menu_bar.hide()
+        macapp.set_menu_bar(menu_bar)
+
+        quititem = self.menu_uim.get_widget("/MenuBar/file/quit")
+        quititem.hide()
+
+        aboutitem = self.menu_uim.get_widget("/MenuBar/help/about")
+        macapp.insert_app_menu_item(aboutitem, 0)
+
+        documentationitem = self.menu_uim.get_widget("/MenuBar/help/gethelp")
+        macapp.insert_app_menu_item(documentationitem, 0)
+
+        macapp.set_use_quartz_accelerators(False)
+        macapp.ready()
+
+        LOG.debug("Initialized MacOS support")
+
+    def __init__(self, *args, **kwargs):
+        gtk.Window.__init__(self, *args, **kwargs)
+
+        def expose(window, event):
+            allocation = window.get_allocation()
+            CONF.set_int("window_w", allocation.width, "state")
+            CONF.set_int("window_h", allocation.height, "state")
+        self.connect("expose_event", expose)
+
+        def state_change(window, event):
+            CONF.set_bool(
+                "window_maximized",
+                event.new_window_state == gtk.gdk.WINDOW_STATE_MAXIMIZED,
+                "state")
+        self.connect("window-state-event", state_change)
+
+        d = CONF.get("last_dir", "state")
+        if d and os.path.isdir(d):
+            platform.get_platform().set_last_dir(d)
+
+        vbox = gtk.VBox(False, 2)
+
+        self._recent = []
+
+        self.menu_ag = None
+        mbar = self.make_menubar()
+
+        if os.name != "nt":
+            self._set_icon()  # Windows gets the icon from the exe
+            if os.uname()[0] == "Darwin":
+                self._init_macos(mbar)
+
+        vbox.pack_start(mbar, 0, 0, 0)
+
+        self.tabs = None
+        tabs = self.make_tabs()
+        tabs.connect("switch-page", lambda n, _, p: self.ev_tab_switched(p))
+        tabs.connect("page-removed", lambda *a: self.ev_tab_switched())
+        tabs.show()
+        self.ev_tab_switched()
+        vbox.pack_start(tabs, 1, 1, 1)
+
+        vbox.pack_start(self.make_status_bar(), 0, 0, 0)
+
+        vbox.show()
+
+        self.add(vbox)
+
+        try:
+            width = CONF.get_int("window_w", "state")
+            height = CONF.get_int("window_h", "state")
+        except Exception:
+            width = 800
+            height = 600
+
+        self.set_default_size(width, height)
+        if CONF.get_bool("window_maximized", "state"):
+            self.maximize()
+        self.set_title("CHIRP")
+
+        self.connect("delete_event", self.ev_delete)
+        self.connect("destroy", self.ev_destroy)
+
+        if not CONF.get_bool("warned_about_reporting") and \
+                not CONF.get_bool("no_report"):
+            d = gtk.MessageDialog(buttons=gtk.BUTTONS_OK, parent=self)
+            d.set_markup("<b><big>" +
+                         _("Error reporting is enabled") +
+                         "</big></b>")
+            d.format_secondary_markup(
+                _("If you wish to disable this feature you may do so in "
+                  "the <u>Help</u> menu"))
+            d.run()
+            d.destroy()
+        CONF.set_bool("warned_about_reporting", True)
+
+        self.update_recent_files()
+        try:
+            self.update_stock_configs()
+        except UnicodeDecodeError:
+            LOG.exception('We hit bug #272 while working with unicode paths. '
+                          'Not copying stock configs so we can continue '
+                          'startup.')
+        self.setup_extra_hotkeys()
+
+        def updates_callback(ver):
+            gobject.idle_add(self._updates, ver)
+
+        if not CONF.get_bool("skip_update_check", "state"):
+            reporting.check_for_updates(updates_callback)


More information about the chirp_devel mailing list