[hamradio-commits] [chirp] 01/19: Imported Upstream version 0.3.0

Iain R. Learmonth irl at moszumanska.debian.org
Sun Nov 1 16:02:42 UTC 2015


This is an automated email from the git hooks/post-receive script.

irl pushed a commit to branch master
in repository chirp.

commit ef2d5b20453b00acc8c343c169138afd7940d3cc
Author: Bdale Garbee <bdale at gag.com>
Date:   Wed Aug 7 22:32:57 2013 -0600

    Imported Upstream version 0.3.0
---
 COPYING                                         |  674 +++
 PKG-INFO                                        |   10 +
 chirp.xsd                                       |   47 +
 chirp/__init__.py                               |   27 +
 chirp/alinco.py                                 |  549 ++
 chirp/baofeng_uv3r.py                           |  292 +
 chirp/bitwise.py                                |  850 +++
 chirp/bitwise_grammar.py                        |   81 +
 chirp/chirp_common.py                           | 1329 +++++
 chirp/detect.py                                 |  105 +
 chirp/directory.py                              |  133 +
 chirp/errors.py                                 |   38 +
 chirp/ft1802.py                                 |  234 +
 chirp/ft2800.py                                 |  279 +
 chirp/ft50.py                                   |   53 +
 chirp/ft50_ll.py                                |  289 +
 chirp/ft60.py                                   |  296 +
 chirp/ft7800.py                                 |  654 +++
 chirp/ft817.py                                  | 1123 ++++
 chirp/ft857.py                                  |  341 ++
 chirp/generic_csv.py                            |  243 +
 chirp/generic_tpe.py                            |   46 +
 chirp/generic_xml.py                            |  143 +
 chirp/ic208.py                                  |  261 +
 chirp/ic2100.py                                 |  252 +
 chirp/ic2200.py                                 |  292 +
 chirp/ic2720.py                                 |  190 +
 chirp/ic2820.py                                 |  350 ++
 chirp/ic9x.py                                   |  417 ++
 chirp/ic9x_icf.py                               |   76 +
 chirp/ic9x_icf_ll.py                            |  139 +
 chirp/ic9x_ll.py                                |  547 ++
 chirp/icf.py                                    |  655 +++
 chirp/icomciv.py                                |  413 ++
 chirp/icq7.py                                   |  150 +
 chirp/ict70.py                                  |  222 +
 chirp/ict7h.py                                  |  122 +
 chirp/ict8.py                                   |  119 +
 chirp/icw32.py                                  |  200 +
 chirp/icx8x.py                                  |  202 +
 chirp/icx8x_ll.py                               |  493 ++
 chirp/id31.py                                   |  334 ++
 chirp/id800.py                                  |  380 ++
 chirp/id880.py                                  |  396 ++
 chirp/idrp.py                                   |  166 +
 chirp/import_logic.py                           |  243 +
 chirp/kenwood_hmk.py                            |  122 +
 chirp/kenwood_live.py                           | 1118 ++++
 chirp/memmap.py                                 |   88 +
 chirp/platform.py                               |  434 ++
 chirp/puxing.py                                 |  506 ++
 chirp/pyPEG.py                                  |  312 ++
 chirp/radioreference.py                         |  180 +
 chirp/rfinder.py                                |  322 ++
 chirp/settings.py                               |  290 +
 chirp/template.py                               |  128 +
 chirp/th_uv3r.py                                |  267 +
 chirp/thd72.py                                  |  542 ++
 chirp/thuv1f.py                                 |  467 ++
 chirp/tmv71.py                                  |   75 +
 chirp/tmv71_ll.py                               |  360 ++
 chirp/util.py                                   |   85 +
 chirp/uv5r.py                                   |  996 ++++
 chirp/vx3.py                                    |  264 +
 chirp/vx5.py                                    |  276 +
 chirp/vx6.py                                    |  269 +
 chirp/vx7.py                                    |  341 ++
 chirp/vx8.py                                    |  309 ++
 chirp/vxa700.py                                 |  311 ++
 chirp/wouxun.py                                 | 1012 ++++
 chirp/wouxun_common.py                          |   79 +
 chirp/xml_ll.py                                 |  250 +
 chirp/yaesu_clone.py                            |  215 +
 chirp_banks.xsd                                 |    8 +
 chirp_memory.xsd                                |  121 +
 chirpui/__init__.py                             |   14 +
 chirpui/bankedit.py                             |  392 ++
 chirpui/clone.py                                |  246 +
 chirpui/cloneprog.py                            |   65 +
 chirpui/common.py                               |  391 ++
 chirpui/config.py                               |  110 +
 chirpui/dstaredit.py                            |  196 +
 chirpui/editorset.py                            |  373 ++
 chirpui/fips.py                                 | 6606 +++++++++++++++++++++++
 chirpui/importdialog.py                         |  647 +++
 chirpui/inputdialog.py                          |  148 +
 chirpui/mainapp.py                              | 1609 ++++++
 chirpui/memdetail.py                            |  324 ++
 chirpui/memedit.py                              | 1495 +++++
 chirpui/miscwidgets.py                          |  743 +++
 chirpui/reporting.py                            |  185 +
 chirpui/settingsedit.py                         |  209 +
 chirpui/shiftdialog.py                          |  156 +
 chirpw                                          |  142 +
 setup.cfg                                       |    5 +
 setup.py                                        |  160 +
 share/chirp.desktop                             |   13 +
 share/chirp.png                                 |  Bin 0 -> 9687 bytes
 share/chirpw.1                                  |   46 +
 stock_configs/EU LPD and PMR Channels.csv       |   78 +
 stock_configs/Marine VHF Channels.csv           |   61 +
 stock_configs/NOAA Weather Alert.csv            |    8 +
 stock_configs/US 60 meter channels (Center).csv |    6 +
 stock_configs/US 60 meter channels (Dial).csv   |    6 +
 stock_configs/US Calling Frequencies.csv        |    5 +
 stock_configs/US FRS and GMRS Channels.csv      |   23 +
 stock_configs/US MURS Channels.csv              |    6 +
 107 files changed, 38670 insertions(+)

diff --git a/COPYING b/COPYING
new file mode 100644
index 0000000..94a9ed0
--- /dev/null
+++ b/COPYING
@@ -0,0 +1,674 @@
+                    GNU GENERAL PUBLIC LICENSE
+                       Version 3, 29 June 2007
+
+ Copyright (C) 2007 Free Software Foundation, Inc. <http://fsf.org/>
+ Everyone is permitted to copy and distribute verbatim copies
+ of this license document, but changing it is not allowed.
+
+                            Preamble
+
+  The GNU General Public License is a free, copyleft license for
+software and other kinds of works.
+
+  The licenses for most software and other practical works are designed
+to take away your freedom to share and change the works.  By contrast,
+the GNU General Public License is intended to guarantee your freedom to
+share and change all versions of a program--to make sure it remains free
+software for all its users.  We, the Free Software Foundation, use the
+GNU General Public License for most of our software; it applies also to
+any other work released this way by its authors.  You can apply it to
+your programs, too.
+
+  When we speak of free software, we are referring to freedom, not
+price.  Our General Public Licenses are designed to make sure that you
+have the freedom to distribute copies of free software (and charge for
+them if you wish), that you receive source code or can get it if you
+want it, that you can change the software or use pieces of it in new
+free programs, and that you know you can do these things.
+
+  To protect your rights, we need to prevent others from denying you
+these rights or asking you to surrender the rights.  Therefore, you have
+certain responsibilities if you distribute copies of the software, or if
+you modify it: responsibilities to respect the freedom of others.
+
+  For example, if you distribute copies of such a program, whether
+gratis or for a fee, you must pass on to the recipients the same
+freedoms that you received.  You must make sure that they, too, receive
+or can get the source code.  And you must show them these terms so they
+know their rights.
+
+  Developers that use the GNU GPL protect your rights with two steps:
+(1) assert copyright on the software, and (2) offer you this License
+giving you legal permission to copy, distribute and/or modify it.
+
+  For the developers' and authors' protection, the GPL clearly explains
+that there is no warranty for this free software.  For both users' and
+authors' sake, the GPL requires that modified versions be marked as
+changed, so that their problems will not be attributed erroneously to
+authors of previous versions.
+
+  Some devices are designed to deny users access to install or run
+modified versions of the software inside them, although the manufacturer
+can do so.  This is fundamentally incompatible with the aim of
+protecting users' freedom to change the software.  The systematic
+pattern of such abuse occurs in the area of products for individuals to
+use, which is precisely where it is most unacceptable.  Therefore, we
+have designed this version of the GPL to prohibit the practice for those
+products.  If such problems arise substantially in other domains, we
+stand ready to extend this provision to those domains in future versions
+of the GPL, as needed to protect the freedom of users.
+
+  Finally, every program is threatened constantly by software patents.
+States should not allow patents to restrict development and use of
+software on general-purpose computers, but in those that do, we wish to
+avoid the special danger that patents applied to a free program could
+make it effectively proprietary.  To prevent this, the GPL assures that
+patents cannot be used to render the program non-free.
+
+  The precise terms and conditions for copying, distribution and
+modification follow.
+
+                       TERMS AND CONDITIONS
+
+  0. Definitions.
+
+  "This License" refers to version 3 of the GNU General Public License.
+
+  "Copyright" also means copyright-like laws that apply to other kinds of
+works, such as semiconductor masks.
+
+  "The Program" refers to any copyrightable work licensed under this
+License.  Each licensee is addressed as "you".  "Licensees" and
+"recipients" may be individuals or organizations.
+
+  To "modify" a work means to copy from or adapt all or part of the work
+in a fashion requiring copyright permission, other than the making of an
+exact copy.  The resulting work is called a "modified version" of the
+earlier work or a work "based on" the earlier work.
+
+  A "covered work" means either the unmodified Program or a work based
+on the Program.
+
+  To "propagate" a work means to do anything with it that, without
+permission, would make you directly or secondarily liable for
+infringement under applicable copyright law, except executing it on a
+computer or modifying a private copy.  Propagation includes copying,
+distribution (with or without modification), making available to the
+public, and in some countries other activities as well.
+
+  To "convey" a work means any kind of propagation that enables other
+parties to make or receive copies.  Mere interaction with a user through
+a computer network, with no transfer of a copy, is not conveying.
+
+  An interactive user interface displays "Appropriate Legal Notices"
+to the extent that it includes a convenient and prominently visible
+feature that (1) displays an appropriate copyright notice, and (2)
+tells the user that there is no warranty for the work (except to the
+extent that warranties are provided), that licensees may convey the
+work under this License, and how to view a copy of this License.  If
+the interface presents a list of user commands or options, such as a
+menu, a prominent item in the list meets this criterion.
+
+  1. Source Code.
+
+  The "source code" for a work means the preferred form of the work
+for making modifications to it.  "Object code" means any non-source
+form of a work.
+
+  A "Standard Interface" means an interface that either is an official
+standard defined by a recognized standards body, or, in the case of
+interfaces specified for a particular programming language, one that
+is widely used among developers working in that language.
+
+  The "System Libraries" of an executable work include anything, other
+than the work as a whole, that (a) is included in the normal form of
+packaging a Major Component, but which is not part of that Major
+Component, and (b) serves only to enable use of the work with that
+Major Component, or to implement a Standard Interface for which an
+implementation is available to the public in source code form.  A
+"Major Component", in this context, means a major essential component
+(kernel, window system, and so on) of the specific operating system
+(if any) on which the executable work runs, or a compiler used to
+produce the work, or an object code interpreter used to run it.
+
+  The "Corresponding Source" for a work in object code form means all
+the source code needed to generate, install, and (for an executable
+work) run the object code and to modify the work, including scripts to
+control those activities.  However, it does not include the work's
+System Libraries, or general-purpose tools or generally available free
+programs which are used unmodified in performing those activities but
+which are not part of the work.  For example, Corresponding Source
+includes interface definition files associated with source files for
+the work, and the source code for shared libraries and dynamically
+linked subprograms that the work is specifically designed to require,
+such as by intimate data communication or control flow between those
+subprograms and other parts of the work.
+
+  The Corresponding Source need not include anything that users
+can regenerate automatically from other parts of the Corresponding
+Source.
+
+  The Corresponding Source for a work in source code form is that
+same work.
+
+  2. Basic Permissions.
+
+  All rights granted under this License are granted for the term of
+copyright on the Program, and are irrevocable provided the stated
+conditions are met.  This License explicitly affirms your unlimited
+permission to run the unmodified Program.  The output from running a
+covered work is covered by this License only if the output, given its
+content, constitutes a covered work.  This License acknowledges your
+rights of fair use or other equivalent, as provided by copyright law.
+
+  You may make, run and propagate covered works that you do not
+convey, without conditions so long as your license otherwise remains
+in force.  You may convey covered works to others for the sole purpose
+of having them make modifications exclusively for you, or provide you
+with facilities for running those works, provided that you comply with
+the terms of this License in conveying all material for which you do
+not control copyright.  Those thus making or running the covered works
+for you must do so exclusively on your behalf, under your direction
+and control, on terms that prohibit them from making any copies of
+your copyrighted material outside their relationship with you.
+
+  Conveying under any other circumstances is permitted solely under
+the conditions stated below.  Sublicensing is not allowed; section 10
+makes it unnecessary.
+
+  3. Protecting Users' Legal Rights From Anti-Circumvention Law.
+
+  No covered work shall be deemed part of an effective technological
+measure under any applicable law fulfilling obligations under article
+11 of the WIPO copyright treaty adopted on 20 December 1996, or
+similar laws prohibiting or restricting circumvention of such
+measures.
+
+  When you convey a covered work, you waive any legal power to forbid
+circumvention of technological measures to the extent such circumvention
+is effected by exercising rights under this License with respect to
+the covered work, and you disclaim any intention to limit operation or
+modification of the work as a means of enforcing, against the work's
+users, your or third parties' legal rights to forbid circumvention of
+technological measures.
+
+  4. Conveying Verbatim Copies.
+
+  You may convey verbatim copies of the Program's source code as you
+receive it, in any medium, provided that you conspicuously and
+appropriately publish on each copy an appropriate copyright notice;
+keep intact all notices stating that this License and any
+non-permissive terms added in accord with section 7 apply to the code;
+keep intact all notices of the absence of any warranty; and give all
+recipients a copy of this License along with the Program.
+
+  You may charge any price or no price for each copy that you convey,
+and you may offer support or warranty protection for a fee.
+
+  5. Conveying Modified Source Versions.
+
+  You may convey a work based on the Program, or the modifications to
+produce it from the Program, in the form of source code under the
+terms of section 4, provided that you also meet all of these conditions:
+
+    a) The work must carry prominent notices stating that you modified
+    it, and giving a relevant date.
+
+    b) The work must carry prominent notices stating that it is
+    released under this License and any conditions added under section
+    7.  This requirement modifies the requirement in section 4 to
+    "keep intact all notices".
+
+    c) You must license the entire work, as a whole, under this
+    License to anyone who comes into possession of a copy.  This
+    License will therefore apply, along with any applicable section 7
+    additional terms, to the whole of the work, and all its parts,
+    regardless of how they are packaged.  This License gives no
+    permission to license the work in any other way, but it does not
+    invalidate such permission if you have separately received it.
+
+    d) If the work has interactive user interfaces, each must display
+    Appropriate Legal Notices; however, if the Program has interactive
+    interfaces that do not display Appropriate Legal Notices, your
+    work need not make them do so.
+
+  A compilation of a covered work with other separate and independent
+works, which are not by their nature extensions of the covered work,
+and which are not combined with it such as to form a larger program,
+in or on a volume of a storage or distribution medium, is called an
+"aggregate" if the compilation and its resulting copyright are not
+used to limit the access or legal rights of the compilation's users
+beyond what the individual works permit.  Inclusion of a covered work
+in an aggregate does not cause this License to apply to the other
+parts of the aggregate.
+
+  6. Conveying Non-Source Forms.
+
+  You may convey a covered work in object code form under the terms
+of sections 4 and 5, provided that you also convey the
+machine-readable Corresponding Source under the terms of this License,
+in one of these ways:
+
+    a) Convey the object code in, or embodied in, a physical product
+    (including a physical distribution medium), accompanied by the
+    Corresponding Source fixed on a durable physical medium
+    customarily used for software interchange.
+
+    b) Convey the object code in, or embodied in, a physical product
+    (including a physical distribution medium), accompanied by a
+    written offer, valid for at least three years and valid for as
+    long as you offer spare parts or customer support for that product
+    model, to give anyone who possesses the object code either (1) a
+    copy of the Corresponding Source for all the software in the
+    product that is covered by this License, on a durable physical
+    medium customarily used for software interchange, for a price no
+    more than your reasonable cost of physically performing this
+    conveying of source, or (2) access to copy the
+    Corresponding Source from a network server at no charge.
+
+    c) Convey individual copies of the object code with a copy of the
+    written offer to provide the Corresponding Source.  This
+    alternative is allowed only occasionally and noncommercially, and
+    only if you received the object code with such an offer, in accord
+    with subsection 6b.
+
+    d) Convey the object code by offering access from a designated
+    place (gratis or for a charge), and offer equivalent access to the
+    Corresponding Source in the same way through the same place at no
+    further charge.  You need not require recipients to copy the
+    Corresponding Source along with the object code.  If the place to
+    copy the object code is a network server, the Corresponding Source
+    may be on a different server (operated by you or a third party)
+    that supports equivalent copying facilities, provided you maintain
+    clear directions next to the object code saying where to find the
+    Corresponding Source.  Regardless of what server hosts the
+    Corresponding Source, you remain obligated to ensure that it is
+    available for as long as needed to satisfy these requirements.
+
+    e) Convey the object code using peer-to-peer transmission, provided
+    you inform other peers where the object code and Corresponding
+    Source of the work are being offered to the general public at no
+    charge under subsection 6d.
+
+  A separable portion of the object code, whose source code is excluded
+from the Corresponding Source as a System Library, need not be
+included in conveying the object code work.
+
+  A "User Product" is either (1) a "consumer product", which means any
+tangible personal property which is normally used for personal, family,
+or household purposes, or (2) anything designed or sold for incorporation
+into a dwelling.  In determining whether a product is a consumer product,
+doubtful cases shall be resolved in favor of coverage.  For a particular
+product received by a particular user, "normally used" refers to a
+typical or common use of that class of product, regardless of the status
+of the particular user or of the way in which the particular user
+actually uses, or expects or is expected to use, the product.  A product
+is a consumer product regardless of whether the product has substantial
+commercial, industrial or non-consumer uses, unless such uses represent
+the only significant mode of use of the product.
+
+  "Installation Information" for a User Product means any methods,
+procedures, authorization keys, or other information required to install
+and execute modified versions of a covered work in that User Product from
+a modified version of its Corresponding Source.  The information must
+suffice to ensure that the continued functioning of the modified object
+code is in no case prevented or interfered with solely because
+modification has been made.
+
+  If you convey an object code work under this section in, or with, or
+specifically for use in, a User Product, and the conveying occurs as
+part of a transaction in which the right of possession and use of the
+User Product is transferred to the recipient in perpetuity or for a
+fixed term (regardless of how the transaction is characterized), the
+Corresponding Source conveyed under this section must be accompanied
+by the Installation Information.  But this requirement does not apply
+if neither you nor any third party retains the ability to install
+modified object code on the User Product (for example, the work has
+been installed in ROM).
+
+  The requirement to provide Installation Information does not include a
+requirement to continue to provide support service, warranty, or updates
+for a work that has been modified or installed by the recipient, or for
+the User Product in which it has been modified or installed.  Access to a
+network may be denied when the modification itself materially and
+adversely affects the operation of the network or violates the rules and
+protocols for communication across the network.
+
+  Corresponding Source conveyed, and Installation Information provided,
+in accord with this section must be in a format that is publicly
+documented (and with an implementation available to the public in
+source code form), and must require no special password or key for
+unpacking, reading or copying.
+
+  7. Additional Terms.
+
+  "Additional permissions" are terms that supplement the terms of this
+License by making exceptions from one or more of its conditions.
+Additional permissions that are applicable to the entire Program shall
+be treated as though they were included in this License, to the extent
+that they are valid under applicable law.  If additional permissions
+apply only to part of the Program, that part may be used separately
+under those permissions, but the entire Program remains governed by
+this License without regard to the additional permissions.
+
+  When you convey a copy of a covered work, you may at your option
+remove any additional permissions from that copy, or from any part of
+it.  (Additional permissions may be written to require their own
+removal in certain cases when you modify the work.)  You may place
+additional permissions on material, added by you to a covered work,
+for which you have or can give appropriate copyright permission.
+
+  Notwithstanding any other provision of this License, for material you
+add to a covered work, you may (if authorized by the copyright holders of
+that material) supplement the terms of this License with terms:
+
+    a) Disclaiming warranty or limiting liability differently from the
+    terms of sections 15 and 16 of this License; or
+
+    b) Requiring preservation of specified reasonable legal notices or
+    author attributions in that material or in the Appropriate Legal
+    Notices displayed by works containing it; or
+
+    c) Prohibiting misrepresentation of the origin of that material, or
+    requiring that modified versions of such material be marked in
+    reasonable ways as different from the original version; or
+
+    d) Limiting the use for publicity purposes of names of licensors or
+    authors of the material; or
+
+    e) Declining to grant rights under trademark law for use of some
+    trade names, trademarks, or service marks; or
+
+    f) Requiring indemnification of licensors and authors of that
+    material by anyone who conveys the material (or modified versions of
+    it) with contractual assumptions of liability to the recipient, for
+    any liability that these contractual assumptions directly impose on
+    those licensors and authors.
+
+  All other non-permissive additional terms are considered "further
+restrictions" within the meaning of section 10.  If the Program as you
+received it, or any part of it, contains a notice stating that it is
+governed by this License along with a term that is a further
+restriction, you may remove that term.  If a license document contains
+a further restriction but permits relicensing or conveying under this
+License, you may add to a covered work material governed by the terms
+of that license document, provided that the further restriction does
+not survive such relicensing or conveying.
+
+  If you add terms to a covered work in accord with this section, you
+must place, in the relevant source files, a statement of the
+additional terms that apply to those files, or a notice indicating
+where to find the applicable terms.
+
+  Additional terms, permissive or non-permissive, may be stated in the
+form of a separately written license, or stated as exceptions;
+the above requirements apply either way.
+
+  8. Termination.
+
+  You may not propagate or modify a covered work except as expressly
+provided under this License.  Any attempt otherwise to propagate or
+modify it is void, and will automatically terminate your rights under
+this License (including any patent licenses granted under the third
+paragraph of section 11).
+
+  However, if you cease all violation of this License, then your
+license from a particular copyright holder is reinstated (a)
+provisionally, unless and until the copyright holder explicitly and
+finally terminates your license, and (b) permanently, if the copyright
+holder fails to notify you of the violation by some reasonable means
+prior to 60 days after the cessation.
+
+  Moreover, your license from a particular copyright holder is
+reinstated permanently if the copyright holder notifies you of the
+violation by some reasonable means, this is the first time you have
+received notice of violation of this License (for any work) from that
+copyright holder, and you cure the violation prior to 30 days after
+your receipt of the notice.
+
+  Termination of your rights under this section does not terminate the
+licenses of parties who have received copies or rights from you under
+this License.  If your rights have been terminated and not permanently
+reinstated, you do not qualify to receive new licenses for the same
+material under section 10.
+
+  9. Acceptance Not Required for Having Copies.
+
+  You are not required to accept this License in order to receive or
+run a copy of the Program.  Ancillary propagation of a covered work
+occurring solely as a consequence of using peer-to-peer transmission
+to receive a copy likewise does not require acceptance.  However,
+nothing other than this License grants you permission to propagate or
+modify any covered work.  These actions infringe copyright if you do
+not accept this License.  Therefore, by modifying or propagating a
+covered work, you indicate your acceptance of this License to do so.
+
+  10. Automatic Licensing of Downstream Recipients.
+
+  Each time you convey a covered work, the recipient automatically
+receives a license from the original licensors, to run, modify and
+propagate that work, subject to this License.  You are not responsible
+for enforcing compliance by third parties with this License.
+
+  An "entity transaction" is a transaction transferring control of an
+organization, or substantially all assets of one, or subdividing an
+organization, or merging organizations.  If propagation of a covered
+work results from an entity transaction, each party to that
+transaction who receives a copy of the work also receives whatever
+licenses to the work the party's predecessor in interest had or could
+give under the previous paragraph, plus a right to possession of the
+Corresponding Source of the work from the predecessor in interest, if
+the predecessor has it or can get it with reasonable efforts.
+
+  You may not impose any further restrictions on the exercise of the
+rights granted or affirmed under this License.  For example, you may
+not impose a license fee, royalty, or other charge for exercise of
+rights granted under this License, and you may not initiate litigation
+(including a cross-claim or counterclaim in a lawsuit) alleging that
+any patent claim is infringed by making, using, selling, offering for
+sale, or importing the Program or any portion of it.
+
+  11. Patents.
+
+  A "contributor" is a copyright holder who authorizes use under this
+License of the Program or a work on which the Program is based.  The
+work thus licensed is called the contributor's "contributor version".
+
+  A contributor's "essential patent claims" are all patent claims
+owned or controlled by the contributor, whether already acquired or
+hereafter acquired, that would be infringed by some manner, permitted
+by this License, of making, using, or selling its contributor version,
+but do not include claims that would be infringed only as a
+consequence of further modification of the contributor version.  For
+purposes of this definition, "control" includes the right to grant
+patent sublicenses in a manner consistent with the requirements of
+this License.
+
+  Each contributor grants you a non-exclusive, worldwide, royalty-free
+patent license under the contributor's essential patent claims, to
+make, use, sell, offer for sale, import and otherwise run, modify and
+propagate the contents of its contributor version.
+
+  In the following three paragraphs, a "patent license" is any express
+agreement or commitment, however denominated, not to enforce a patent
+(such as an express permission to practice a patent or covenant not to
+sue for patent infringement).  To "grant" such a patent license to a
+party means to make such an agreement or commitment not to enforce a
+patent against the party.
+
+  If you convey a covered work, knowingly relying on a patent license,
+and the Corresponding Source of the work is not available for anyone
+to copy, free of charge and under the terms of this License, through a
+publicly available network server or other readily accessible means,
+then you must either (1) cause the Corresponding Source to be so
+available, or (2) arrange to deprive yourself of the benefit of the
+patent license for this particular work, or (3) arrange, in a manner
+consistent with the requirements of this License, to extend the patent
+license to downstream recipients.  "Knowingly relying" means you have
+actual knowledge that, but for the patent license, your conveying the
+covered work in a country, or your recipient's use of the covered work
+in a country, would infringe one or more identifiable patents in that
+country that you have reason to believe are valid.
+
+  If, pursuant to or in connection with a single transaction or
+arrangement, you convey, or propagate by procuring conveyance of, a
+covered work, and grant a patent license to some of the parties
+receiving the covered work authorizing them to use, propagate, modify
+or convey a specific copy of the covered work, then the patent license
+you grant is automatically extended to all recipients of the covered
+work and works based on it.
+
+  A patent license is "discriminatory" if it does not include within
+the scope of its coverage, prohibits the exercise of, or is
+conditioned on the non-exercise of one or more of the rights that are
+specifically granted under this License.  You may not convey a covered
+work if you are a party to an arrangement with a third party that is
+in the business of distributing software, under which you make payment
+to the third party based on the extent of your activity of conveying
+the work, and under which the third party grants, to any of the
+parties who would receive the covered work from you, a discriminatory
+patent license (a) in connection with copies of the covered work
+conveyed by you (or copies made from those copies), or (b) primarily
+for and in connection with specific products or compilations that
+contain the covered work, unless you entered into that arrangement,
+or that patent license was granted, prior to 28 March 2007.
+
+  Nothing in this License shall be construed as excluding or limiting
+any implied license or other defenses to infringement that may
+otherwise be available to you under applicable patent law.
+
+  12. No Surrender of Others' Freedom.
+
+  If conditions are imposed on you (whether by court order, agreement or
+otherwise) that contradict the conditions of this License, they do not
+excuse you from the conditions of this License.  If you cannot convey a
+covered work so as to satisfy simultaneously your obligations under this
+License and any other pertinent obligations, then as a consequence you may
+not convey it at all.  For example, if you agree to terms that obligate you
+to collect a royalty for further conveying from those to whom you convey
+the Program, the only way you could satisfy both those terms and this
+License would be to refrain entirely from conveying the Program.
+
+  13. Use with the GNU Affero General Public License.
+
+  Notwithstanding any other provision of this License, you have
+permission to link or combine any covered work with a work licensed
+under version 3 of the GNU Affero General Public License into a single
+combined work, and to convey the resulting work.  The terms of this
+License will continue to apply to the part which is the covered work,
+but the special requirements of the GNU Affero General Public License,
+section 13, concerning interaction through a network will apply to the
+combination as such.
+
+  14. Revised Versions of this License.
+
+  The Free Software Foundation may publish revised and/or new versions of
+the GNU General Public License from time to time.  Such new versions will
+be similar in spirit to the present version, but may differ in detail to
+address new problems or concerns.
+
+  Each version is given a distinguishing version number.  If the
+Program specifies that a certain numbered version of the GNU General
+Public License "or any later version" applies to it, you have the
+option of following the terms and conditions either of that numbered
+version or of any later version published by the Free Software
+Foundation.  If the Program does not specify a version number of the
+GNU General Public License, you may choose any version ever published
+by the Free Software Foundation.
+
+  If the Program specifies that a proxy can decide which future
+versions of the GNU General Public License can be used, that proxy's
+public statement of acceptance of a version permanently authorizes you
+to choose that version for the Program.
+
+  Later license versions may give you additional or different
+permissions.  However, no additional obligations are imposed on any
+author or copyright holder as a result of your choosing to follow a
+later version.
+
+  15. Disclaimer of Warranty.
+
+  THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY
+APPLICABLE LAW.  EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT
+HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY
+OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,
+THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
+PURPOSE.  THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM
+IS WITH YOU.  SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF
+ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
+
+  16. Limitation of Liability.
+
+  IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
+WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS
+THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY
+GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE
+USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF
+DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD
+PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),
+EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF
+SUCH DAMAGES.
+
+  17. Interpretation of Sections 15 and 16.
+
+  If the disclaimer of warranty and limitation of liability provided
+above cannot be given local legal effect according to their terms,
+reviewing courts shall apply local law that most closely approximates
+an absolute waiver of all civil liability in connection with the
+Program, unless a warranty or assumption of liability accompanies a
+copy of the Program in return for a fee.
+
+                     END OF TERMS AND CONDITIONS
+
+            How to Apply These Terms to Your New Programs
+
+  If you develop a new program, and you want it to be of the greatest
+possible use to the public, the best way to achieve this is to make it
+free software which everyone can redistribute and change under these terms.
+
+  To do so, attach the following notices to the program.  It is safest
+to attach them to the start of each source file to most effectively
+state the exclusion of warranty; and each file should have at least
+the "copyright" line and a pointer to where the full notice is found.
+
+    <one line to give the program's name and a brief idea of what it does.>
+    Copyright (C) <year>  <name of author>
+
+    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/>.
+
+Also add information on how to contact you by electronic and paper mail.
+
+  If the program does terminal interaction, make it output a short
+notice like this when it starts in an interactive mode:
+
+    <program>  Copyright (C) <year>  <name of author>
+    This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
+    This is free software, and you are welcome to redistribute it
+    under certain conditions; type `show c' for details.
+
+The hypothetical commands `show w' and `show c' should show the appropriate
+parts of the General Public License.  Of course, your program's commands
+might be different; for a GUI interface, you would use an "about box".
+
+  You should also get your employer (if you work as a programmer) or school,
+if any, to sign a "copyright disclaimer" for the program, if necessary.
+For more information on this, and how to apply and follow the GNU GPL, see
+<http://www.gnu.org/licenses/>.
+
+  The GNU General Public License does not permit incorporating your program
+into proprietary programs.  If your program is a subroutine library, you
+may consider it more useful to permit linking proprietary applications with
+the library.  If this is what you want to do, use the GNU Lesser General
+Public License instead of this License.  But first, please read
+<http://www.gnu.org/philosophy/why-not-lgpl.html>.
diff --git a/PKG-INFO b/PKG-INFO
new file mode 100644
index 0000000..96f5a6d
--- /dev/null
+++ b/PKG-INFO
@@ -0,0 +1,10 @@
+Metadata-Version: 1.0
+Name: chirp
+Version: 0.3.0
+Summary: UNKNOWN
+Home-page: UNKNOWN
+Author: UNKNOWN
+Author-email: UNKNOWN
+License: UNKNOWN
+Description: UNKNOWN
+Platform: UNKNOWN
diff --git a/chirp.xsd b/chirp.xsd
new file mode 100644
index 0000000..ff0d93c
--- /dev/null
+++ b/chirp.xsd
@@ -0,0 +1,47 @@
+<!-- 
+
+     CHIRP XML Schema
+     Copyright 2008 Dan Smith <dsmith at danplanet.com>
+
+  -->
+
+<xsd:schema xmlns:xsd="http://www.w3.org/2001/XMLSchema">
+
+  <xsd:include schemaLocation="chirp_memory.xsd"/>
+  <xsd:include schemaLocation="chirp_banks.xsd"/>
+
+  <xsd:element name="radio" type="radioType"/>
+
+  <xsd:complexType name="radioType">
+    <xsd:sequence>
+      <xsd:element name="comment" type="xsd:string"
+		   minOccurs="0" maxOccurs="1"/>
+      <xsd:element name="memories" type="memoryList"
+		   minOccurs="1" maxOccurs="1"/>
+      <xsd:element name="banks" type="bankList"
+		   minOccurs="1" maxOccurs="1"/>
+    </xsd:sequence>
+    <xsd:attribute name="version" type="chirpSchemaVersionType"/>
+  </xsd:complexType>
+
+  <xsd:complexType name="memoryList">
+    <xsd:sequence>
+      <xsd:element name="memory" type="memoryType"
+		   minOccurs="0" maxOccurs="unbounded"/>
+    </xsd:sequence>
+  </xsd:complexType>
+
+  <xsd:complexType name="bankList">
+    <xsd:sequence>
+      <xsd:element name="bank" type="bankType"
+		   minOccurs="0" maxOccurs="unbounded"/>
+    </xsd:sequence>
+  </xsd:complexType>
+
+  <xsd:simpleType name="chirpSchemaVersionType">
+    <xsd:restriction base="xsd:string">
+      <xsd:pattern value="[0-9][0-9]*.[0-9][0-9]*.[0-9]{1,4}"/>
+    </xsd:restriction>
+  </xsd:simpleType>
+      
+</xsd:schema>
diff --git a/chirp/__init__.py b/chirp/__init__.py
new file mode 100644
index 0000000..8bd6d99
--- /dev/null
+++ b/chirp/__init__.py
@@ -0,0 +1,27 @@
+# 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/>.
+
+CHIRP_VERSION="0.3.0"
+
+import os
+import sys
+from glob import glob
+
+module_dir = os.path.dirname(sys.modules["chirp"].__file__)
+__all__ = []
+for i in glob(os.path.join(module_dir, "*.py")):
+    name = os.path.basename(i)[:-3]
+    if not name.startswith("__"):
+        __all__.append(name)
diff --git a/chirp/alinco.py b/chirp/alinco.py
new file mode 100644
index 0000000..a92e6ab
--- /dev/null
+++ b/chirp/alinco.py
@@ -0,0 +1,549 @@
+# Copyright 2011 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/>.
+
+from chirp import chirp_common, bitwise, memmap, errors, directory, util
+from chirp.settings import RadioSettingGroup, RadioSetting
+from chirp.settings import RadioSettingValueBoolean
+
+import time
+
+DRX35_MEM_FORMAT = """
+#seekto 0x0120;
+u8 used_flags[25];
+
+#seekto 0x0200;
+struct {
+  u8 new_used:1,
+     unknown1:1,
+     isnarrow:1,
+     isdigital:1,
+     ishigh:1,
+     unknown2:3;
+  u8 unknown3:6,
+     duplex:2;
+  u8 unknown4:4,
+     tmode:4;
+  u8 unknown5:4,
+     step:4;
+  bbcd freq[4];
+  u8 unknown6[1];
+  bbcd offset[3];
+  u8 rtone;
+  u8 ctone;
+  u8 dtcs_tx;
+  u8 dtcs_rx;
+  u8 name[7];
+  u8 unknown8[2];
+  u8 unknown9:6,
+     power:2;
+  u8 unknownA[6];
+} memory[100];
+
+#seekto 0x0130;
+u8 skips[25];
+"""
+
+# 0000 0111
+# 0000 0010
+
+# Response length is:
+# 1. \r\n
+# 2. Four-digit address, followed by a colon
+# 3. 16 bytes in hex (32 characters)
+# 4. \r\n
+RLENGTH = 2 + 5 + 32 + 2
+
+STEPS = [5.0, 10.0, 12.5, 15.0, 20.0, 25.0, 30.0]
+
+def isascii(data):
+    for byte in data:
+        if (ord(byte) < ord(" ") or ord(byte) > ord("~")) and \
+                byte not in "\r\n":
+            return False
+    return True
+
+def tohex(data):
+    if isascii(data):
+        return repr(data)
+    string = ""
+    for byte in data:
+        string += "%02X" % ord(byte)
+    return string
+
+class AlincoStyleRadio(chirp_common.CloneModeRadio):
+    """Base class for all known Alinco radios"""
+    _memsize = 0
+    _model = "NONE"
+
+    def _send(self, data):
+        print "PC->R: (%2i) %s" % (len(data), tohex(data))
+        self.pipe.write(data)
+        self.pipe.read(len(data))
+
+    def _read(self, length):
+        data = self.pipe.read(length)
+        print "R->PC: (%2i) %s" % (len(data), tohex(data))
+        return data
+
+    def _download_chunk(self, addr):
+        if addr % 16:
+            raise Exception("Addr 0x%04x not on 16-byte boundary" % addr)
+
+        cmd = "AL~F%04XR\r\n" % addr
+        self._send(cmd)
+
+        resp = self._read(RLENGTH).strip()
+        if len(resp) == 0:
+            raise errors.RadioError("No response from radio")
+        if ":" not in resp:
+            raise errors.RadioError("Unexpected response from radio")
+        addr, _data = resp.split(":", 1)
+        data = ""
+        for i in range(0, len(_data), 2):
+            data += chr(int(_data[i:i+2], 16))
+
+        if len(data) != 16:
+            print "Response was:"
+            print "|%s|"
+            print "Which I converted to:"
+            print util.hexprint(data)
+            raise Exception("Radio returned less than 16 bytes")
+
+        return data
+
+    def _download(self, limit):
+        self._identify()
+
+        data = ""
+        for addr in range(0, limit, 16):
+            data += self._download_chunk(addr)
+            time.sleep(0.1)
+
+            if self.status_fn:
+                status = chirp_common.Status()
+                status.cur = addr + 16
+                status.max = self._memsize
+                status.msg = "Downloading from radio"
+                self.status_fn(status)
+
+        self._send("AL~E\r\n")
+        self._read(20)
+
+        return memmap.MemoryMap(data)
+
+    def _identify(self):
+        for _i in range(0, 3):
+            self._send("%s\r\n" % self._model)
+            resp = self._read(6)
+            if resp.strip() == "OK":
+                return True
+            time.sleep(1)
+
+        return False
+
+    def _upload_chunk(self, addr):
+        if addr % 16:
+            raise Exception("Addr 0x%04x not on 16-byte boundary" % addr)
+
+        _data = self._mmap[addr:addr+16]
+        data = "".join(["%02X" % ord(x) for x in _data])
+
+        cmd = "AL~F%04XW%s\r\n" % (addr, data)
+        self._send(cmd)
+
+    def _upload(self, limit):
+        if not self._identify():
+            raise Exception("I can't talk to this model")
+
+        for addr in range(0x100, limit, 16):
+            self._upload_chunk(addr)
+            time.sleep(0.1)
+
+            if self.status_fn:
+                status = chirp_common.Status()
+                status.cur = addr + 16
+                status.max = self._memsize
+                status.msg = "Uploading to radio"
+                self.status_fn(status)
+
+        self._send("AL~E\r\n")
+        self.pipe._read(20)
+
+    def process_mmap(self):
+        self._memobj = bitwise.parse(DRX35_MEM_FORMAT, self._mmap)
+
+    def sync_in(self):
+        try:
+            self._mmap = self._download(self._memsize)
+        except errors.RadioError:
+            raise
+        except Exception, e:
+            raise errors.RadioError("Failed to communicate with radio: %s" % e)
+        self.process_mmap()
+
+    def sync_out(self):
+        try:
+            self._upload(self._memsize)
+        except errors.RadioError:
+            raise
+        except Exception, e:
+            raise errors.RadioError("Failed to communicate with radio: %s" % e)
+
+    def get_raw_memory(self, number):
+        return repr(self._memobj.memory[number])
+
+DUPLEX = ["", "-", "+"]
+TMODES = ["", "Tone", "", "TSQL"] + [""] * 12
+TMODES[12] = "DTCS"
+DCS_CODES = {
+    "Alinco" : chirp_common.DTCS_CODES,
+    "Jetstream" : [17] + chirp_common.DTCS_CODES,
+}
+
+CHARSET = (["\x00"] * 0x30) + \
+    [chr(x + ord("0")) for x in range(0, 10)] + \
+    [chr(x + ord("A")) for x in range(0, 26)] + [" "] + \
+    list("\x00" * 128)
+
+def _get_name(_mem):
+    name = ""
+    for i in _mem.name:
+        if i in [0x00, 0xFF]:
+            break
+        name += CHARSET[i]
+    return name
+
+def _set_name(mem, _mem):
+    name = [0x00] * 7
+    j = 0
+    for i in range(0, 7):
+        try:
+            name[j] = CHARSET.index(mem.name[i])
+            j += 1
+        except IndexError:
+            pass
+        except ValueError:
+            pass
+    return name
+
+ALINCO_TONES = list(chirp_common.TONES)
+ALINCO_TONES.remove(159.8)
+ALINCO_TONES.remove(165.5)
+ALINCO_TONES.remove(171.3)
+ALINCO_TONES.remove(177.3)
+ALINCO_TONES.remove(183.5)
+ALINCO_TONES.remove(189.9)
+ALINCO_TONES.remove(196.6)
+ALINCO_TONES.remove(199.5)
+ALINCO_TONES.remove(206.5)
+ALINCO_TONES.remove(229.1)
+ALINCO_TONES.remove(254.1)
+
+class DRx35Radio(AlincoStyleRadio):
+    """Base class for the DR-x35 radios"""
+    _range = [(118000000, 155000000)]
+    _power_levels = []
+    _valid_tones = ALINCO_TONES
+
+    def get_features(self):
+        rf = chirp_common.RadioFeatures()
+        rf.valid_tmodes = ["", "Tone", "TSQL", "DTCS"]
+        rf.valid_modes = ["FM", "NFM"]
+        rf.valid_skips = ["", "S"]
+        rf.valid_bands = self._range
+        rf.memory_bounds = (0, 99)
+        rf.has_ctone = True
+        rf.has_bank = False
+        rf.has_dtcs_polarity = False
+        rf.valid_tuning_steps = STEPS
+        rf.valid_name_length = 7
+        rf.valid_power_levels = self._power_levels
+        return rf
+
+    def _get_used(self, number):
+        _usd = self._memobj.used_flags[number / 8]
+        bit = (0x80 >> (number % 8))
+        return _usd & bit
+
+    def _set_used(self, number, is_used):
+        _usd = self._memobj.used_flags[number / 8]
+        bit = (0x80 >> (number % 8))
+        if is_used:
+            _usd |= bit
+        else:
+            _usd &= ~bit
+
+    def _get_power(self, _mem):
+        if self._power_levels:
+            return self._power_levels[_mem.ishigh]
+        return None
+
+    def _set_power(self, _mem, mem):
+        if self._power_levels:
+            _mem.ishigh = mem.power is None or \
+                mem.power == self._power_levels[1]
+
+    def _get_extra(self, _mem, mem):
+        mem.extra = RadioSettingGroup("extra", "Extra")
+        dig = RadioSetting("isdigital", "Digital",
+                           RadioSettingValueBoolean(bool(_mem.isdigital)))
+        dig.set_doc("Digital/Packet mode enabled")
+        mem.extra.append(dig)
+
+    def _set_extra(self, _mem, mem):
+        for setting in mem.extra:
+            setattr(_mem, setting.get_name(), setting.value)
+
+    def get_memory(self, number):
+        _mem = self._memobj.memory[number]
+        _skp = self._memobj.skips[number / 8]
+        _usd = self._memobj.used_flags[number / 8]
+        bit = (0x80 >> (number % 8))
+
+        mem = chirp_common.Memory()
+        mem.number = number
+        if not self._get_used(number) and self.MODEL != "JT220M":
+            mem.empty = True
+            return mem
+
+        mem.freq = int(_mem.freq) * 100
+        mem.rtone = self._valid_tones[_mem.rtone]
+        mem.ctone = self._valid_tones[_mem.ctone]
+        mem.duplex = DUPLEX[_mem.duplex]
+        mem.offset = int(_mem.offset) * 100
+        mem.tmode = TMODES[_mem.tmode]
+        mem.dtcs = DCS_CODES[self.VENDOR][_mem.dtcs_tx]
+        mem.tuning_step = STEPS[_mem.step]
+
+        if _mem.isnarrow:
+            mem.mode = "NFM"
+
+        mem.power = self._get_power(_mem)
+
+        if _skp & bit:
+            mem.skip = "S"
+
+        mem.name = _get_name(_mem).rstrip()
+
+        self._get_extra(_mem, mem)
+
+        return mem
+
+    def set_memory(self, mem):
+        _mem = self._memobj.memory[mem.number]
+        _skp = self._memobj.skips[mem.number / 8]
+        _usd = self._memobj.used_flags[mem.number / 8]
+        bit = (0x80 >> (mem.number % 8))
+
+        if self._get_used(mem.number) and not mem.empty:
+            # Initialize the memory
+            _mem.set_raw("\x00" * 32)
+
+        self._set_used(mem.number, not mem.empty)
+        if mem.empty:
+            return
+
+        _mem.freq = mem.freq / 100
+
+        try:
+            _tone = mem.rtone
+            _mem.rtone = self._valid_tones.index(mem.rtone)
+            _tone = mem.ctone
+            _mem.ctone = self._valid_tones.index(mem.ctone)
+        except ValueError:
+            raise errors.UnsupportedToneError("This radio does not support " +
+                                              "tone %.1fHz" % _tone)
+
+        _mem.duplex = DUPLEX.index(mem.duplex)
+        _mem.offset = mem.offset / 100
+        _mem.tmode = TMODES.index(mem.tmode)
+        _mem.dtcs_tx = DCS_CODES[self.VENDOR].index(mem.dtcs)
+        _mem.dtcs_rx = DCS_CODES[self.VENDOR].index(mem.dtcs)
+        _mem.step = STEPS.index(mem.tuning_step)
+
+        _mem.isnarrow = mem.mode == "NFM"
+        self._set_power(_mem, mem)
+
+        if mem.skip:
+            _skp |= bit
+        else:
+            _skp &= ~bit
+
+        _mem.name = _set_name(mem, _mem)
+
+        self._set_extra(_mem, mem)
+
+ at directory.register
+class DR03Radio(DRx35Radio):
+    """Alinco DR03"""
+    VENDOR = "Alinco"
+    MODEL = "DR03T"
+
+    _model = "DR135"
+    _memsize = 4096
+    _range = [(28000000, 29695000)]
+
+    @classmethod
+    def match_model(cls, filedata, filename):
+        return len(filedata) == cls._memsize and \
+            filedata[0x64] == chr(0x00) and filedata[0x65] == chr(0x28)
+
+ at directory.register
+class DR06Radio(DRx35Radio):
+    """Alinco DR06"""
+    VENDOR = "Alinco"
+    MODEL = "DR06T"
+
+    _model = "DR435"
+    _memsize = 4096
+    _range = [(50000000, 53995000)]
+
+    @classmethod
+    def match_model(cls, filedata, filename):
+        return len(filedata) == cls._memsize and \
+            filedata[0x64] == chr(0x00) and filedata[0x65] == chr(0x50)
+            
+ at directory.register
+class DR135Radio(DRx35Radio):
+    """Alinco DR135"""
+    VENDOR = "Alinco"
+    MODEL = "DR135T"
+
+    _model = "DR135"
+    _memsize = 4096
+    _range = [(118000000, 173000000)]
+
+    @classmethod
+    def match_model(cls, filedata, filename):
+        return len(filedata) == cls._memsize and \
+            filedata[0x64] == chr(0x01) and filedata[0x65] == chr(0x44)
+
+ at directory.register
+class DR235Radio(DRx35Radio):
+    """Alinco DR235"""
+    VENDOR = "Alinco"
+    MODEL = "DR235T"
+
+    _model = "DR235"
+    _memsize = 4096
+    _range = [(216000000, 280000000)]
+
+    @classmethod
+    def match_model(cls, filedata, filename):
+        return len(filedata) == cls._memsize and \
+            filedata[0x64] == chr(0x02) and filedata[0x65] == chr(0x22)
+
+ at directory.register
+class DR435Radio(DRx35Radio):
+    """Alinco DR435"""
+    VENDOR = "Alinco"
+    MODEL = "DR435T"
+
+    _model = "DR435"
+    _memsize = 4096
+    _range = [(350000000, 511000000)]
+
+    @classmethod
+    def match_model(cls, filedata, filename):
+        return len(filedata) == cls._memsize and \
+            filedata[0x64] == chr(0x04) and filedata[0x65] == chr(0x00)
+
+ at directory.register
+class DJ596Radio(DRx35Radio):
+    """Alinco DJ596"""
+    VENDOR = "Alinco"
+    MODEL = "DJ596"
+
+    _model = "DJ596"
+    _memsize = 4096
+    _range = [(136000000, 174000000), (400000000, 511000000)]
+    _power_levels = [chirp_common.PowerLevel("Low", watts=1.00),
+                     chirp_common.PowerLevel("High", watts=5.00)]
+
+    @classmethod
+    def match_model(cls, filedata, filename):
+        return len(filedata) == cls._memsize and \
+            filedata[0x64] == chr(0x45) and filedata[0x65] == chr(0x01)
+
+ at directory.register
+class JT220MRadio(DRx35Radio):
+    """Jetstream JT220"""
+    VENDOR = "Jetstream"
+    MODEL = "JT220M"
+
+    _model = "DR136"
+    _memsize = 8192
+    _range = [(216000000, 280000000)]
+
+    @classmethod
+    def match_model(cls, filedata, filename):
+        return len(filedata) == cls._memsize and \
+            filedata[0x60:0x64] == "2009"
+
+ at directory.register
+class DJ175Radio(DRx35Radio):
+    """Alinco DJ175"""
+    VENDOR = "Alinco"
+    MODEL = "DJ175"
+
+    _model = "DJ175"
+    _memsize = 6896
+    _range = [(136000000, 174000000), (400000000, 511000000)]
+    _power_levels = [
+        chirp_common.PowerLevel("Low", watts=0.50),
+        chirp_common.PowerLevel("Mid", watts=2.00),
+        chirp_common.PowerLevel("High", watts=5.00),
+        ]
+
+    @classmethod
+    def match_model(cls, filedata, filename):
+        return len(filedata) == cls._memsize
+
+    def _get_used(self, number):
+        return self._memobj.memory[number].new_used
+
+    def _set_used(self, number, is_used):
+        self._memobj.memory[number].new_used = is_used
+
+    def _get_power(self, _mem):
+        return self._power_levels[_mem.power]
+
+    def _set_power(self, _mem, mem):
+        if mem.power in self._power_levels:
+            _mem.power = self._power_levels.index(mem.power)
+
+    def _download_chunk(self, addr):
+        if addr % 16:
+            raise Exception("Addr 0x%04x not on 16-byte boundary" % addr)
+
+        cmd = "AL~F%04XR\r\n" % addr
+        self._send(cmd)
+
+        _data = self._read(34).strip()
+        if len(_data) == 0:
+            raise errors.RadioError("No response from radio")
+
+        data = ""
+        for i in range(0, len(_data), 2):
+            data += chr(int(_data[i:i+2], 16))
+
+        if len(data) != 16:
+            print "Response was:"
+            print "|%s|"
+            print "Which I converted to:"
+            print util.hexprint(data)
+            raise Exception("Radio returned less than 16 bytes")
+
+        return data
diff --git a/chirp/baofeng_uv3r.py b/chirp/baofeng_uv3r.py
new file mode 100644
index 0000000..19d6921
--- /dev/null
+++ b/chirp/baofeng_uv3r.py
@@ -0,0 +1,292 @@
+# Copyright 2011 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/>.
+
+"""Baofeng UV3r radio management module"""
+
+import time
+import os
+from chirp import util, chirp_common, bitwise, errors, directory
+from chirp.wouxun_common import do_download, do_upload
+
+if os.getenv("CHIRP_DEBUG"):
+    DEBUG = True
+else:
+    DEBUG = False
+
+def _uv3r_prep(radio):
+    radio.pipe.write("\x05PROGRAM")
+    ack = radio.pipe.read(1)
+    if ack != "\x06":
+        raise errors.RadioError("Radio did not ACK first command")
+
+    radio.pipe.write("\x02")
+    ident = radio.pipe.read(8)
+    if len(ident) != 8:
+        print util.hexprint(ident)
+        raise errors.RadioError("Radio did not send identification")
+
+    radio.pipe.write("\x06")
+    if radio.pipe.read(1) != "\x06":
+        raise errors.RadioError("Radio did not ACK ident")
+
+def uv3r_prep(radio):
+    """Do the UV3R identification dance"""
+    for _i in range(0, 10):
+        try:
+            return _uv3r_prep(radio)
+        except errors.RadioError, e:
+            time.sleep(1)
+
+    raise e
+
+def uv3r_download(radio):
+    """Talk to a UV3R and do a download"""
+    try:
+        uv3r_prep(radio)
+        return do_download(radio, 0x0000, 0x0E40, 0x0010)
+    except errors.RadioError:
+        raise
+    except Exception, e:
+        raise errors.RadioError("Failed to communicate with radio: %s" % e)
+
+def uv3r_upload(radio):
+    """Talk to a UV3R and do an upload"""
+    try:
+        uv3r_prep(radio)
+        return do_upload(radio, 0x0000, 0x0E40, 0x0010)
+    except errors.RadioError:
+        raise
+    except Exception, e:
+        raise errors.RadioError("Failed to communicate with radio: %s" % e)
+
+UV3R_MEM_FORMAT = """
+#seekto 0x0010;
+struct {
+  lbcd rx_freq[4];
+  u8 rxtone;
+  lbcd offset[4];
+  u8 txtone;
+  u8 ishighpower:1,
+     iswide:1,
+     dtcsinvt:1,
+     unknown1:1,
+     dtcsinvr:1,
+     unknown2:1,
+     duplex:2;
+  u8 unknown;
+  lbcd tx_freq[4];
+} tx_memory[99];
+#seekto 0x0810;
+struct {
+  lbcd rx_freq[4];
+  u8 rxtone;
+  lbcd offset[4];
+  u8 txtone;
+  u8 ishighpower:1,
+     iswide:1,
+     dtcsinvt:1,
+     unknown1:1,
+     dtcsinvr:1,
+     unknown2:1,
+     duplex:2;
+  u8 unknown;
+  lbcd tx_freq[4];
+} rx_memory[99];
+
+#seekto 0x1008;
+struct {
+  u8 unknown[8];
+  u8 name[6];
+  u8 pad[2];
+} names[128];
+"""
+
+UV3R_DUPLEX = ["", "-", "+", ""]
+UV3R_POWER_LEVELS = [chirp_common.PowerLevel("High", watts=2.00),
+                     chirp_common.PowerLevel("Low", watts=0.50)]
+UV3R_DTCS_POL = ["NN", "NR", "RN", "RR"]
+
+ at directory.register
+class UV3RRadio(chirp_common.CloneModeRadio):
+    """Baofeng UV-3R"""
+    VENDOR = "Baofeng"
+    MODEL = "UV-3R"
+
+    def get_features(self):
+        rf = chirp_common.RadioFeatures()
+        rf.valid_tmodes = ["", "Tone", "TSQL", "DTCS", "Cross"]
+        rf.valid_modes = ["FM", "NFM"]
+        rf.valid_power_levels = UV3R_POWER_LEVELS
+        rf.valid_bands = [(136000000, 174000000), (400000000, 470000000)]
+        rf.valid_skips = []
+        rf.valid_duplexes = ["", "-", "+", "split"]
+        rf.valid_cross_modes = ["Tone->Tone", "Tone->DTCS", "DTCS->Tone",
+                                "->Tone", "->DTCS"]
+        rf.has_ctone = True
+        rf.has_cross = True
+        rf.has_tuning_step = False
+        rf.has_bank = False
+        rf.has_name = False
+        rf.can_odd_split = True
+        rf.memory_bounds = (1, 99)
+        return rf
+
+    def sync_in(self):
+        self._mmap = uv3r_download(self)
+        self.process_mmap()
+
+    def sync_out(self):
+        uv3r_upload(self)
+
+    def process_mmap(self):
+        self._memobj = bitwise.parse(UV3R_MEM_FORMAT, self._mmap)
+
+    def get_memory(self, number):
+        _mem = self._memobj.rx_memory[number - 1]
+        mem = chirp_common.Memory()
+        mem.number = number
+
+        if _mem.get_raw()[0] == "\xff":
+            mem.empty = True
+            return mem
+
+        mem.freq = int(_mem.rx_freq) * 10
+        mem.offset = int(_mem.offset) * 10
+        mem.duplex = UV3R_DUPLEX[_mem.duplex]
+        if mem.offset > 60000000:
+            if mem.duplex == "+":
+                mem.offset = mem.freq + mem.offset
+            elif mem.duplex == "-":
+                mem.offset = mem.freq - mem.offset
+            mem.duplex = "split"
+        mem.power = UV3R_POWER_LEVELS[1 - _mem.ishighpower]
+        if not _mem.iswide:
+            mem.mode = "NFM"
+
+        dtcspol = (int(_mem.dtcsinvt) << 1) + _mem.dtcsinvr
+        mem.dtcs_polarity = UV3R_DTCS_POL[dtcspol]
+
+        if _mem.txtone in [0, 0xFF]:
+            txmode = ""
+        elif _mem.txtone < 0x33:
+            mem.rtone = chirp_common.TONES[_mem.txtone - 1]
+            txmode = "Tone"
+        elif _mem.txtone >= 0x33:
+            tcode = chirp_common.DTCS_CODES[_mem.txtone - 0x33]
+            mem.dtcs = tcode
+            txmode = "DTCS"
+        else:
+            print "Bug: tx_mode is %02x" % _mem.txtone
+
+        if _mem.rxtone in [0, 0xFF]:
+            rxmode = ""
+        elif _mem.rxtone < 0x33:
+            mem.ctone = chirp_common.TONES[_mem.rxtone - 1]
+            rxmode = "Tone"
+        elif _mem.rxtone >= 0x33:
+            rcode = chirp_common.DTCS_CODES[_mem.rxtone - 0x33]
+            mem.dtcs = rcode
+            rxmode = "DTCS"
+        else:
+            print "Bug: rx_mode is %02x" % _mem.rxtone
+
+        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":
+            mem.tmode = "DTCS"
+        elif rxmode or txmode:
+            mem.tmode = "Cross"
+            mem.cross_mode = "%s->%s" % (txmode, rxmode)
+
+        return mem
+
+    def _set_tone(self, _mem, which, value, mode):
+        if mode == "Tone":
+            val = chirp_common.TONES.index(value) + 1
+        elif mode == "DTCS":
+            val = chirp_common.DTCS_CODES.index(value) + 0x33
+        elif mode == "":
+            val = 0
+        else:
+            raise errors.RadioError("Internal error: tmode %s" % mode)
+
+        setattr(_mem, which, val)
+
+    def _set_memory(self, mem, _mem):
+        if mem.empty:
+            _mem.set_raw("\xff" * 16)
+            return
+
+        _mem.rx_freq = mem.freq / 10
+        if mem.duplex == "split":
+            diff = mem.freq - mem.offset
+            _mem.offset = abs(diff) / 10
+            _mem.duplex = UV3R_DUPLEX.index(diff < 0 and "+" or "-")
+            for i in range(0, 4):
+                _mem.tx_freq[i].set_raw("\xFF")
+        else:
+            _mem.offset = mem.offset / 10
+            _mem.duplex = UV3R_DUPLEX.index(mem.duplex)
+            _mem.tx_freq = (mem.freq + mem.offset) / 10
+
+        _mem.ishighpower = mem.power == UV3R_POWER_LEVELS[0]
+        _mem.iswide = mem.mode == "FM"
+
+        _mem.dtcsinvt = mem.dtcs_polarity[0] == "R"
+        _mem.dtcsinvr = mem.dtcs_polarity[1] == "R"
+
+        rxtone = txtone = 0
+        rxmode = txmode = ""
+
+        if mem.tmode == "DTCS":
+            rxmode = txmode = "DTCS"
+            rxtone = txtone = mem.dtcs
+        elif mem.tmode and mem.tmode != "Cross":
+            rxtone = txtone = mem.tmode == "Tone" and mem.rtone or mem.ctone
+            txmode = "Tone"
+            rxmode = mem.tmode == "TSQL" and "Tone" or ""
+        elif mem.tmode == "Cross":
+            txmode, rxmode = mem.cross_mode.split("->", 1)
+
+            if txmode == "DTCS":
+                txtone = mem.dtcs
+            elif txmode == "Tone":
+                txtone = mem.rtone
+
+            if rxmode == "DTCS":
+                rxtone = mem.dtcs
+            elif rxmode == "Tone":
+                rxtone = mem.ctone
+
+        self._set_tone(_mem, "txtone", txtone, txmode)
+        self._set_tone(_mem, "rxtone", rxtone, rxmode)
+
+    def set_memory(self, mem):
+        _tmem = self._memobj.tx_memory[mem.number - 1]
+        _rmem = self._memobj.rx_memory[mem.number - 1]
+
+        self._set_memory(mem, _tmem)
+        self._set_memory(mem, _rmem)
+
+    @classmethod
+    def match_model(cls, filedata, filename):
+        return len(filedata) == 3648
+
+    def get_raw_memory(self, number):
+        _rmem = self._memobj.tx_memory[number - 1]
+        _tmem = self._memobj.rx_memory[number - 1]
+        return repr(_rmem) + repr(_tmem)
diff --git a/chirp/bitwise.py b/chirp/bitwise.py
new file mode 100644
index 0000000..3f91c8a
--- /dev/null
+++ b/chirp/bitwise.py
@@ -0,0 +1,850 @@
+# Copyright 2010 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/>.
+
+# Language:
+#
+# Example definitions:
+#
+#  u8   foo;     /* Unsigned 8-bit value                    */
+#  u16  foo;     /* Unsigned 16-bit value                   */
+#  ul16 foo;     /* Unsigned 16-bit value (LE)              */
+#  u24  foo;     /* Unsigned 24-bit value                   */
+#  ul24 foo;     /* Unsigned 24-bit value (LE)              */
+#  u32  foo;     /* Unsigned 32-bit value                   */
+#  ul32 foo;     /* Unsigned 32-bit value (LE)              */
+#  char foo;     /* Character (single-byte                  */
+#  lbcd foo;     /* BCD-encoded byte (LE)                   */
+#  bbcd foo;     /* BCD-encoded byte (BE)                   */
+#  char foo[8];  /* 8-char array                            */
+#  struct {                                                 
+#   u8 foo;                                                 
+#   u16 bar;                                                
+#  } baz;        /* Structure with u8 and u16               */
+#
+# Example directives:
+# 
+# #seekto 0x1AB; /* Set the data offset to 0x1AB            */
+# #seek 4;       /* Set the data offset += 4                */
+# #printoffset "foobar" /* Echo the live data offset,
+#                          prefixed by string while parsing */
+#
+# Usage:
+#
+# Create a data definition in a string, and pass it and the data
+# to parse to the parse() function.  The result is a structure with 
+# dict-like objects for structures, indexed by name, and lists of
+# objects for arrays.  The actual data elements can be interpreted
+# as integers directly (for int types).  Strings and BCD arrays
+# behave as expected.
+
+import struct
+import os
+
+from chirp import bitwise_grammar
+from chirp.memmap import MemoryMap
+
+class ParseError(Exception):
+    """Indicates an error parsing a definition"""
+    pass
+
+def format_binary(nbits, value, pad=8):
+    s = ""
+    for i in range(0, nbits):
+        s = "%i%s" % (value & 0x01, s)
+        value >>= 1
+    return "%s%s" % ((pad - len(s)) * ".", s)
+
+def bits_between(start, end):
+    bits = (1 << (end - start )) - 1
+    return bits << start
+
+def pp(structure, level=0):
+    for i in structure:
+        if isinstance(i, list):
+            pp(i, level+2)
+        elif isinstance(i, tuple):
+            if isinstance(i[1], str):
+                print "%s%s: %s" % (" " * level, i[0], i[1])
+            else:
+                print "%s%s:" % (" " * level, i[0])
+                pp(i, level+2)
+        elif isinstance(i, str):
+            print "%s%s" % (" " * level, i)
+
+def array_copy(dst, src):
+    """Copy an array src into DataElement array dst"""
+    if len(dst) != len(src):
+        raise Exception("Arrays differ in size")
+
+    for i in range(0, len(dst)):
+        dst[i].set_value(src[i])
+
+def bcd_to_int(bcd_array):
+    """Convert an array of bcdDataElement like \x12\x34 into an int like 1234"""
+    value = 0
+    for bcd in bcd_array:
+        a, b = bcd.get_value()
+        value = (value * 100) + (a * 10) + b
+    return value
+        
+def int_to_bcd(bcd_array, value):
+    """Convert an int like 1234 into bcdDataElements like "\x12\x34" """
+    for i in reversed(range(0, len(bcd_array))):
+        bcd_array[i].set_value(value % 100)
+        value /= 100
+
+def get_string(char_array):
+    """Convert an array of charDataElements into a string"""
+    return "".join([x.get_value() for x in char_array])
+
+def set_string(char_array, string):
+    """Set an array of charDataElements from a string"""
+    array_copy(char_array, list(string))
+
+class DataElement:
+    _size = 1
+
+    def __init__(self, data, offset, count=1):
+        self._data = data
+        self._offset = offset
+        self._count = count
+
+    def size(self):
+        return self._size * 8
+
+    def get_offset(self):
+        return self._offset
+
+    def _get_value(self, data):
+        raise Exception("Not implemented")
+
+    def get_value(self):
+        return self._get_value(self._data[self._offset:self._offset+self._size])
+
+    def set_value(self, value):
+        raise Exception("Not implemented for %s" % self.__class__)
+
+    def get_raw(self):
+        return self._data[self._offset:self._offset+self._size]
+
+    def set_raw(self, data):
+        self._data[self._offset] = data[:self._size]
+
+    def __getattr__(self, name):
+        raise AttributeError("Unknown attribute %s in %s" % (name,
+                                                             self.__class__))
+
+    def __repr__(self):
+        return "(%s:%i bytes @ %04x)" % (self.__class__.__name__,
+                                         self._size,
+                                         self._offset)
+
+class arrayDataElement(DataElement):
+    def __repr__(self):
+        if isinstance(self.__items[0], bcdDataElement):
+            return "%i:[(%i)]" % (len(self.__items), int(self))
+
+        if isinstance(self.__items[0], charDataElement):
+            return "%i:[(%s)]" % (len(self.__items), str(self))
+
+        s = "%i:[" % len(self.__items)
+        s += ",".join([repr(item) for item in self.__items])
+        s += "]"
+        return s
+
+    def __init__(self, offset):
+        self.__items = []
+        self._offset = offset
+
+    def append(self, item):
+        self.__items.append(item)
+
+    def get_value(self):
+        return list(self.__items)
+
+    def get_raw(self):
+        return "".join([item.get_raw() for item in self.__items])
+
+    def __setitem__(self, index, val):
+        self.__items[index].set_value(val)
+
+    def __getitem__(self, index):
+        return self.__items[index]
+
+    def __len__(self):
+        return len(self.__items)
+
+    def __str__(self):
+        if isinstance(self.__items[0], charDataElement):
+            return "".join([x.get_value() for x in self.__items])
+        else:
+            return str(self.__items)
+
+    def __int__(self):
+        if isinstance(self.__items[0], bbcdDataElement):
+            val = 0
+            for i in self.__items:
+                tens, ones = i.get_value()
+                val = (val * 100) + (tens * 10) + ones
+            return val
+        elif isinstance(self.__items[0], lbcdDataElement):
+            val = 0
+            for i in reversed(self.__items):
+                ones, tens = i.get_value()
+                val = (val * 100) + (tens * 10) + ones
+            return val
+        else:
+            raise ValueError("Cannot coerce this to int")
+
+    def __set_value_bbcd(self, value):
+        for i in reversed(self.__items):
+            twodigits = value % 100
+            value /= 100
+            i.set_value(twodigits)
+
+    def __set_value_lbcd(self, value):
+        for i in self.__items:
+            twodigits = value % 100
+            value /= 100
+            i.set_value(twodigits)
+
+    def __set_value_char(self, value):
+        for i in range(0, len(self.__items)):
+            self.__items[i].set_value(value[i])
+
+    def set_value(self, value):
+        if isinstance(self.__items[0], bbcdDataElement):
+            self.__set_value_bbcd(int(value))
+        elif isinstance(self.__items[0], lbcdDataElement):
+            self.__set_value_lbcd(int(value))
+        elif isinstance(self.__items[0], charDataElement):
+            self.__set_value_char(str(value))
+        elif len(value) != len(self.__items):
+            raise ValueError("Array cardinality mismatch")
+        else:
+            for i in range(0, len(value)):
+                self.__items[i].set_value(value[i])
+
+    def index(self, value):
+        index = 0
+        for i in self.__items:
+            if i.get_value() == value:
+                return index
+            index += 1
+        raise IndexError()            
+
+    def __iter__(self):
+        return iter(self.__items)
+
+    def size(self):
+        size = 0
+        for i in self.__items:
+            size += i.size()
+        return size
+
+class intDataElement(DataElement):
+    def __repr__(self):
+        fmt = "0x%%0%iX" % (self._size * 2)
+        return fmt % int(self)
+    
+    def __int__(self):
+        return self.get_value()
+
+    def __invert__(self):
+        return ~self.get_value()
+
+    def __trunc__(self):
+        return self.get_value()
+
+    def __abs__(self):
+        return abs(self.get_value())
+
+    def __mod__(self, val):
+        return self.get_value() % val
+
+    def __mul__(self, val):
+        return self.get_value() * val
+
+    def __div__(self, val):
+        return self.get_value() / val
+
+    def __add__(self, val):
+        return self.get_value() + val
+
+    def __sub__(self, val):
+        return self.get_value() - val
+
+    def __or__(self, val):
+        return self.get_value() | val
+
+    def __and__(self, val):
+        return self.get_value() & val
+
+    def __radd__(self, val):
+        return val + self.get_value()
+
+    def __rsub__(self, val):
+        return val - self.get_value()
+
+    def __rmul__(self, val):
+        return val * self.get_value()
+
+    def __rdiv__(self, val):
+        return val / self.get_value()
+
+    def __rand__(self, val):
+        return val & self.get_value()
+
+    def __ror__(self, val):
+        return val | self.get_value()
+
+    def __rmod__(self, val):
+        return val % self.get_value()
+
+    def __lshift__(self, val):
+        return self.get_value() << val
+
+    def __rshift__(self, val):
+        return self.get_value() >> val
+
+    def __iadd__(self, val):
+        self.set_value(self.get_value() + val)
+        return self
+
+    def __isub__(self, val):
+        self.set_value(self.get_value() - val)
+        return self
+
+    def __imul__(self, val):
+        self.set_value(self.get_value() * val)
+        return self
+
+    def __idiv__(self, val):
+        self.set_value(self.get_value() / val)
+        return self
+
+    def __imod__(self, val):
+        self.set_value(self.get_value() % val)
+        return self
+
+    def __iand__(self, val):
+        self.set_value(self.get_value() & val)
+        return self
+
+    def __ior__(self, val):
+        self.set_value(self.get_value() | val)
+        return self
+
+    def __index__(self):
+        return abs(self)
+
+    def __eq__(self, val):
+        return self.get_value() == val
+
+    def __ne__(self, val):
+        return self.get_value() != val
+
+    def __lt__(self, val):
+        return self.get_value() < val
+
+    def __le__(self, val):
+        return self.get_value() <= val
+
+    def __gt__(self, val):
+        return self.get_value() > val
+
+    def __ge__(self, val):
+        return self.get_value() >= val
+
+    def __nonzero__(self):
+        return self.get_value() != 0
+
+class u8DataElement(intDataElement):
+    _size = 1
+
+    def _get_value(self, data):
+        return ord(data)
+
+    def set_value(self, value):
+        self._data[self._offset] = (int(value) & 0xFF)
+
+class u16DataElement(intDataElement):
+    _size = 2
+    _endianess = ">"
+
+    def _get_value(self, data):
+        return struct.unpack(self._endianess + "H", data)[0]
+
+    def set_value(self, value):
+        self._data[self._offset] = struct.pack(self._endianess + "H",
+                                               int(value) & 0xFFFF)
+
+class ul16DataElement(u16DataElement):
+    _endianess = "<"
+
+class u24DataElement(intDataElement):
+    _size = 3
+    _endianess = ">"
+
+    def _get_value(self, data):
+        pre = self._endianess == ">" and "\x00" or ""
+        post = self._endianess == "<" and "\x00" or ""
+        return struct.unpack(self._endianess + "I", pre+data+post)[0]
+
+    def set_value(self, value):
+        if self._endianess == "<":
+            start = 0
+            end = 3
+        else:
+            start = 1
+            end = 4
+        self._data[self._offset] = struct.pack(self._endianess + "I",
+                                               int(value) & 0xFFFFFFFF)[start:end]
+
+class ul24DataElement(u24DataElement):
+    _endianess = "<"
+
+class u32DataElement(intDataElement):
+    _size = 4
+    _endianess = ">"
+
+    def _get_value(self, data):
+        return struct.unpack(self._endianess + "I", data)[0]
+
+    def set_value(self, value):
+        self._data[self._offset] = struct.pack(self._endianess + "I",
+                                               int(value) & 0xFFFFFFFF)
+
+class ul32DataElement(u32DataElement):
+    _endianess = "<"
+
+class charDataElement(DataElement):
+    _size = 1
+
+    def __str__(self):
+        return str(self.get_value())
+
+    def __int__(self):
+        return ord(self.get_value())
+
+    def _get_value(self, data):
+        return data
+
+    def set_value(self, value):
+        self._data[self._offset] = str(value)
+
+class bcdDataElement(DataElement):
+    def __int__(self):
+        tens, ones = self.get_value()
+        return (tens * 10) + ones
+
+    def set_bits(self, mask):
+        self._data[self._offset] = ord(self._data[self._offset]) | int(mask)
+
+    def clr_bits(self, mask):
+        self._data[self._offset] = ord(self._data[self._offset]) & ~int(mask)
+
+    def get_bits(self, mask):
+        return ord(self._data[self._offset]) & int(mask)
+
+    def set_raw(self, data):
+        if isinstance(data, int):
+            self._data[self._offset] = data & 0xFF
+        elif isinstance(data, str):
+            self._data[self._offset] = data[0]
+        else:
+            raise TypeError("Unable to set bcdDataElement from type %s" %
+                            type(data))
+
+class lbcdDataElement(bcdDataElement):
+    _size = 1
+
+    def _get_value(self, data):
+        a = (ord(data) & 0xF0) >> 4
+        b = ord(data) & 0x0F
+        return (b, a)
+
+    def set_value(self, value):
+        value = int("%02i" % value, 16)
+        self._data[self._offset] = value
+
+class bbcdDataElement(bcdDataElement):
+    _size = 1
+
+    def _get_value(self, data):
+        a = (ord(data) & 0xF0) >> 4
+        b = ord(data) & 0x0F
+        return (a, b)
+
+    def set_value(self, value):
+        self._data[self._offset] = int("%02i" % value, 16)
+
+class bitDataElement(intDataElement):
+    _nbits = 0
+    _shift = 0
+    _subgen = u8DataElement # Default to a byte
+
+    def __repr__(self):
+        fmt = "0x%%0%iX (%%sb)" % (self._size * 2)
+        return fmt % (int(self), format_binary(self._nbits, self.get_value()))
+
+    def get_value(self):
+        data = self._subgen(self._data, self._offset).get_value()
+        mask = bits_between(self._shift-self._nbits, self._shift)
+        val = data & mask
+
+        #print "start: %i bits: %i" % (self._shift, self._nbits)
+        #print "data:  %04x" % data
+        #print "mask:  %04x" % mask
+        #print " val:  %04x" % val
+
+        val >>= (self._shift - self._nbits)
+        return val
+
+    def set_value(self, value):
+        mask = bits_between(self._shift-self._nbits, self._shift)
+        data = self._subgen(self._data, self._offset).get_value()
+        data &= ~mask
+
+        #print "data: %04x" % data
+        #print "mask: %04x" % mask
+        #print "valu: %04x" % value
+
+        value = ((int(value) << (self._shift-self._nbits)) & mask) | data
+        self._subgen(self._data, self._offset).set_value(value)
+        
+    def size(self):
+        return self._nbits
+
+class structDataElement(DataElement):
+    def __repr__(self):
+        s = "struct {" + os.linesep
+        for prop in self._keys:
+            s += "  %15s: %s%s" % (prop, repr(self._generators[prop]),
+                                   os.linesep)
+        s += "} %s (%i bytes at 0x%04X)%s" % (self._name,
+                                              self.size() / 8,
+                                              self._offset,
+                                              os.linesep)
+        return s
+
+    def __init__(self, *args, **kwargs):
+        self._generators = {}
+        self._keys = []
+        self._count = 1
+        if "name" in kwargs.keys():
+            self._name = kwargs["name"]
+            del kwargs["name"]
+        else:
+            self._name = "(anonymous)"
+        DataElement.__init__(self, *args, **kwargs)
+        self.__init = True
+
+    def _value(self, data, generators):
+        result = {}
+        for name, gen in generators.items():
+            result[name] = gen.get_value(data)
+        return result
+
+    def get_value(self):
+        result = []
+        for i in range(0, self._count):
+            result.append(self._value(self._data, self._generators[i]))
+
+        if self._count == 1:
+            return result[0]
+        else:
+            return result
+
+    def __getitem__(self, key):
+        return self._generators[key]
+
+    def __setitem__(self, key, value):
+        if key in self._generators:
+            self._generators[key].set_value(value)
+        else:
+            self._generators[key] = value
+            self._keys.append(key)
+
+    def __getattr__(self, name):
+        try:
+            return self._generators[name]
+        except KeyError:
+            raise AttributeError("No attribute %s in struct" % name)
+
+    def __setattr__(self, name, value):
+        if not self.__dict__.has_key("_structDataElement__init"):
+            self.__dict__[name] = value
+        else:
+            self.__dict__["_generators"][name].set_value(value)
+
+    def size(self):
+        size = 0
+        for name, gen in self._generators.items():
+            if not isinstance(gen, list):
+                gen = [gen]
+
+            i = 0
+            for el in gen:
+                i += 1
+                size += el.size()
+                #print "Size of %s[%i] = %i" % (name, i, el.size())
+        return size
+
+    def get_raw(self):
+        size = self.size() / 8
+        return self._data[self._offset:self._offset+size]
+
+    def set_raw(self, buffer):
+        if len(buffer) != (self.size() / 8):
+            raise ValueError("Struct size mismatch during set_raw()")
+        self._data[self._offset] = buffer
+
+class Processor:
+
+    _types = {
+        "u8"   : u8DataElement,
+        "u16"  : u16DataElement,
+        "ul16" : ul16DataElement,
+        "u24"  : u24DataElement,
+        "ul24" : ul24DataElement,
+        "u32"  : u32DataElement,
+        "ul32" : ul32DataElement,
+        "char" : charDataElement,
+        "lbcd" : lbcdDataElement,
+        "bbcd" : bbcdDataElement,
+        }
+
+    def __init__(self, data, offset):
+        self._data = data
+        self._offset = offset
+        self._obj = None
+        self._user_types = {}
+
+    def do_symbol(self, symdef, gen):
+        name = symdef[1]
+        self._generators[name] = gen
+
+    def do_bitfield(self, dtype, bitfield):
+        bytes = self._types[dtype](self._data, 0).size() / 8
+        bitsleft = bytes * 8
+
+        for _bitdef, defn in bitfield:
+            name = defn[0][1]
+            bits = int(defn[1][1])
+            if bitsleft < 0:
+                raise ParseError("Invalid bitfield spec")
+
+            class bitDE(bitDataElement):
+                _nbits = bits
+                _shift = bitsleft
+                _subgen = self._types[dtype]
+            
+            self._generators[name] = bitDE(self._data, self._offset)
+            bitsleft -= bits
+
+        if bitsleft:
+            print "WARNING: %i trailing bits unaccounted for in %s" % (bitsleft,
+                                                                       bitfield)
+
+        return bytes
+
+    def parse_defn(self, defn):
+        dtype = defn[0]
+
+        if defn[1][0] == "bitfield":
+            size = self.do_bitfield(dtype, defn[1][1])
+            count = 1
+            self._offset += size
+        else:
+            if defn[1][0] == "array":
+                sym = defn[1][1][0]
+                count = int(defn[1][1][1][1])
+            else:
+                count = 1
+                sym = defn[1]
+
+            name = sym[1]
+            res = arrayDataElement(self._offset)
+            size = 0
+            for i in range(0, count):
+                gen = self._types[dtype](self._data, self._offset)
+                res.append(gen)
+                self._offset += (gen.size() / 8)
+
+            if count == 1:
+                self._generators[name] = res[0]
+            else:
+                self._generators[name] = res
+
+    def parse_struct_decl(self, struct):
+        block = struct[:-1]
+        if block[0][0] == "symbol":
+            # This is a pre-defined struct
+            block = self._user_types[block[0][1]]
+        deftype = struct[-1]
+        if deftype[0] == "array":
+            name = deftype[1][0][1]
+            count = int(deftype[1][1][1])
+        elif deftype[0] == "symbol":
+            name = deftype[1]
+            count = 1
+
+        result = arrayDataElement(self._offset)
+        for i in range(0, count):
+            element = structDataElement(self._data, self._offset, count,
+                                        name=name)
+            result.append(element)
+            tmp = self._generators
+            self._generators = element
+            self.parse_block(block)
+            self._generators = tmp
+
+        if count == 1:
+            self._generators[name] = result[0]
+        else:
+            self._generators[name] = result
+
+    def parse_struct_defn(self, struct):
+        name = struct[0][1]
+        block = struct[1:]
+        self._user_types[name] = block
+
+    def parse_struct(self, struct):
+        if struct[0][0] == "struct_defn":
+            return self.parse_struct_defn(struct[0][1])
+        elif struct [0][0] == "struct_decl":
+            return self.parse_struct_decl(struct[0][1])
+        else:
+            raise Exception("Internal error: What is `%s'?" % struct[0][0])
+
+    def parse_directive(self, directive):
+        name = directive[0][0]
+        if name == "seekto":
+            offset = directive[0][1][0][1]
+            if offset.startswith("0x"):
+                offset = int(offset[2:], 16)
+            else:
+                offset = int(offset)
+            #print "NOTICE: Setting offset to %i (0x%X)" % (offset, offset)
+            self._offset = offset
+        elif name == "seek":
+            offset = int(directive[0][1][0][1])
+            self._offset += offset
+        elif name == "printoffset":
+            string = directive[0][1][0][1]
+            print "%s: %i (0x%08X)" % (string[1:-1], self._offset, self._offset)
+
+    def parse_block(self, lang):
+        for t, d in lang:
+            #print t
+            if t == "struct":
+                self.parse_struct(d)
+            elif t == "definition":
+                self.parse_defn(d)
+            elif t == "directive":
+                self.parse_directive(d)
+        
+
+    def parse(self, lang):
+        self._generators = structDataElement(self._data, self._offset)
+        self.parse_block(lang[0])
+        return self._generators
+
+
+def parse(spec, data, offset=0):
+    ast = bitwise_grammar.parse(spec)
+    p = Processor(data, offset)
+    return p.parse(ast)
+
+if __name__ == "__main__":
+    defn = """
+struct mytype { u8 foo; };
+struct mytype bar;
+struct {
+  u8 foo;
+  u8 highbit:1,
+     sixzeros:6,
+     lowbit:1;
+  char string[3];
+  bbcd fourdigits[2];
+} mystruct;
+"""
+    data = "\xab\x7F\x81abc\x12\x34"
+    tree = parse(defn, data)
+
+    print repr(tree)
+
+    print "Foo %i" % tree.mystruct.foo
+    print "Highbit: %i SixZeros: %i: Lowbit: %i" % (tree.mystruct.highbit,
+                                                    tree.mystruct.sixzeros,
+                                                    tree.mystruct.lowbit)
+    print "String: %s" % tree.mystruct.string
+    print "Fourdigits: %i" % tree.mystruct.fourdigits
+
+    import sys
+    sys.exit(0)
+
+
+    test = """
+    struct {
+      u16 bar;
+      u16 baz;
+      u8 one;
+      u8 upper:2,
+         twobit:1,
+         onebit:1,
+         lower:4;
+      u8 array[3];
+      char str[3];
+      bbcd bcdL[2];
+    } foo[2];
+    u8 tail;
+    """
+    data = "\xfe\x10\x00\x08\xFF\x23\x01\x02\x03abc\x34\x89"
+    data = (data * 2) + "\x12"
+    data = MemoryMap(data)
+    
+    ast = bitwise_grammar.parse(test)
+
+    # Just for testing, pretty-print the tree
+    pp(ast)
+    
+    # Mess with it a little
+    p = Processor(data, 0)
+    obj = p.parse(ast)
+    print "Object: %s" % obj
+    print obj["foo"][0]["bcdL"]
+    print obj["tail"]
+    print obj["foo"][0]["bar"]
+    obj["foo"][0]["bar"].set_value(255 << 8)
+    obj["foo"][0]["twobit"].set_value(0)
+    obj["foo"][0]["onebit"].set_value(1)
+    print "%i" % int(obj["foo"][0]["bar"])
+
+    for i in  obj["foo"][0]["array"]:
+        print int(i)
+    obj["foo"][0]["array"][1].set_value(255)
+
+    for i in obj["foo"][0]["bcdL"]:
+        print i.get_value()
+
+    int_to_bcd(obj["foo"][0]["bcdL"], 1234)
+    print bcd_to_int(obj["foo"][0]["bcdL"])
+
+    set_string(obj["foo"][0]["str"], "xyz")
+    print get_string(obj["foo"][0]["str"])
+
+    print repr(data.get_packed())
diff --git a/chirp/bitwise_grammar.py b/chirp/bitwise_grammar.py
new file mode 100644
index 0000000..f311410
--- /dev/null
+++ b/chirp/bitwise_grammar.py
@@ -0,0 +1,81 @@
+# Copyright 2010 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 re
+from chirp.pyPEG import keyword, parseLine
+
+TYPES = ["u8", "u16", "ul16", "u24", "ul24", "u32", "ul32", "char",
+         "lbcd", "bbcd"]
+DIRECTIVES = ["seekto", "seek", "printoffset"]
+
+def string():
+    return re.compile(r"\"[^\"]*\"")
+
+def symbol():
+    return re.compile(r"\w+")
+
+def count():
+    return re.compile(r"([1-9][0-9]*|0x[0-9a-fA-F]+)")
+
+def bitdef():
+    return symbol, ":", count, -1
+
+def _bitdeflist():
+    return bitdef, -1, (",", bitdef)
+
+def bitfield():
+    return -2, _bitdeflist
+
+def array():
+    return symbol, '[', count, ']'
+
+def _typedef():
+    return re.compile(r"(%s)" % "|".join(TYPES))
+
+def definition():
+    return _typedef, [array, bitfield, symbol], ";"
+
+def seekto():
+    return keyword("seekto"), count
+
+def seek():
+    return keyword("seek"), count
+
+def printoffset():
+    return keyword("printoffset"), string
+
+def directive():
+    return "#", [seekto, seek, printoffset], ";"
+
+def _block_inner():
+    return -2, [definition, struct, directive]
+
+def _block():
+    return "{", _block_inner, "}"
+
+def struct_defn():
+    return symbol, _block
+
+def struct_decl():
+    return [symbol, _block], [array, symbol]
+
+def struct():
+    return keyword("struct"), [struct_defn, struct_decl], ";"
+
+def _language():
+    return _block_inner
+
+def parse(data):
+    return parseLine(data, _language, resultSoFar=[]) 
diff --git a/chirp/chirp_common.py b/chirp/chirp_common.py
new file mode 100644
index 0000000..f2472a0
--- /dev/null
+++ b/chirp/chirp_common.py
@@ -0,0 +1,1329 @@
+# 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/>.
+
+SEPCHAR = ","
+    
+#print "Using separation character of '%s'" % SEPCHAR
+
+import math
+
+from chirp import errors, memmap
+
+# 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,
+          ]          
+
+# 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,
+     ]
+
+# Some radios have some strange codes
+DTCS_EXTRA_CODES = [ 17, 645 ]
+
+CROSS_MODES = [
+    "Tone->Tone",
+    "Tone->DTCS",
+    "DTCS->Tone",
+    "DTCS->",
+    "->Tone",
+    "->DTCS",
+    "DTCS->DTCS",
+]
+
+MODES = ["WFM", "FM", "NFM", "AM", "NAM", "DV", "USB", "LSB", "CW", "RTTY",
+         "DIG", "PKT", "NCW", "NCWR", "CWR", "P25", "Auto"]
+
+STD_6M_OFFSETS = [
+    (51620000, 51980000, -500000),
+    (52500000, 52980000, -500000),
+    (53500000, 53980000, -500000),
+    ]
+
+STD_2M_OFFSETS = [
+    (145100000, 145500000, -600000),
+    (146000000, 146400000,  600000),
+    (146600000, 147000000, -600000),
+    (147000000, 147400000,  600000),
+    (147600000, 148000000, -600000),
+    ]
+
+STD_220_OFFSETS = [
+    (223850000, 224980000, -1600000),
+    ]
+
+STD_70CM_OFFSETS = [
+    (440000000, 445000000,  5000000),
+    (447000000, 450000000, -5000000),
+    ]
+
+STD_23CM_OFFSETS = [
+    (1282000000, 1288000000, -12000000),
+    ]
+
+# Standard offsets, indexed by band (wavelength in cm)
+STD_OFFSETS = {
+    600 : STD_6M_OFFSETS,
+    200 : STD_2M_OFFSETS,
+    125 : STD_220_OFFSETS,
+     70 : STD_70CM_OFFSETS,
+     23 : STD_23CM_OFFSETS,
+    }
+
+BAND_TO_MHZ = {
+    600 : (   50000000,   54000000 ),
+    200 : (  144000000,  148000000 ),
+    125 : (  219000000,  225000000 ),
+    70 :  (  420000000,  450000000 ),
+    23 :  ( 1240000000, 1300000000 ),
+}
+
+# NB: This only works for some bands, throws an Exception otherwise
+def freq_to_band(freq):
+    """Returns the band (in cm) for a given frequency"""
+    for band, (lo, hi) in BAND_TO_MHZ.items():
+        if int(freq) > lo and int(freq) < hi:
+            return band
+    raise Exception("No conversion for frequency %i" % freq)
+
+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)])
+
+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"""
+    if "." in freqstr:
+        mhz, khz = freqstr.split(".")
+    else:
+        mhz = freqstr
+        khz = "0"
+    if not mhz.isdigit() and khz.isdigit():
+        raise ValueError("Invalid value")
+
+    # Make kHz exactly six decimal places
+    return int(("%s%-6s" % (mhz, khz)).replace(" ", "0"))
+
+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 RadioSettingsGroup 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,
+        "ctone"         : TONES,
+        "dtcs"          : DTCS_CODES + DTCS_EXTRA_CODES,
+        "rx_dtcs"       : DTCS_CODES + DTCS_EXTRA_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"]
+
+    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 self._valid_map.has_key(name) 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 %i: %s%s%s %s (%s) r%.1f%s c%.1f%s d%03i%s%s [%.2f]"% \
+            (self.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:
+            print "Loc: %s" % vals[0]
+            raise errors.InvalidDataError("Location is not a valid integer")
+
+        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 Bank:
+    """Base class for a radio's Bank"""
+    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 "Bank-%s" % self._index
+
+    def get_name(self):
+        """Returns the static or user-adjustable bank name"""
+        return self._name
+
+    def get_index(self):
+        """Returns the immutable bank index (string or int)"""
+        return self._index
+
+    def __eq__(self, other):
+        return self.get_index() == other.get_index()
+
+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:
+    """A bank model where one memory is in zero or one banks at any point"""
+    def __init__(self, radio):
+        self._radio = radio
+
+    def get_num_banks(self):
+        """Returns the number of banks (should be callable without
+        consulting the radio"""
+        raise Exception("Not implemented")
+
+    def get_banks(self):
+        """Return a list of banks"""
+        raise Exception("Not implemented")
+
+    def add_memory_to_bank(self, memory, bank):
+        """Add @memory to @bank."""
+        raise Exception("Not implemented")
+
+    def remove_memory_from_bank(self, memory, bank):
+        """Remove @memory from @bank.
+        Shall raise exception if @memory is not in @bank."""
+        raise Exception("Not implemented")
+
+    def get_bank_memories(self, bank):
+        """Return a list of memories in @bank"""
+        raise Exception("Not implemented")
+
+    def get_memory_banks(self, memory):
+        """Returns a list of the banks that @memory is in"""
+        raise Exception("Not implemented")
+
+class BankIndexInterface:
+    """Interface for banks with index capabilities"""
+    def get_index_bounds(self):
+        """Returns a tuple (lo,hi) of the minimum and maximum bank indices"""
+        raise Exception("Not implemented")
+
+    def get_memory_index(self, memory, bank):
+        """Returns the index of @memory in @bank"""
+        raise Exception("Not implemented")
+
+    def set_memory_index(self, memory, bank, index):
+        """Sets the index of @memory in @bank to @index"""
+        raise Exception("Not implemented")
+
+    def get_next_bank_index(self, bank):
+        """Returns the next available bank index in @bank, or raises
+        Exception if full"""
+        raise Exception("Not implemented")
+
+
+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 sys
+
+    sys.stderr.write("\r%s" % status)
+    
+
+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_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 not name 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_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_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 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 Radio:
+    """Base class for all Radio drivers"""
+    BAUD_RATE = 9600
+    HARDWARE_FLOW = False
+    VENDOR = "Unknown"
+    MODEL = "Unknown"
+    VARIANT = ""
+
+    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)
+
+    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_bank_model(self):
+        """Returns either a BankModel or None if not supported"""
+        return None
+
+    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 RadioSettingGroup 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 = txval = None
+    txpol = mem.dtcs_polarity[0]
+    rxmode = rxval = None
+    rxpol = mem.dtcs_polarity[1]
+    
+    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
+
+    return ((txmode, txval, txpol),
+            (rxmode, rxval, rxpol))
diff --git a/chirp/detect.py b/chirp/detect.py
new file mode 100644
index 0000000..eeefb32
--- /dev/null
+++ b/chirp/detect.py
@@ -0,0 +1,105 @@
+# Copyright 2010 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 serial
+
+from chirp import errors, icf, directory, ic9x_ll
+from chirp import kenwood_live, icomciv
+
+def _icom_model_data_to_rclass(md):
+    for _rtype, rclass in directory.DRV_TO_RADIO.items():
+        if rclass.VENDOR != "Icom":
+            continue
+        if not hasattr(rclass, 'get_model') or not rclass.get_model():
+            continue
+        if rclass.get_model()[:4] == md[:4]:
+            return rclass
+
+    raise errors.RadioError("Unknown radio type %02x%02x%02x%02x" %\
+                                (ord(md[0]),
+                                 ord(md[1]),
+                                 ord(md[2]),
+                                 ord(md[3])))
+
+def _detect_icom_radio(ser):
+    # ICOM VHF/UHF Clone-type radios @ 9600 baud
+
+    try:
+        ser.setBaudrate(9600)
+        md = icf.get_model_data(ser)
+        return _icom_model_data_to_rclass(md)
+    except errors.RadioError, e:
+        print e
+
+    # ICOM IC-91/92 Live-mode radios @ 4800/38400 baud
+
+    ser.setBaudrate(4800)
+    try:
+        ic9x_ll.send_magic(ser)
+        return _icom_model_data_to_rclass("ic9x")
+    except errors.RadioError:
+        pass
+
+    # ICOM CI/V Radios @ various bauds
+
+    for rate in [9600, 4800, 19200]:
+        try:
+            ser.setBaudrate(rate)
+            return icomciv.probe_model(ser)
+        except errors.RadioError:
+            pass
+
+    ser.close()
+
+    raise errors.RadioError("Unable to get radio model")
+
+def detect_icom_radio(port):
+    """Detect which Icom model is connected to @port"""
+    ser = serial.Serial(port=port, timeout=0.5)
+
+    try:
+        result = _detect_icom_radio(ser)
+    except Exception:
+        ser.close()
+        raise
+
+    ser.close()
+
+    print "Auto-detected %s %s on %s" % (result.VENDOR,
+                                         result.MODEL,
+                                         port)
+
+    return result
+
+def detect_kenwoodlive_radio(port):
+    """Detect which Kenwood model is connected to @port"""
+    ser = serial.Serial(port=port, baudrate=9600, timeout=0.5)
+    r_id = kenwood_live.get_id(ser)
+    ser.close()
+
+    models = {}
+    for rclass in directory.DRV_TO_RADIO.values():
+        if rclass.VENDOR == "Kenwood":
+            models[rclass.MODEL] = rclass
+
+    if r_id in models.keys():
+        return models[r_id]
+    else:
+        raise errors.RadioError("Unsupported model `%s'" % r_id)
+
+DETECT_FUNCTIONS = {
+    "Icom" : detect_icom_radio,
+    "Kenwood" : detect_kenwoodlive_radio,
+}
diff --git a/chirp/directory.py b/chirp/directory.py
new file mode 100644
index 0000000..5fc47e3
--- /dev/null
+++ b/chirp/directory.py
@@ -0,0 +1,133 @@
+# Copyright 2010 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/>.
+
+import os
+import tempfile
+
+from chirp import icf
+from chirp import chirp_common, util, rfinder, radioreference, errors
+
+def radio_class_id(cls):
+    """Return a unique identification string for @cls"""
+    ident = "%s_%s" % (cls.VENDOR, cls.MODEL)
+    if cls.VARIANT:
+        ident += "_%s" % cls.VARIANT
+    ident = ident.replace("/", "_")
+    ident = ident.replace(" ", "_")
+    ident = ident.replace("(", "")
+    ident = ident.replace(")", "")
+    return ident
+
+ALLOW_DUPS = False
+def enable_reregistrations():
+    """Set the global flag ALLOW_DUPS=True, which will enable a driver
+    to re-register for a slot in the directory without triggering an
+    exception"""
+    global ALLOW_DUPS
+    if not ALLOW_DUPS:
+        print "NOTE: driver re-registration enabled"
+    ALLOW_DUPS = True
+
+def register(cls):
+    """Register radio @cls with the directory"""
+    global DRV_TO_RADIO
+    ident = radio_class_id(cls)
+    if ident in DRV_TO_RADIO.keys():
+        if ALLOW_DUPS:
+            print "Replacing existing driver id `%s'" % ident
+        else:
+            raise Exception("Duplicate radio driver id `%s'" % ident)
+    DRV_TO_RADIO[ident] = cls
+    RADIO_TO_DRV[cls] = ident
+    print "Registered %s = %s" % (ident, cls.__name__)
+
+    return cls
+
+DRV_TO_RADIO = {}
+RADIO_TO_DRV = {}
+
+def get_radio(driver):
+    """Get radio driver class by identification string"""
+    if DRV_TO_RADIO.has_key(driver):
+        return DRV_TO_RADIO[driver]
+    else:
+        raise Exception("Unknown radio type `%s'" % driver)
+
+def get_driver(rclass):
+    """Get the identification string for a given class"""
+    if RADIO_TO_DRV.has_key(rclass):
+        return RADIO_TO_DRV[rclass]
+    elif RADIO_TO_DRV.has_key(rclass.__bases__[0]):
+        return RADIO_TO_DRV[rclass.__bases__[0]]
+    else:
+        raise Exception("Unknown radio type `%s'" % rclass)
+
+def icf_to_image(icf_file, img_file):
+    # FIXME: Why is this here?
+    """Convert an ICF file to a .img file"""
+    mdata, mmap = icf.read_file(icf_file)
+    img_data = None
+
+    for model in DRV_TO_RADIO.values():
+        try:
+            if model._model == mdata:
+                img_data = mmap.get_packed()[:model._memsize]
+                break
+        except Exception:
+            pass # Skip non-Icoms
+
+    if img_data:
+        f = file(img_file, "wb")
+        f.write(img_data)
+        f.close()
+    else:
+        print "Unsupported model data:"
+        print util.hexprint(mdata)
+        raise Exception("Unsupported model")
+
+def get_radio_by_image(image_file):
+    """Attempt to get the radio class that owns @image_file"""
+    if image_file.startswith("radioreference://"):
+        _, _, zipcode, username, password = image_file.split("/", 4)
+        rr = radioreference.RadioReferenceRadio(None)
+        rr.set_params(zipcode, username, password)
+        return rr
+    
+    if image_file.startswith("rfinder://"):
+        _, _, email, passwd, lat, lon, miles = image_file.split("/")
+        rf = rfinder.RFinderRadio(None)
+        rf.set_params((float(lat), float(lon)), int(miles), email, passwd)
+        return rf
+    
+    if os.path.exists(image_file) and icf.is_icf_file(image_file):
+        tempf = tempfile.mktemp()
+        icf_to_image(image_file, tempf)
+        print "Auto-converted %s -> %s" % (image_file, tempf)
+        image_file = tempf
+
+    if os.path.exists(image_file):
+        f = file(image_file, "rb")
+        filedata = f.read()
+        f.close()
+    else:
+        filedata = ""
+
+    for rclass in DRV_TO_RADIO.values():
+        if not issubclass(rclass, chirp_common.FileBackedRadio):
+            continue
+        if rclass.match_model(filedata, image_file):
+            return rclass(image_file)
+    raise errors.ImageDetectFailed("Unknown file format")
diff --git a/chirp/errors.py b/chirp/errors.py
new file mode 100644
index 0000000..3fe1027
--- /dev/null
+++ b/chirp/errors.py
@@ -0,0 +1,38 @@
+# 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/>.
+
+class InvalidDataError(Exception):
+    """The radio driver encountered some invalid data"""
+    pass
+
+class InvalidValueError(Exception):
+    """An invalid value for a given parameter was used"""
+    pass
+
+class InvalidMemoryLocation(Exception):
+    """The requested memory location does not exist"""
+    pass
+
+class RadioError(Exception):
+    """An error occurred while talking to the radio"""
+    pass
+
+class UnsupportedToneError(Exception):
+    """The radio does not support the specified tone value"""
+    pass
+
+class ImageDetectFailed(Exception):
+    """The driver for the supplied image could not be determined"""
+    pass
diff --git a/chirp/ft1802.py b/chirp/ft1802.py
new file mode 100644
index 0000000..9a806c0
--- /dev/null
+++ b/chirp/ft1802.py
@@ -0,0 +1,234 @@
+# 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/>.
+
+# FT-1802 Clone Proceedure
+# 1. Turn radio off.
+# 2. Connect cable to mic jack.
+# 3. Press and hold in the [LOW(A/N)] key while turning the radio on.
+# 4. In Chirp, choose Download from Radio.
+# 5. Press the [MHz(SET)] key to send image.
+# or
+# 4. Press the [D/MR(MW)] key ("--WAIT--" will appear on the LCD).
+# 5. In Chirp, choose Upload to Radio.
+
+from chirp import chirp_common, bitwise, directory, yaesu_clone
+from chirp.settings import RadioSetting, RadioSettingGroup, \
+    RadioSettingValueBoolean
+
+MEM_FORMAT = """
+#seekto 0x06ea;
+struct {
+  u8 odd_pskip:1,
+     odd_skip:1,
+     odd_visible:1,
+     odd_valid:1,
+     even_pskip:1,
+     even_skip:1,
+     even_visible:1,
+     even_valid:1;
+} flags[100];
+
+#seekto 0x076a;
+struct {
+  u8 unknown1a:1,
+     step_changed:1,
+     narrow:1,
+     clk_shift:1,
+     unknown1b:4;
+  u8 unknown2a:2,
+     duplex:2,
+     unknown2b:1,
+     tune_step:3;
+  bbcd freq[3];
+  u8 power:2,
+     unknown3:3,
+     tmode:3;
+  u8 name[6];
+  bbcd offset[3];
+  u8 tone;
+  u8 dtcs;
+  u8 unknown4;
+} memory[200];
+"""
+
+
+MODES = ["FM", "NFM"]
+TMODES = ["", "Tone", "TSQL", "DTCS", "TSQL-R", "Cross"]
+CROSS_MODES = ["DTCS->", "Tone->DTCS", "DTCS->Tone"]
+DUPLEX = ["", "-", "+", "split"]
+STEPS = [5.0, 10.0, 12.5, 15.0, 20.0, 25.0, 50.0, 100.0]
+POWER_LEVELS = [chirp_common.PowerLevel("LOW1", watts=5),
+                chirp_common.PowerLevel("LOW2", watts=10),
+                chirp_common.PowerLevel("LOW3", watts=25),
+                chirp_common.PowerLevel("HIGH", watts=50),
+                ]
+CHARSET = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ +-/?()?_"
+
+
+ at directory.register
+class FT1802Radio(yaesu_clone.YaesuCloneModeRadio):
+    """Yaesu FT-1802"""
+    VENDOR = "Yaesu"
+    MODEL = "FT-1802M"
+    BAUD_RATE = 19200
+
+    _model = "AH023"
+    _block_lengths = [10, 8001]
+    _memsize = 8011
+
+    def get_features(self):
+        rf = chirp_common.RadioFeatures()
+
+        rf.memory_bounds = (0, 199)
+
+        rf.can_odd_split = True
+        rf.has_ctone = False
+        rf.has_tuning_step = True
+        rf.has_dtcs_polarity = False # in radio settings, not per memory
+        rf.has_bank = False # has banks, but not implemented
+
+        rf.valid_tuning_steps = STEPS
+        rf.valid_modes = MODES
+        rf.valid_tmodes = TMODES
+        rf.valid_bands = [(137000000, 174000000)]
+        rf.valid_power_levels = POWER_LEVELS
+        rf.valid_duplexes = DUPLEX
+        rf.valid_skips = ["", "S", "P"]
+        rf.valid_name_length = 6
+        rf.valid_characters = CHARSET
+        rf.has_cross = True
+        rf.valid_cross_modes = list(CROSS_MODES)
+
+        return rf
+
+    def _checksums(self):
+        return [yaesu_clone.YaesuChecksum(0, self._memsize-2)]
+
+    def process_mmap(self):
+        self._memobj = bitwise.parse(MEM_FORMAT, self._mmap)
+
+    def get_raw_memory(self, number):
+        return repr(self._memobj.memory[number]) + \
+               repr(self._memobj.flags[number/2])
+
+    def get_memory(self, number):
+        _mem = self._memobj.memory[number]
+        _flag = self._memobj.flags[number/2]
+
+        nibble = (number % 2) and "odd" or "even"
+        visible = _flag["%s_visible" % nibble]
+        valid = _flag["%s_valid" % nibble]
+        pskip = _flag["%s_pskip" % nibble]
+        skip = _flag["%s_skip" % nibble]
+
+        mem = chirp_common.Memory()
+        mem.number = number
+
+        if not visible:
+            mem.empty = True
+        if not valid:
+            mem.empty = True
+            return mem
+
+        mem.freq = chirp_common.fix_rounded_step(int(_mem.freq) * 1000)
+        mem.offset = chirp_common.fix_rounded_step(int(_mem.offset) * 1000)
+        mem.duplex = DUPLEX[_mem.duplex]
+        mem.tuning_step = _mem.step_changed and STEPS[_mem.tune_step] or STEPS[0]
+        if _mem.tmode < TMODES.index("Cross"):
+            mem.tmode = TMODES[_mem.tmode]
+            mem.cross_mode = CROSS_MODES[0]
+        else:
+            mem.tmode = "Cross"
+            mem.cross_mode = CROSS_MODES[_mem.tmode - TMODES.index("Cross")]
+        mem.rtone = chirp_common.TONES[_mem.tone]
+        mem.dtcs = chirp_common.DTCS_CODES[_mem.dtcs]
+        for i in _mem.name:
+            if i == 0xFF:
+                break
+            if i & 0x80 == 0x80:
+                # first bit in name is "show name"
+                mem.name += CHARSET[0x80 ^ int(i)]
+            else:
+                mem.name += CHARSET[i]
+        mem.name = mem.name.rstrip()
+        mem.mode = _mem.narrow and "NFM" or "FM"
+        mem.skip = pskip and "P" or skip and "S" or ""
+        mem.power = POWER_LEVELS[_mem.power]
+
+        mem.extra = RadioSettingGroup("extra", "Extra Settings")
+        rs = RadioSetting("clk_shift", "Clock Shift",
+                          RadioSettingValueBoolean(_mem.clk_shift))
+        mem.extra.append(rs)
+
+        return mem
+
+    def set_memory(self, mem):
+        _mem = self._memobj.memory[mem.number]
+        _flag = self._memobj.flags[mem.number/2]
+
+        nibble = (mem.number % 2) and "odd" or "even"
+        
+        valid = _flag["%s_valid" % nibble]
+        visible = _flag["%s_visible" % nibble]
+
+        if not mem.empty and not valid:
+            _flag["%s_valid" % nibble] = True
+            _mem.unknown1a = 0x00
+            _mem.clk_shift = 0x00
+            _mem.unknown1b = 0x00
+            _mem.unknown2a = 0x00
+            _mem.unknown2b = 0x00
+            _mem.unknown3 = 0x00
+            _mem.unknown4 = 0x00
+
+        if mem.empty and valid and not visible:
+            _flag["%s_valid" % nibble] = False
+            return
+        _flag["%s_visible" % nibble] = not mem.empty
+
+        if mem.empty:
+            return
+
+        _flag["%s_valid" % nibble] = True
+
+        _mem.freq = mem.freq / 1000
+        _mem.offset = mem.offset / 1000
+        _mem.duplex = DUPLEX.index(mem.duplex)
+        _mem.tune_step = STEPS.index(mem.tuning_step)
+        _mem.step_changed = mem.tuning_step != STEPS[0]
+        if mem.tmode != "Cross":
+            _mem.tmode = TMODES.index(mem.tmode)
+        else:
+            _mem.tmode = TMODES.index("Cross") + CROSS_MODES.index(mem.cross_mode)
+        _mem.tone = chirp_common.TONES.index(mem.rtone)
+        _mem.dtcs = chirp_common.DTCS_CODES.index(mem.dtcs)
+
+        _mem.name = [0xFF] * 6
+        for i in range(0, len(mem.name)):
+            try:
+                _mem.name[i] = CHARSET.index(mem.name[i])
+            except IndexError:
+                raise Exception("Character `%s' not supported")
+        if _mem.name[0] != 0xFF:
+            _mem.name[0] += 0x80 # show name instead of frequency
+
+        _mem.narrow = MODES.index(mem.mode)
+        _mem.power = 3 if mem.power is None else POWER_LEVELS.index(mem.power)
+
+        _flag["%s_pskip" % nibble] = mem.skip == "P"
+        _flag["%s_skip" % nibble] = mem.skip == "S"
+
+        for element in mem.extra:
+            setattr(_mem, element.get_name(), element.value)
diff --git a/chirp/ft2800.py b/chirp/ft2800.py
new file mode 100644
index 0000000..fb9d10c
--- /dev/null
+++ b/chirp/ft2800.py
@@ -0,0 +1,279 @@
+# Copyright 2011 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 time
+import os
+
+from chirp import util, memmap, chirp_common, bitwise, directory, errors
+from chirp.yaesu_clone import YaesuCloneModeRadio
+
+DEBUG = os.getenv("CHIRP_DEBUG") and True or False
+
+CHUNK_SIZE = 16
+def _send(s, data):
+    for i in range(0, len(data), CHUNK_SIZE):
+        chunk = data[i:i+CHUNK_SIZE]
+        s.write(chunk)
+        echo = s.read(len(chunk))
+        if chunk != echo:
+            raise Exception("Failed to read echo chunk")
+
+IDBLOCK = "\x0c\x01\x41\x33\x35\x02\x00\xb8"
+TRAILER = "\x0c\x02\x41\x33\x35\x00\x00\xb7"
+ACK = "\x0C\x06\x00"
+
+def _download(radio):
+    data = ""
+    for _i in range(0, 10):
+        data = radio.pipe.read(8)
+        if data == IDBLOCK:
+            break
+
+    if DEBUG:
+        print "Header:\n%s" % util.hexprint(data)
+
+    if len(data) != 8:
+        raise Exception("Failed to read header")
+
+    _send(radio.pipe, ACK)
+
+    data = ""
+
+    while len(data) < radio._block_sizes[1]:
+        time.sleep(0.1)
+        chunk = radio.pipe.read(38)
+        if DEBUG:
+            print "Got: %i:\n%s" % (len(chunk), util.hexprint(chunk))
+        if len(chunk) == 8:
+            print "END?"
+        elif len(chunk) != 38:
+            print "Should fail?"
+            break
+            #raise Exception("Failed to get full data block")
+        else:
+            cs = 0
+            for byte in chunk[:-1]:
+                cs += ord(byte)
+            if ord(chunk[-1]) != (cs & 0xFF):
+                raise Exception("Block failed checksum!")
+
+            data += chunk[5:-1]
+
+        _send(radio.pipe, ACK)
+        if radio.status_fn:
+            status = chirp_common.Status()
+            status.max = radio._block_sizes[1]
+            status.cur = len(data)
+            status.msg = "Cloning from radio"
+            radio.status_fn(status)
+
+    if DEBUG:
+        print "Total: %i" % len(data)
+
+    return memmap.MemoryMap(data)
+
+def _upload(radio):
+    for _i in range(0, 10):
+        data = radio.pipe.read(256)
+        if not data:
+            break
+        print "What is this garbage?\n%s" % util.hexprint(data)
+
+    _send(radio.pipe, IDBLOCK)
+    time.sleep(1)
+    ack = radio.pipe.read(300)
+    if DEBUG:
+        print "Ack was (%i):\n%s" % (len(ack), util.hexprint(ack))
+    if ack != ACK:
+        raise Exception("Radio did not ack ID")
+
+    block = 0
+    while block < (radio.get_memsize() / 32):
+        data = "\x0C\x03\x00\x00" + chr(block)
+        data += radio.get_mmap()[block*32:(block+1)*32]
+        cs = 0
+        for byte in data:
+            cs += ord(byte)
+        data += chr(cs & 0xFF)
+
+        if DEBUG:
+            print "Writing block %i:\n%s" % (block, util.hexprint(data))
+
+        _send(radio.pipe, data)
+        time.sleep(0.1)
+        ack = radio.pipe.read(3)
+        if ack != ACK:
+            raise Exception("Radio did not ack block %i" % block)
+
+        if radio.status_fn:
+            status = chirp_common.Status()
+            status.max = radio._block_sizes[1]
+            status.cur = block * 32
+            status.msg = "Cloning to radio"
+            radio.status_fn(status)
+        block += 1
+
+    _send(radio.pipe, TRAILER)
+
+MEM_FORMAT = """
+struct {
+  bbcd freq[4];
+  u8 unknown1[4];
+  bbcd offset[2];
+  u8 unknown2[2];
+  u8 pskip:1,
+     skip:1,
+     unknown3:1,
+     isnarrow:1,
+     power:2,
+     duplex:2;
+  u8 unknown4:6,
+     tmode:2;
+  u8 tone;
+  u8 dtcs;
+} memory[200];
+
+#seekto 0x0E00;
+struct {
+  char name[6];
+} names[200];
+"""
+
+MODES = ["FM", "NFM"]
+TMODES = ["", "Tone", "TSQL", "DTCS"]
+DUPLEX = ["", "-", "+", ""]
+POWER_LEVELS = [chirp_common.PowerLevel("Hi", watts=65),
+                chirp_common.PowerLevel("Mid", watts=25),
+                chirp_common.PowerLevel("Low2", watts=10),
+                chirp_common.PowerLevel("Low1", watts=5),
+                ]
+CHARSET = chirp_common.CHARSET_UPPER_NUMERIC + "()+-=*/???|_"
+
+ at directory.register
+class FT2800Radio(YaesuCloneModeRadio):
+    """Yaesu FT-2800"""
+    VENDOR = "Yaesu"
+    MODEL = "FT-2800M"
+
+    _block_sizes = [8, 7680]
+    _memsize = 7680
+
+    def get_features(self):
+        rf = chirp_common.RadioFeatures()
+
+        rf.memory_bounds = (0, 199)
+
+        rf.has_ctone = False
+        rf.has_tuning_step = False
+        rf.has_dtcs_polarity = False
+        rf.has_bank = False
+
+        rf.valid_tuning_steps = [5.0, 10.0, 12.5, 15.0, 20.0, 25.0, 50.0, 100.0]
+        rf.valid_modes = MODES
+        rf.valid_tmodes = TMODES
+        rf.valid_bands = [(137000000, 174000000)]
+        rf.valid_power_levels = POWER_LEVELS
+        rf.valid_duplexes = DUPLEX
+        rf.valid_skips = ["", "S", "P"]
+        rf.valid_name_length = 6
+        rf.valid_characters = CHARSET
+
+        return rf
+
+    def sync_in(self):
+        self.pipe.setParity("E")
+        start = time.time()
+        try:
+            self._mmap = _download(self)
+        except errors.RadioError:
+            raise
+        except Exception, e:
+            raise errors.RadioError("Failed to communicate with radio: %s" % e)
+        print "Downloaded in %.2f sec" % (time.time() - start)
+        self.process_mmap()
+
+    def sync_out(self):
+        self.pipe.setTimeout(1)
+        self.pipe.setParity("E")
+        start = time.time()
+        try:
+            _upload(self)
+        except errors.RadioError:
+            raise
+        except Exception, e:
+            raise errors.RadioError("Failed to communicate with radio: %s" % e)
+        print "Uploaded in %.2f sec" % (time.time() - start)
+
+    def process_mmap(self):
+        self._memobj = bitwise.parse(MEM_FORMAT, self._mmap)
+
+    def get_raw_memory(self, number):
+        return repr(self._memobj.memory[number])
+
+    def get_memory(self, number):
+        _mem = self._memobj.memory[number]
+        _nam = self._memobj.names[number]
+        mem = chirp_common.Memory()
+
+        mem.number = number
+
+        if _mem.get_raw()[0] == "\xFF":
+            mem.empty = True
+            return mem
+
+        mem.freq = int(_mem.freq) * 10
+        mem.offset = int(_mem.offset) * 100000
+        mem.duplex = DUPLEX[_mem.duplex]
+        mem.tmode = TMODES[_mem.tmode]
+        mem.rtone = chirp_common.TONES[_mem.tone]
+        mem.dtcs = chirp_common.DTCS_CODES[_mem.dtcs]
+        mem.name = str(_nam.name).rstrip()
+        mem.mode = _mem.isnarrow and "NFM" or "FM"
+        mem.skip = _mem.pskip and "P" or _mem.skip and "S" or ""
+        mem.power = POWER_LEVELS[_mem.power]
+
+        return mem
+
+    def set_memory(self, mem):
+        _mem = self._memobj.memory[mem.number]
+        _nam = self._memobj.names[mem.number]
+
+        if mem.empty:
+            _mem.set_raw("\xFF" * (_mem.size() / 8))
+            return
+
+        if _mem.get_raw()[0] == "\xFF":
+            # Emtpy -> Non-empty, so initialize
+            _mem.set_raw("\x00" * (_mem.size() / 8))
+
+        _mem.freq = mem.freq / 10
+        _mem.offset = mem.offset / 100000
+        _mem.duplex = DUPLEX.index(mem.duplex)
+        _mem.tmode = TMODES.index(mem.tmode)
+        _mem.tone = chirp_common.TONES.index(mem.rtone)
+        _mem.dtcs = chirp_common.DTCS_CODES.index(mem.dtcs)
+        _mem.isnarrow = MODES.index(mem.mode)
+        _mem.pskip = mem.skip == "P"
+        _mem.skip = mem.skip == "S"
+        if mem.power:
+            _mem.power = POWER_LEVELS.index(mem.power)
+        else:
+            _mem.power = 0
+
+        _nam.name = mem.name.ljust(6)[:6]
+
+    @classmethod
+    def match_model(cls, filedata, filename):
+        return len(filedata) == cls._memsize
diff --git a/chirp/ft50.py b/chirp/ft50.py
new file mode 100644
index 0000000..d101360
--- /dev/null
+++ b/chirp/ft50.py
@@ -0,0 +1,53 @@
+# Copyright 2010 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/>.
+
+from chirp import chirp_common, yaesu_clone, ft50_ll, directory
+
+# Not working, don't register
+#@directory.register
+class FT50Radio(yaesu_clone.YaesuCloneModeRadio):
+    BAUD_RATE = 9600
+    VENDOR = "Yaesu"
+    MODEL = "FT-50"
+
+    _memsize = 3723
+    _block_lengths = [10, 16, 112, 16, 16, 1776, 1776, 1]
+    _block_delay = 0.15
+
+    def get_features(self):
+        rf = chirp_common.RadioFeatures()
+        rf.memory_bounds = (1, 100)
+        rf.has_dtcs_polarity = False
+        rf.has_bank = False
+        rf.valid_modes = [ "FM", "WFM", "AM" ]
+        return rf
+
+    def _update_checksum(self):
+        ft50_ll.update_checksum(self._mmap)
+
+    def get_raw_memory(self, number):
+        return ft50_ll.get_raw_memory(self._mmap, number)
+
+    def get_memory(self, number):
+        return ft50_ll.get_memory(self._mmap, number)
+
+    def set_memory(self, number):
+        return ft50_ll.set_memory(self._mmap, number)
+
+    def erase_memory(self, number):
+        return ft50_ll.erase_memory(self._mmap, number)
+
+    def filter_name(self, name):
+        return name[:4].upper()
diff --git a/chirp/ft50_ll.py b/chirp/ft50_ll.py
new file mode 100644
index 0000000..c38b4f9
--- /dev/null
+++ b/chirp/ft50_ll.py
@@ -0,0 +1,289 @@
+# Copyright 2010 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/>.
+
+from chirp import chirp_common, util, errors, memmap
+import time
+
+ACK = chr(0x06)
+
+MEM_LOC_BASE = 0x00AB
+MEM_LOC_SIZE = 16
+
+POS_DUPLEX = 1
+POS_TMODE  = 2
+POS_TONE   = 2
+POS_DTCS   = 3
+POS_MODE   = 4
+POS_FREQ   = 5
+POS_OFFSET = 9
+POS_NAME   = 11
+
+POS_USED   = 0x079C
+
+CHARSET = [str(x) for x in range(0, 10)] + \
+    [chr(x) for x in range(ord("A"), ord("Z")+1)] + \
+    list(" ()+--*/???|0123456789")
+
+def send(s, data):
+    s.write(data)
+    r = s.read(len(data))
+    if len(r) != len(data):
+        raise errors.RadioError("Failed to read echo")
+
+def read_exact(s, count):
+    data = ""
+    i = 0
+    while len(data) < count:
+        if i == 3:
+            print util.hexprint(data)
+            raise errors.RadioError("Failed to read %i (%i) from radio" % (count,
+                                                                           len(data)))
+        elif i > 0:
+            print "Retry %i" % i
+        data += s.read(count - len(data))
+        i += 1
+
+    return data
+
+def download(radio):
+    data = ""
+
+    radio.pipe.setTimeout(1)
+
+    for block in radio._block_lengths:
+        print "Doing block %i" % block
+        if block > 112:
+            step = 16
+        else:
+            step = block
+        for i in range(0, block, step):
+            #data += read_exact(radio.pipe, step)
+            chunk = radio.pipe.read(step*2)
+            print "Length of chunk: %i" % len(chunk)
+            data += chunk
+            print "Reading %i" % i
+            time.sleep(0.1)
+            send(radio.pipe, ACK)
+            if radio.status_fn:
+                status = chirp_common.Status()
+                status.max = radio._memsize
+                status.cur = len(data)
+                status.msg = "Cloning from radio"
+                radio.status_fn(status)
+
+    r = radio.pipe.read(100)
+    send(radio.pipe, ACK)
+    print "R: %i" % len(r)
+    print util.hexprint(r)
+
+    print "Got: %i Expecting %i" % (len(data), radio._memsize)
+
+    return memmap.MemoryMap(data)
+
+def get_mem_offset(number):
+    return MEM_LOC_BASE + (number * MEM_LOC_SIZE)
+
+def get_raw_memory(map, number):
+    pos = get_mem_offset(number)
+    return memmap.MemoryMap(map[pos:pos+MEM_LOC_SIZE])
+
+def get_freq(mmap):
+    khz = (int("%02x" % (ord(mmap[POS_FREQ])), 10) * 100000) + \
+        (int("%02x" % ord(mmap[POS_FREQ+1]), 10) * 1000) + \
+        (int("%02x" % ord(mmap[POS_FREQ+2]), 10) * 10)
+    return khz / 10000.0
+
+def set_freq(mmap, freq):
+    val = util.bcd_encode(int(freq * 1000), width=6)[:3]
+    mmap[POS_FREQ] = val
+
+def get_tmode(mmap):
+    val = ord(mmap[POS_TMODE]) & 0xC0
+
+    tmodemap = {
+        0x00 : "",
+        0x40 : "Tone",
+        0x80 : "TSQL",
+        0xC0 : "DTCS",
+        }
+    
+    return tmodemap[val]
+
+def set_tmode(mmap, tmode):
+    val = ord(mmap[POS_TMODE]) & 0x3F
+
+    tmodemap = {
+        ""     : 0x00,
+        "Tone" : 0x40,
+        "TSQL" : 0x80,
+        "DTCS" : 0xC0,
+        }
+
+    val |= tmodemap[tmode]
+
+    mmap[POS_TMODE] = val
+
+def get_tone(mmap):
+    val = ord(mmap[POS_TONE]) & 0x3F
+
+    return chirp_common.TONES[val]
+
+def set_tone(mmap, tone):
+    val = ord(mmap[POS_TONE]) & 0xC0
+
+    mmap[POS_TONE] = val | chirp_common.TONES.index(tone)
+
+def get_dtcs(mmap):
+    val = ord(mmap[POS_DTCS])
+
+    return chirp_common.DTCS_CODES[val]
+
+def set_dtcs(mmap, dtcs):
+    mmap[POS_DTCS] = chirp_common.DTCS_CODES.index(dtcs)
+
+def get_offset(mmap):
+    khz = (int("%02x" % ord(mmap[POS_OFFSET]), 10) * 10) + \
+        (int("%02x" % (ord(mmap[POS_OFFSET+1]) >> 4), 10) * 1)
+
+    return khz / 1000.0
+
+def set_offset(mmap, offset):
+    val = util.bcd_encode(int(offset * 1000), width=4)[:3]
+    print "Offfset:\n%s"% util.hexprint(val)
+    mmap[POS_OFFSET] = val
+
+def get_duplex(mmap):
+    val = ord(mmap[POS_DUPLEX]) & 0x03
+
+    dupmap = {
+        0x00 : "",
+        0x01 : "-",
+        0x02 : "+",
+        0x03 : "split",
+        }
+
+    return dupmap[val]
+
+def set_duplex(mmap, duplex):
+    val = ord(mmap[POS_DUPLEX]) & 0xFC
+
+    dupmap = {
+        ""      : 0x00,
+        "-"     : 0x01,
+        "+"     : 0x02,
+        "split" : 0x03,
+        }
+    
+    mmap[POS_DUPLEX] = val | dupmap[duplex]
+
+def get_name(mmap):
+    name = ""
+    for x in mmap[POS_NAME:POS_NAME+4]:
+        if ord(x) >= len(CHARSET):
+            break
+        name += CHARSET[ord(x)]
+    return name
+
+def set_name(mmap, name):
+    val = ""
+    for i in name[:4].ljust(4):
+        val += chr(CHARSET.index(i))
+    mmap[POS_NAME] = val
+
+def get_mode(mmap):
+    val = ord(mmap[POS_MODE]) & 0x03
+
+    modemap = {
+        0x00 : "FM",
+        0x01 : "AM",
+        0x02 : "WFM",
+        0x03 : "WFM",
+        }
+
+    return modemap[val]
+
+def set_mode(mmap, mode):
+    val = ord(mmap[POS_MODE]) & 0xCF
+
+    modemap = {
+        "FM"  : 0x00,
+        "AM"  : 0x01,
+        "WFM" : 0x02,
+        }
+
+    mmap[POS_MODE] = val | modemap[mode]
+
+def get_used(mmap, number):
+    return ord(mmap[POS_USED + number]) & 0x01
+
+def set_used(mmap, number, used):
+    val = ord(mmap[POS_USED + number]) & 0xFC
+    if used:
+        val |= 0x03
+    mmap[POS_USED + number] = val
+    
+def get_memory(map, number):
+    index = number - 1
+    mmap = get_raw_memory(map, index)
+
+    mem = chirp_common.Memory()
+    mem.number = number
+    if not get_used(map, index):
+        mem.empty = True
+        return mem
+
+    mem.freq = get_freq(mmap)
+    mem.tmode = get_tmode(mmap)
+    mem.rtone = mem.ctone = get_tone(mmap)
+    mem.dtcs = get_dtcs(mmap)
+    mem.offset = get_offset(mmap)
+    mem.duplex = get_duplex(mmap)
+    mem.name = get_name(mmap)
+    mem.mode = get_mode(mmap)
+
+    return mem
+
+def set_memory(_map, mem):
+    index = mem.number - 1
+    mmap = get_raw_memory(_map, index)
+
+    if not get_used(_map, index):
+        mmap[0] = ("\x00" * MEM_LOC_SIZE)
+
+    set_freq(mmap, mem.freq)
+    set_tmode(mmap, mem.tmode)
+    set_tone(mmap, mem.rtone)
+    set_dtcs(mmap, mem.dtcs)
+    set_offset(mmap, mem.offset)
+    set_duplex(mmap, mem.duplex)
+    set_name(mmap, mem.name)
+    set_mode(mmap, mem.mode)
+
+    _map[get_mem_offset(index)] = mmap.get_packed()
+    set_used(_map, index, True)
+
+    return _map
+
+def erase_memory(map, number):
+    set_used(map, number-1, False)
+    return map
+
+def update_checksum(map):
+    cs = 0
+    for i in range(0, 3722):
+        cs += ord(map[i])
+    cs %= 256
+    print "Checksum old=%02x new=%02x" % (ord(map[3722]), cs)
+    map[3722] = cs
diff --git a/chirp/ft60.py b/chirp/ft60.py
new file mode 100644
index 0000000..d51d83b
--- /dev/null
+++ b/chirp/ft60.py
@@ -0,0 +1,296 @@
+# Copyright 2011 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 time
+from chirp import chirp_common, yaesu_clone, memmap, bitwise, directory
+from chirp import errors
+
+ACK = "\x06"
+
+def _send(pipe, data):
+    pipe.write(data)
+    echo = pipe.read(len(data))
+    if echo != data:
+        raise errors.RadioError("Error reading echo (Bad cable?)")
+
+def _download(radio):
+    data = ""
+    for i in range(0, 10):
+        chunk = radio.pipe.read(8)
+        if len(chunk) == 8:
+            data += chunk
+            break
+        elif chunk:
+            raise Exception("Received invalid response from radio")
+        time.sleep(1)
+        print "Trying again..."
+
+    if not data:
+        raise Exception("Radio is not responding")
+
+    _send(radio.pipe, ACK)
+
+    for i in range(0, 448):
+        chunk = radio.pipe.read(64)
+        data += chunk
+        _send(radio.pipe, ACK)
+        if len(chunk) == 1 and i == 447:
+            break
+        elif len(chunk) != 64:
+            raise Exception("Reading block %i was short (%i)" % (i, len(chunk)))
+        if radio.status_fn:
+            status = chirp_common.Status()
+            status.cur = i * 64
+            status.max = radio.get_memsize()
+            status.msg = "Cloning from radio"
+            radio.status_fn(status)
+
+    return memmap.MemoryMap(data)
+
+def _upload(radio):
+    _send(radio.pipe, radio.get_mmap()[0:8])
+
+    ack = radio.pipe.read(1)
+    if ack != ACK:
+        raise Exception("Radio did not respond")
+
+    for i in range(0, 448):
+        offset = 8 + (i * 64)
+        _send(radio.pipe, radio.get_mmap()[offset:offset+64])
+        ack = radio.pipe.read(1)
+        if ack != ACK:
+            raise Exception("Radio did not ack block %i" % i)
+
+        if radio.status_fn:
+            status = chirp_common.Status()
+            status.cur = offset+64
+            status.max = radio.get_memsize()
+            status.msg = "Cloning to radio"
+            radio.status_fn(status)            
+
+def _decode_freq(freqraw):
+    freq = int(freqraw) * 10000
+    if freq > 8000000000:
+        freq = (freq - 8000000000) + 5000
+
+    if freq > 4000000000:
+        freq -= 4000000000
+        for i in range(0, 3):
+            freq += 2500
+            if chirp_common.required_step(freq) == 12.5:
+                break
+
+    return freq
+
+def _encode_freq(freq):
+    freqraw = freq / 10000
+    if ((freq / 1000) % 10) == 5:
+        freqraw += 800000
+    if chirp_common.is_fractional_step(freq):
+        freqraw += 400000
+    return freqraw
+
+
+MEM_FORMAT = """
+#seekto 0x0238;
+struct {
+  u8 used:1,
+     unknown1:1,
+     isnarrow:1,
+     isam:1,
+     duplex:4;
+  bbcd freq[3];
+  u8 unknown2:1,
+     step:3, 
+     unknown2_1:1,
+     tmode:3;
+  bbcd tx_freq[3];
+  u8 power:2,
+     tone:6;
+  u8 unknown4:1,
+     dtcs:7;
+  u8 unknown5[2];
+  u8 offset;
+  u8 unknown6[3];
+} memory[1000];
+
+#seekto 0x6EC8;
+struct {
+  u8 skip0:2,
+     skip1:2,
+     skip2:2,
+     skip3:2;
+} flags[500];
+
+#seekto 0x4700;
+struct {
+  u8 name[6];
+  u8 use_name:1,
+     unknown1:7;
+  u8 valid:1,
+     unknown2:7;
+} names[1000];
+
+#seekto 0x6FC8;
+u8 checksum;
+"""
+
+DUPLEX = ["", "", "-", "+", "split"]
+TMODES = ["", "Tone", "TSQL", "TSQL-R", "DTCS"]
+POWER_LEVELS = [chirp_common.PowerLevel("High", watts=5.0),
+                chirp_common.PowerLevel("Mid", watts=2.5),
+                chirp_common.PowerLevel("Low", watts=1.0)]
+STEPS = [5.0, 10.0, 12.5, 15.0, 20.0, 25.0, 50.0, 100.0]
+SKIPS = ["", "P", "S"]
+CHARSET = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ [?]^__|`?$%&-()*+,-,/|;/=>?@"
+
+ at directory.register
+class FT60Radio(yaesu_clone.YaesuCloneModeRadio):
+    """Yaesu FT-60"""
+    BAUD_RATE = 9600
+    VENDOR = "Yaesu"
+    MODEL = "FT-60"
+    _model = "AH017"
+
+    _memsize = 28617
+
+    def get_features(self):
+        rf = chirp_common.RadioFeatures()
+        rf.memory_bounds = (1, 999)
+        rf.valid_duplexes = DUPLEX
+        rf.valid_tmodes = TMODES
+        rf.valid_power_levels = POWER_LEVELS
+        rf.valid_tuning_steps = STEPS
+        rf.valid_skips = SKIPS
+        rf.valid_characters = CHARSET
+        rf.valid_name_length = 6
+        rf.valid_modes = ["FM", "NFM", "AM"]
+        rf.valid_bands = [(108000000, 520000000), (700000000, 999990000)]
+        rf.can_odd_split = True
+        rf.has_ctone = False
+        rf.has_bank = False
+        rf.has_dtcs_polarity = False
+
+        return rf
+
+    def _checksums(self):
+        return [ yaesu_clone.YaesuChecksum(0x0000, 0x6FC7) ]
+
+    def sync_in(self):
+        try:
+            self._mmap = _download(self)
+        except errors.RadioError:
+            raise
+        except Exception, e:
+            raise errors.RadioError("Failed to communicate with radio: %s" % e)
+        self.process_mmap()
+
+    def sync_out(self):
+        self.update_checksums()
+        try:
+            _upload(self)
+        except errors.RadioError:
+            raise
+        except Exception, e:
+            raise errors.RadioError("Failed to communicate with radio: %s" % e)
+
+    def process_mmap(self):
+        self._memobj = bitwise.parse(MEM_FORMAT, self._mmap)
+
+    def get_raw_memory(self, number):
+        return repr(self._memobj.memory[number]) + \
+            repr(self._memobj.flags[number/4]) + \
+            repr(self._memobj.names[number])
+
+    def get_memory(self, number):
+        _mem = self._memobj.memory[number]
+        _skp = self._memobj.flags[number/4]
+        _nam = self._memobj.names[number]
+
+        skip = _skp["skip%i" % (number%4)]
+
+        mem = chirp_common.Memory()
+        mem.number = number
+
+        if not _mem.used:
+            mem.empty = True
+            return mem
+
+        mem.freq = _decode_freq(_mem.freq)
+        mem.offset = int(_mem.offset) * 50000
+
+        mem.duplex = DUPLEX[_mem.duplex]
+        if mem.duplex == "split":
+            mem.offset = _decode_freq(_mem.tx_freq)
+        mem.tmode = TMODES[_mem.tmode]
+        mem.rtone = chirp_common.TONES[_mem.tone]
+        mem.dtcs = chirp_common.DTCS_CODES[_mem.dtcs]
+        mem.power = POWER_LEVELS[_mem.power]
+        mem.mode = _mem.isam and "AM" or _mem.isnarrow and "NFM" or "FM"
+        mem.tuning_step = STEPS[_mem.step]
+        mem.skip = SKIPS[skip]
+
+        if _nam.use_name and _nam.valid:
+            for i in _nam.name:
+                if i == 0xFF:
+                    break
+                try:
+                    mem.name += CHARSET[i]
+                except IndexError:
+                    print "Memory %i: Unknown char index: %i " % (number, i)
+            mem.name = mem.name.rstrip()
+
+        return mem
+
+    def set_memory(self, mem):
+        _mem = self._memobj.memory[mem.number]
+        _skp = self._memobj.flags[mem.number/4]
+        _nam = self._memobj.names[mem.number]
+
+        if mem.empty:
+            _mem.used = False
+            return
+
+        if not _mem.used:
+            _mem.set_raw("\x00" * 16)
+            _mem.used = 1
+            print "Wiped"
+
+        _mem.freq = _encode_freq(mem.freq)
+        if mem.duplex == "split":
+            _mem.tx_freq = _encode_freq(mem.offset)
+            _mem.offset = 0
+        else:
+            _mem.tx_freq = 0
+            _mem.offset = mem.offset / 50000
+        _mem.duplex = DUPLEX.index(mem.duplex)
+        _mem.tmode = TMODES.index(mem.tmode)
+        _mem.tone = chirp_common.TONES.index(mem.rtone)
+        _mem.dtcs = chirp_common.DTCS_CODES.index(mem.dtcs)
+        _mem.power = mem.power and POWER_LEVELS.index(mem.power) or 0
+        _mem.isnarrow = mem.mode == "NFM"
+        _mem.isam = mem.mode == "AM"
+        _mem.step = STEPS.index(mem.tuning_step)
+
+        _skp["skip%i" % (mem.number%4)] = SKIPS.index(mem.skip)
+
+        for i in range(0, 6):
+            try:
+                _nam.name[i] = CHARSET.index(mem.name[i])
+            except IndexError:
+                _nam.name[i] = CHARSET.index(" ")
+            
+        _nam.use_name = mem.name.strip() and True or False
+        _nam.valid = _nam.use_name
diff --git a/chirp/ft7800.py b/chirp/ft7800.py
new file mode 100644
index 0000000..f12b95c
--- /dev/null
+++ b/chirp/ft7800.py
@@ -0,0 +1,654 @@
+# Copyright 2010 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 time
+from chirp import chirp_common, yaesu_clone, memmap, directory
+from chirp import bitwise, errors
+
+from collections import defaultdict
+
+ACK = chr(0x06)
+
+MEM_FORMAT = """
+#seekto 0x04C8;
+struct {
+  u8 used:1,
+     unknown1:1,
+     mode:2,
+     unknown2:1,
+     duplex:3;
+  bbcd freq[3];
+  u8 unknown3:1,
+     tune_step:3,
+     unknown5:2,
+     tmode:2;
+  bbcd split[3];
+  u8 power:2,
+     tone:6;
+  u8 unknown6:1,
+     dtcs:7;
+  u8 unknown7[2];
+  u8 offset;
+  u8 unknown9[3];
+} memory[1000];
+
+#seekto 0x4988;
+struct {
+  char name[6];
+  u8 enabled:1,
+     unknown1:7;
+  u8 used:1,
+     unknown2:7;
+} names[1000];
+
+#seekto 0x6c48;
+struct {
+   u32 bitmap[32];
+} bank_channels[20];
+
+#seekto 0x7648;
+struct {
+  u8 skip0:2,
+     skip1:2,
+     skip2:2,
+     skip3:2;
+} flags[250];
+
+#seekto 0x7B48;
+u8 checksum;
+"""
+
+MODES = ["FM", "AM", "NFM"]
+TMODES = ["", "Tone", "TSQL", "DTCS"]
+DUPLEX = ["", "", "-", "+", "split"]
+STEPS =  [5.0, 10.0, 12.5, 15.0, 20.0, 25.0, 50.0, 100.0]
+SKIPS = ["", "S", "P", ""]
+
+CHARSET = ["%i" % int(x) for x in range(0, 10)] + \
+    [chr(x) for x in range(ord("A"), ord("Z")+1)] + \
+    list(" " * 10) + \
+    list("*+,- /|      [ ] _") + \
+    list("\x00" * 100)
+
+POWER_LEVELS_VHF = [chirp_common.PowerLevel("Hi", watts=50),
+                    chirp_common.PowerLevel("Mid1", watts=20),
+                    chirp_common.PowerLevel("Mid2", watts=10),
+                    chirp_common.PowerLevel("Low", watts=5)]
+
+POWER_LEVELS_UHF = [chirp_common.PowerLevel("Hi", watts=35),
+                    chirp_common.PowerLevel("Mid1", watts=20),
+                    chirp_common.PowerLevel("Mid2", watts=10),
+                    chirp_common.PowerLevel("Low", watts=5)]
+
+def _send(ser, data):
+    for i in data:
+        ser.write(i)
+        time.sleep(0.002)
+    echo = ser.read(len(data))
+    if echo != data:
+        raise errors.RadioError("Error reading echo (Bad cable?)")
+
+def _download(radio):
+    data = ""
+
+    chunk = ""
+    for i in range(0, 30):
+        chunk += radio.pipe.read(radio._block_lengths[0])
+        if chunk:
+            break
+
+    if len(chunk) != radio._block_lengths[0]:
+        raise Exception("Failed to read header (%i)" % len(chunk))
+    data += chunk
+
+    _send(radio.pipe, ACK)
+
+    for i in range(0, radio._block_lengths[1], 64):
+        chunk = radio.pipe.read(64)
+        data += chunk
+        if len(chunk) != 64:
+            break
+        time.sleep(0.01)
+        _send(radio.pipe, ACK)
+        if radio.status_fn:
+            status = chirp_common.Status()
+            status.max = radio.get_memsize()
+            status.cur = i+len(chunk)
+            status.msg = "Cloning from radio"
+            radio.status_fn(status)
+
+    data += radio.pipe.read(1)
+    _send(radio.pipe, ACK)
+
+    return memmap.MemoryMap(data)
+
+def _upload(radio):
+    cur = 0
+    for block in radio._block_lengths:
+        for _i in range(0, block, 64):
+            length = min(64, block)
+            #print "i=%i length=%i range: %i-%i" % (i, length,
+            #                                       cur, cur+length)
+            _send(radio.pipe, radio.get_mmap()[cur:cur+length])
+            if radio.pipe.read(1) != ACK:
+                raise errors.RadioError("Radio did not ack block at %i" % cur)
+            cur += length
+            time.sleep(0.05)
+
+            if radio.status_fn:
+                status = chirp_common.Status()
+                status.cur = cur
+                status.max = radio.get_memsize()
+                status.msg = "Cloning to radio"
+                radio.status_fn(status)
+
+def get_freq(rawfreq):
+    """Decode a frequency that may include a fractional step flag"""
+    # Ugh.  The 0x80 and 0x40 indicate values to add to get the
+    # real frequency.  Gross.
+    if rawfreq > 8000000000:
+        rawfreq = (rawfreq - 8000000000) + 5000
+
+    if rawfreq > 4000000000:
+        rawfreq = (rawfreq - 4000000000) + 2500
+
+    return rawfreq
+
+def set_freq(freq, obj, field):
+    """Encode a frequency with any necessary fractional step flags"""
+    obj[field] = freq / 10000
+    if (freq % 1000) == 500:
+        obj[field][0].set_bits(0x40)
+
+    if (freq % 10000) >= 5000:
+        obj[field][0].set_bits(0x80)
+        
+    return freq
+
+class FTx800Radio(yaesu_clone.YaesuCloneModeRadio):
+    """Base class for FT-7800,7900,8800,8900 radios"""
+    BAUD_RATE = 9600
+    VENDOR = "Yaesu"
+
+    def get_features(self):
+        rf = chirp_common.RadioFeatures()
+        rf.memory_bounds = (1, 999)
+        rf.has_bank = False
+        rf.has_ctone = False
+        rf.has_dtcs_polarity = False
+        rf.valid_modes = MODES
+        rf.valid_tmodes = ["", "Tone", "TSQL", "DTCS"]
+        rf.valid_duplexes = ["", "-", "+", "split"]
+        rf.valid_tuning_steps = STEPS
+        rf.valid_bands = [(108000000, 520000000), (700000000, 990000000)]
+        rf.valid_skips = ["", "S", "P"]
+        rf.valid_power_levels = POWER_LEVELS_VHF
+        rf.valid_characters = "".join(CHARSET)
+        rf.valid_name_length = 6
+        rf.can_odd_split = True
+        return rf
+
+    def _checksums(self):
+        return [ yaesu_clone.YaesuChecksum(0x0000, 0x7B47) ]
+
+    def sync_in(self):
+        start = time.time()
+        try:
+            self._mmap = _download(self)
+        except errors.RadioError:
+            raise
+        except Exception, e:
+            raise errors.RadioError("Failed to communicate with radio: %s" % e)
+        print "Download finished in %i seconds" % (time.time() - start)
+        self.check_checksums()
+        self.process_mmap()
+
+    def process_mmap(self):
+        self._memobj = bitwise.parse(MEM_FORMAT, self._mmap)
+
+    def sync_out(self):
+        self.update_checksums()
+        start = time.time()
+        try:
+            _upload(self)
+        except errors.RadioError:
+            raise
+        except Exception, e:
+            raise errors.RadioError("Failed to communicate with radio: %s" % e)
+        print "Upload finished in %i seconds" % (time.time() - start)
+
+    def get_raw_memory(self, number):
+        return repr(self._memobj.memory[number-1])
+
+    def _get_mem_offset(self, mem, _mem):
+        if mem.duplex == "split":
+            return get_freq(int(_mem.split) * 10000)
+        else:
+            return (_mem.offset * 5) * 10000
+
+    def _set_mem_offset(self, mem, _mem):
+        if mem.duplex == "split":
+            set_freq(mem.offset, _mem, "split")
+        else:
+            _mem.offset = (int(mem.offset / 10000) / 5)
+
+    def _get_mem_name(self, mem, _mem):
+        _nam = self._memobj.names[mem.number - 1]
+
+        name = ""
+        if _nam.used:
+            for i in str(_nam.name):
+                name += CHARSET[ord(i)]
+
+        return name.rstrip()
+
+    def _set_mem_name(self, mem, _mem):
+        _nam = self._memobj.names[mem.number - 1]
+
+        if mem.name.rstrip():
+            name = [chr(CHARSET.index(x)) for x in mem.name.ljust(6)[:6]]
+            _nam.name = "".join(name)
+            _nam.used = 1
+            _nam.enabled = 1
+        else:
+            _nam.used = 0
+            _nam.enabled = 0
+
+    def _get_mem_skip(self, mem, _mem):
+        _flg = self._memobj.flags[(mem.number - 1) / 4]
+        flgidx = (mem.number - 1) % 4
+        return SKIPS[_flg["skip%i" % flgidx]]
+
+    def _set_mem_skip(self, mem, _mem):
+        _flg = self._memobj.flags[(mem.number - 1) / 4]
+        flgidx = (mem.number - 1) % 4
+        _flg["skip%i" % flgidx] = SKIPS.index(mem.skip)
+
+    def get_memory(self, number):
+        _mem = self._memobj.memory[number - 1]
+
+        mem = chirp_common.Memory()
+        mem.number = number
+        mem.empty = not _mem.used
+        if mem.empty:
+            return mem
+
+        mem.freq = get_freq(int(_mem.freq) * 10000)
+        mem.rtone = chirp_common.TONES[_mem.tone]
+        mem.tmode = TMODES[_mem.tmode]
+        mem.mode = MODES[_mem.mode]
+        mem.dtcs = chirp_common.DTCS_CODES[_mem.dtcs]
+        if self.get_features().has_tuning_step:
+            mem.tuning_step = STEPS[_mem.tune_step]
+        mem.duplex = DUPLEX[_mem.duplex]
+        mem.offset = self._get_mem_offset(mem, _mem)
+        mem.name = self._get_mem_name(mem, _mem)
+
+        if int(mem.freq / 100) == 4:
+            mem.power = POWER_LEVELS_UHF[_mem.power]
+        else:
+            mem.power = POWER_LEVELS_VHF[_mem.power]
+
+        mem.skip = self._get_mem_skip(mem, _mem)
+
+        return mem
+
+    def set_memory(self, mem):
+        _mem = self._memobj.memory[mem.number - 1]
+
+        _mem.used = int(not mem.empty)
+        if mem.empty:
+            return
+
+        set_freq(mem.freq, _mem, "freq")
+        _mem.tone = chirp_common.TONES.index(mem.rtone)
+        _mem.tmode = TMODES.index(mem.tmode)
+        _mem.mode = MODES.index(mem.mode)
+        _mem.dtcs = chirp_common.DTCS_CODES.index(mem.dtcs)
+        if self.get_features().has_tuning_step:
+            _mem.tune_step = STEPS.index(mem.tuning_step)
+        _mem.duplex = DUPLEX.index(mem.duplex)
+        _mem.split = mem.duplex == "split" and int (mem.offset / 10000) or 0
+        if mem.power:
+            _mem.power = POWER_LEVELS_VHF.index(mem.power)
+        else:
+            _mem.power = 0
+        _mem.unknown5 = 0 # Make sure we don't leave garbage here
+
+        # NB: Leave offset after mem name for the 8800!
+        self._set_mem_name(mem, _mem)
+        self._set_mem_offset(mem, _mem)
+
+        self._set_mem_skip(mem, _mem)
+
+class FT7800BankModel(chirp_common.BankModel):
+    """Yaesu FT-7800/7900 bank model"""
+    def __init__(self, radio):
+        chirp_common.BankModel.__init__(self, radio)
+        self.__b2m_cache = defaultdict(list)
+        self.__m2b_cache = defaultdict(list)
+
+    def __precache(self):
+        if self.__b2m_cache:
+            return
+
+        for bank in self.get_banks():
+            self.__b2m_cache[bank.index] = self._get_bank_memories(bank)
+            for memnum in self.__b2m_cache[bank.index]:
+                self.__m2b_cache[memnum].append(bank.index)
+
+    def get_num_banks(self):
+        return 20
+
+    def get_banks(self):
+        banks = []
+        for i in range(0, self.get_num_banks()):
+            bank = chirp_common.Bank(self, "%i" % i, "BANK-%i" % (i + 1))
+            bank.index = i
+            banks.append(bank)
+
+        return banks
+
+    def add_memory_to_bank(self, memory, bank):
+        self.__precache()
+
+        index = memory.number - 1
+        _bitmap = self._radio._memobj.bank_channels[bank.index]
+        ishft = 31 - (index % 32)
+        _bitmap.bitmap[index / 32] |= (1 << ishft)
+        self.__m2b_cache[memory.number].append(bank.index)
+        self.__b2m_cache[bank.index].append(memory.number)
+
+    def remove_memory_from_bank(self, memory, bank):
+        self.__precache()
+
+        index = memory.number - 1
+        _bitmap = self._radio._memobj.bank_channels[bank.index]
+        ishft = 31 - (index % 32)
+        if not (_bitmap.bitmap[index / 32] & (1 << ishft)):
+            raise Exception("Memory {num} is " +
+                            "not in bank {bank}".format(num=memory.number,
+                                                           bank=bank))
+        _bitmap.bitmap[index / 32] &= ~(1 << ishft)
+        self.__b2m_cache[bank.index].remove(memory.number)
+        self.__m2b_cache[memory.number].remove(bank.index)
+
+    def _get_bank_memories(self, bank):
+        memories = []
+        upper = self._radio.get_features().memory_bounds[1]
+        for i in range(0, upper):
+            _bitmap = self._radio._memobj.bank_channels[bank.index].bitmap[i/32]
+            ishft = 31 - (i % 32)
+            if _bitmap & (1 << ishft):
+                memories.append(i + 1)
+        return memories
+
+    def get_bank_memories(self, bank):
+        self.__precache()
+
+        return [self._radio.get_memory(n)
+                for n in self.__b2m_cache[bank.index]]
+
+    def get_memory_banks(self, memory):
+        self.__precache()
+
+        _banks = self.get_banks()
+        return [_banks[b] for b in self.__m2b_cache[memory.number]]
+
+ at directory.register
+class FT7800Radio(FTx800Radio):
+    """Yaesu FT-7800"""
+    MODEL = "FT-7800"
+
+    _model = "AH016"
+    _memsize = 31561
+
+    def get_bank_model(self):
+        return FT7800BankModel(self)
+
+    def get_features(self):
+        rf = FTx800Radio.get_features(self)
+        rf.has_bank = True
+        return rf
+
+    def set_memory(self, memory):
+        if memory.empty:
+            self._wipe_memory_banks(memory)
+        FTx800Radio.set_memory(self, memory)
+
+class FT7900Radio(FT7800Radio):
+    """Yaesu FT-7900"""
+    MODEL = "FT-7900"
+
+MEM_FORMAT_8800 = """
+#seekto 0x%X;
+struct {
+  u8 used:1,
+     unknown1:1,
+     mode:2,
+     unknown2:1,
+     duplex:3;
+  bbcd freq[3];
+  u8 unknown3:1,
+     tune_step:3,
+     power:2,
+     tmode:2;
+  bbcd split[3];
+  u8 nameused:1,
+     unknown5:1,
+     tone:6;
+  u8 namevalid:1,
+     dtcs:7;
+  u8 name[6];
+} memory[500];
+
+#seekto 0x51C8;
+struct {
+  u8 skip0:2,
+     skip1:2,
+     skip2:2,
+     skip3:2;
+} flags[250];
+
+#seekto 0x%X;
+struct {
+   u32 bitmap[16];
+} bank_channels[10];
+
+
+#seekto 0x7B48;
+u8 checksum;
+"""
+
+class FT8800BankModel(FT7800BankModel):
+    def get_num_banks(self):
+        return 10
+
+ at directory.register
+class FT8800Radio(FTx800Radio):
+    """Base class for Yaesu FT-8800"""
+    MODEL = "FT-8800"
+
+    _model = "AH018"
+    _memsize = 22217
+
+    _block_lengths = [8, 22208, 1]
+    _block_size = 64
+
+    _memstart = 0x0000
+
+    def get_features(self):
+        rf = FTx800Radio.get_features(self)
+        rf.has_sub_devices = self.VARIANT == ""
+        rf.has_bank = True
+        rf.memory_bounds = (1, 500)
+        return rf
+
+    def get_sub_devices(self):
+        return [FT8800RadioLeft(self._mmap), FT8800RadioRight(self._mmap)]
+
+    def get_bank_model(self):
+        return FT8800BankModel(self)
+
+    def _checksums(self):
+        return [ yaesu_clone.YaesuChecksum(0x0000, 0x56C7) ]
+
+    def process_mmap(self):
+        if not self._memstart:
+            return
+
+        self._memobj = bitwise.parse(MEM_FORMAT_8800 % (self._memstart,
+                                                        self._bankstart),
+                                     self._mmap)
+
+    def _get_mem_offset(self, mem, _mem):
+        if mem.duplex == "split":
+            return get_freq(int(_mem.split) * 10000)
+
+        # The offset is packed into the upper two bits of the last four
+        # bytes of the name (?!)
+        val = 0
+        for i in _mem.name[2:6]:
+            val <<= 2
+            val |= ((i & 0xC0) >> 6)
+
+        return (val * 5) * 10000
+
+    def _set_mem_offset(self, mem, _mem):
+        if mem.duplex == "split":
+            set_freq(mem.offset, _mem, "split")
+            return
+
+        val = int(mem.offset / 10000) / 5
+        for i in reversed(range(2, 6)):
+            _mem.name[i] = (_mem.name[i] & 0x3F) | ((val & 0x03) << 6)
+            val >>= 2
+
+    def _get_mem_name(self, mem, _mem):
+        name = ""
+        if _mem.namevalid:
+            for i in _mem.name:
+                index = int(i) & 0x3F
+                if index < len(CHARSET):
+                    name += CHARSET[index]
+
+        return name.rstrip()
+
+    def _set_mem_name(self, mem, _mem):
+        _mem.name = [CHARSET.index(x) for x in mem.name.ljust(6)[:6]]
+        _mem.namevalid = 1
+        _mem.nameused = bool(mem.name.rstrip())
+
+class FT8800RadioLeft(FT8800Radio):
+    """Yaesu FT-8800 Left VFO subdevice"""
+    VARIANT = "Left"
+    _memstart = 0x0948
+    _bankstart = 0x4BC8
+
+
+class FT8800RadioRight(FT8800Radio):
+    """Yaesu FT-8800 Right VFO subdevice"""
+    VARIANT = "Right"
+    _memstart = 0x2948
+    _bankstart = 0x4BC8
+
+MEM_FORMAT_8900 = """
+#seekto 0x0708;
+struct {
+  u8 used:1,
+     skip:2,
+     sub_used:1,
+     unknown2:1,
+     duplex:3;
+  bbcd freq[3];
+  u8 mode:2,
+     nameused:1,
+     unknown4:1,
+     power:2,
+     tmode:2;
+  bbcd split[3];
+  u8 unknown5:2,
+     tone:6;
+  u8 namevalid:1,
+     dtcs:7;
+  u8 name[6];
+} memory[799];
+
+#seekto 0x51C8;
+struct {
+  u8 skip0:2,
+     skip1:2,
+     skip2:2,
+     skip3:2;
+} flags[400];
+
+#seekto 0x7B48;
+u8 checksum;
+"""
+
+ at directory.register
+class FT8900Radio(FT8800Radio):
+    """Yaesu FT-8900"""
+    MODEL = "FT-8900"
+
+    _model = "AH008"
+    _memsize = 14793
+    _block_lengths = [8, 14784, 1]
+
+    def process_mmap(self):
+        self._memobj = bitwise.parse(MEM_FORMAT_8900, self._mmap)
+
+    def get_features(self):
+        rf = FT8800Radio.get_features(self)
+        rf.has_sub_devices = False
+        rf.has_bank = False
+        rf.valid_modes = MODES
+        rf.valid_bands = [( 28000000,  29700000),
+                          ( 50000000,  54000000),
+                          (108000000, 180000000),
+                          (320000000, 480000000),
+                          (700000000, 985000000)]
+        rf.memory_bounds = (1, 799)
+        rf.has_tuning_step = False
+
+        return rf
+
+    def _checksums(self):
+        return [ yaesu_clone.YaesuChecksum(0x0000, 0x39C7) ]
+
+    def _get_mem_skip(self, mem, _mem):
+        return SKIPS[_mem.skip]
+
+    def _set_mem_skip(self, mem, _mem):
+        _mem.skip = SKIPS.index(mem.skip)
+
+    def get_memory(self, number):
+        mem = FT8800Radio.get_memory(self, number)
+
+        _mem = self._memobj.memory[number - 1]
+
+        return mem
+
+    def set_memory(self, mem):
+        FT8800Radio.set_memory(self, mem)
+
+        # The 8900 has a bit flag that tells the radio whether or not
+        # the memory should show up on the sub (right) band
+        _mem = self._memobj.memory[mem.number - 1]
+        if mem.freq < 108000000 or mem.freq > 480000000:
+            _mem.sub_used = 0
+        else:
+            _mem.sub_used = 1
+
diff --git a/chirp/ft817.py b/chirp/ft817.py
new file mode 100644
index 0000000..0fb3502
--- /dev/null
+++ b/chirp/ft817.py
@@ -0,0 +1,1123 @@
+#
+# Copyright 2012 Filippi Marco <iz3gme.marco at gmail.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/>.
+
+"""FT817 - FT817ND - FT817ND/US management module"""
+
+from chirp import chirp_common, yaesu_clone, util, memmap, errors, directory
+from chirp import bitwise
+from chirp.settings import RadioSetting, RadioSettingGroup, \
+    RadioSettingValueInteger, RadioSettingValueList, \
+    RadioSettingValueBoolean, RadioSettingValueString
+import time, os
+
+CMD_ACK = 0x06
+
+ at directory.register
+class FT817Radio(yaesu_clone.YaesuCloneModeRadio):
+    """Yaesu FT-817"""
+    BAUD_RATE = 9600
+    MODEL = "FT-817"
+    _model = ""
+
+    DUPLEX = ["", "-", "+", "split"]
+    # narrow modes has to be at end
+    MODES  = ["LSB", "USB", "CW", "CWR", "AM", "FM", "DIG", "PKT", "NCW",
+              "NCWR", "NFM"]
+    TMODES = ["", "Tone", "TSQL", "DTCS"]
+    STEPSFM = [5.0, 6.25, 10.0, 12.5, 15.0, 20.0, 25.0, 50.0]
+    STEPSAM = [2.5, 5.0, 9.0, 10.0, 12.5, 25.0]
+    STEPSSSB = [1.0, 2.5, 5.0]
+
+    # warning ranges has to be in this exact order
+    VALID_BANDS = [(100000, 33000000), (33000000, 56000000),
+                   (76000000, 108000000), (108000000, 137000000),
+                   (137000000, 154000000), (420000000, 470000000)] 
+
+    CHARSET = [chr(x) for x in range(0, 256)]
+
+    # Hi not used in memory
+    POWER_LEVELS = [chirp_common.PowerLevel("Hi", watts=5.00),
+                    chirp_common.PowerLevel("L3", watts=2.50),
+                    chirp_common.PowerLevel("L2", watts=1.00),
+                    chirp_common.PowerLevel("L1", watts=0.5)]
+
+    _memsize = 6509
+    # block 9 (130 Bytes long) is to be repeted 40 times
+    _block_lengths = [ 2, 40, 208, 182, 208, 182, 198, 53, 130, 118, 118]
+
+    MEM_FORMAT = """
+        struct mem_struct {
+            u8  tag_on_off:1,
+                tag_default:1,
+                unknown1:3,
+                mode:3;
+            u8  duplex:2,
+                is_duplex:1,
+                is_cwdig_narrow:1,
+                is_fm_narrow:1,
+                freq_range:3;
+            u8  skip:1,
+                unknown2:1,
+                ipo:1,
+                att:1,
+                unknown3:4;
+            u8  ssb_step:2,
+                am_step:3,
+                fm_step:3;
+            u8  unknown4:6,
+                tmode:2;
+            u8  unknown5:2,
+                tx_mode:3,
+                tx_freq_range:3;
+            u8  unknown6:1,
+                unknown_toneflag:1,
+                tone:6;
+            u8  unknown7:1,
+                dcs:7;
+            ul16 rit;
+            u32 freq;
+            u32 offset;
+            u8  name[8];
+        };
+        
+        #seekto 0x4;
+        struct {
+            u8  fst:1,
+                lock:1,
+                nb:1,
+                pbt:1,
+                unknownb:1,
+                dsp:1,
+                agc:2;
+            u8  vox:1,
+                vlt:1,
+                bk:1,
+                kyr:1,
+                unknown5:1,
+                cw_paddle:1,
+                pwr_meter_mode:2;
+            u8  vfob_band_select:4,
+                vfoa_band_select:4;
+            u8  unknowna;
+            u8  backlight:2,
+                color:2,
+                contrast:4;
+            u8  beep_freq:1,
+                beep_volume:7;
+            u8  arts_beep:2,
+                main_step:1,
+                cw_id:1,
+                scope:1,
+                pkt_rate:1,
+                resume_scan:2;
+            u8  op_filter:2,
+                lock_mode:2,
+                cw_pitch:4;
+            u8  sql_rf_gain:1,
+                ars_144:1,
+                ars_430:1,
+                cw_weight:5;
+            u8  cw_delay;
+            u8  unknown8:1,
+                sidetone:7;
+            u8  batt_chg:2,
+                cw_speed:6;
+            u8  disable_amfm_dial:1,
+                vox_gain:7;
+            u8  cat_rate:2,
+                emergency:1,
+                vox_delay:5;
+            u8  dig_mode:3,
+                mem_group:1,
+                unknown9:1,
+                apo_time:3;
+            u8  dcs_inv:2,
+                unknown10:1,
+                tot_time:5;
+            u8  mic_scan:1,
+                ssb_mic:7;
+            u8  mic_key:1,
+                am_mic:7;
+            u8  unknown11:1,
+                fm_mic:7;
+            u8  unknown12:1,
+                dig_mic:7;
+            u8  extended_menu:1,
+                pkt_mic:7;
+            u8  unknown14:1,
+                pkt9600_mic:7;
+            ul16 dig_shift;
+            ul16 dig_disp;
+            u8  r_lsb_car;
+            u8  r_usb_car;
+            u8  t_lsb_car;
+            u8  t_usb_car;
+            u8  unknown15:2,
+                menu_item:6;
+            u8  unknown16:4,
+                menu_sel:4;
+            u16 unknown17;
+            u8  art:1,
+                scn_mode:2,
+                dw:1,
+                pri:1,
+                unknown18:1,
+                tx_power:2;
+            u8  spl:1,
+                unknown:1,
+                uhf_antenna:1,
+                vhf_antenna:1,
+                air_antenna:1,
+                bc_antenna:1,
+                sixm_antenna:1,
+                hf_antenna:1;
+        } settings;
+
+        #seekto 0x2A;
+        struct mem_struct vfoa[15];
+        struct mem_struct vfob[15];
+        struct mem_struct home[4];
+        struct mem_struct qmb;
+        struct mem_struct mtqmb;
+        struct mem_struct mtune;
+        
+        #seekto 0x3FD;
+        u8 visible[25];
+        u8 pmsvisible;
+        
+        #seekto 0x417;
+        u8 filled[25];
+        u8 pmsfilled;
+        
+        #seekto 0x431;
+        struct mem_struct memory[200];
+        struct mem_struct pms[2];
+
+        #seekto 0x18cf;
+        u8 callsign[7];
+
+        #seekto 0x1979;
+        struct mem_struct sixtymeterchannels[5];
+    """
+    _CALLSIGN_CHARSET = [chr(x) for x in range(ord("0"), ord("9")+1) +
+                                        range(ord("A"), ord("Z")+1) + 
+                                        [ord(" ")]]
+    _CALLSIGN_CHARSET_REV = dict(zip(_CALLSIGN_CHARSET,
+                                    range(0,len(_CALLSIGN_CHARSET))))
+
+    # WARNING Index are hard wired in memory management code !!!
+    SPECIAL_MEMORIES = {
+        "VFOa-1.8M" : -35,
+        "VFOa-3.5M" : -34,
+        "VFOa-7M" : -33,
+        "VFOa-10M" : -32,
+        "VFOa-14M" : -31,
+        "VFOa-18M" : -30,
+        "VFOa-21M" : -29,
+        "VFOa-24M" : -28,
+        "VFOa-28M" : -27,
+        "VFOa-50M" : -26,
+        "VFOa-FM" : -25,
+        "VFOa-AIR" : -24,
+        "VFOa-144" : -23,
+        "VFOa-430" : -22,
+        "VFOa-HF" : -21,
+        "VFOb-1.8M" : -20,
+        "VFOb-3.5M" : -19,
+        "VFOb-7M" : -18,
+        "VFOb-10M" : -17,
+        "VFOb-14M" : -16,
+        "VFOb-18M" : -15,
+        "VFOb-21M" : -14,
+        "VFOb-24M" : -13,
+        "VFOb-28M" : -12,
+        "VFOb-50M" : -11,
+        "VFOb-FM" : -10,
+        "VFOb-AIR" : -9,
+        "VFOb-144M" : -8,
+        "VFOb-430M" : -7,
+        "VFOb-HF" : -6,
+        "HOME HF" : -5,
+        "HOME 50M" : -4,
+        "HOME 144M" : -3,
+        "HOME 430M" : -2,
+        "QMB" : -1,
+    }
+    FIRST_VFOB_INDEX = -6
+    LAST_VFOB_INDEX = -20
+    FIRST_VFOA_INDEX = -21
+    LAST_VFOA_INDEX = -35
+
+    SPECIAL_PMS = {
+        "PMS-L" : -37,
+        "PMS-U" : -36,
+    }
+    LAST_PMS_INDEX = -37
+
+    SPECIAL_MEMORIES.update(SPECIAL_PMS)
+    
+    SPECIAL_MEMORIES_REV = dict(zip(SPECIAL_MEMORIES.values(),
+                                    SPECIAL_MEMORIES.keys()))
+
+    def _read(self, block, blocknum):
+        for _i in range(0, 60):
+            data = self.pipe.read(block+2)
+            if data:
+                break
+            time.sleep(0.5)
+        if len(data) == block+2 and data[0] == chr(blocknum):
+            checksum = yaesu_clone.YaesuChecksum(1, block)
+            if checksum.get_existing(data) != \
+                    checksum.get_calculated(data):
+                raise Exception("Checksum Failed [%02X<>%02X] block %02X" %
+                                    (checksum.get_existing(data),
+                                    checksum.get_calculated(data), blocknum))
+            data = data[1:block+1] # Chew away the block number and the checksum
+        else:
+            raise Exception("Unable to read block %02X expected %i got %i" %
+                                (blocknum, block+2, len(data)))
+    
+        if os.getenv("CHIRP_DEBUG"):
+            print "Read %i" % len(data)
+        return data        
+    
+    def _clone_in(self):
+        # Be very patient with the radio
+        self.pipe.setTimeout(2)
+    
+        start = time.time()
+    
+        data = ""
+        blocks = 0
+        status = chirp_common.Status()
+        status.msg = "Cloning from radio"
+        status.max = len(self._block_lengths) + 39
+        for block in self._block_lengths:
+            if blocks == 8:
+                # repeated read of 40 block same size (memory area)
+                repeat = 40
+            else:
+                repeat = 1
+            for _i in range(0, repeat):	
+                data += self._read(block, blocks)
+                self.pipe.write(chr(CMD_ACK))
+                blocks += 1
+                status.cur = blocks
+                self.status_fn(status)
+    
+        print "Clone completed in %i seconds" % (time.time() - start)
+    
+        return memmap.MemoryMap(data)
+    
+    def _clone_out(self):
+        delay = 0.5
+        start = time.time()
+    
+        blocks = 0
+        pos = 0
+        status = chirp_common.Status()
+        status.msg = "Cloning to radio"
+        status.max = len(self._block_lengths) + 39
+        for block in self._block_lengths:
+            if blocks == 8:
+                # repeated read of 40 block same size (memory area)
+                repeat = 40
+            else:
+                repeat = 1
+            for _i in range(0, repeat):
+                time.sleep(0.01)
+                checksum = yaesu_clone.YaesuChecksum(pos, pos+block-1)
+                if os.getenv("CHIRP_DEBUG"):
+                    print "Block %i - will send from %i to %i byte " % \
+                        (blocks,
+                         pos,
+                         pos + block)
+                    print util.hexprint(chr(blocks))
+                    print util.hexprint(self.get_mmap()[pos:pos+block])
+                    print util.hexprint(chr(checksum.get_calculated(
+                                self.get_mmap())))
+                self.pipe.write(chr(blocks))
+                self.pipe.write(self.get_mmap()[pos:pos+block])
+                self.pipe.write(chr(checksum.get_calculated(self.get_mmap())))
+                buf = self.pipe.read(1)
+                if not buf or buf[0] != chr(CMD_ACK):
+                    time.sleep(delay)
+                    buf = self.pipe.read(1)
+                if not buf or buf[0] != chr(CMD_ACK):
+                    if os.getenv("CHIRP_DEBUG"):
+                        print util.hexprint(buf)
+                    raise Exception("Radio did not ack block %i" % blocks)
+                pos += block
+                blocks += 1
+                status.cur = blocks
+                self.status_fn(status)
+    
+        print "Clone completed in %i seconds" % (time.time() - start)
+    
+    def sync_in(self):
+        try:
+            self._mmap = self._clone_in()
+        except errors.RadioError:
+            raise
+        except Exception, e:
+            raise errors.RadioError("Failed to communicate with radio: %s" % e)
+        self.process_mmap()
+
+    def sync_out(self):
+        try:
+            self._clone_out()
+        except errors.RadioError:
+            raise
+        except Exception, e:
+            raise errors.RadioError("Failed to communicate with radio: %s" % e)
+
+    def process_mmap(self):
+        self._memobj = bitwise.parse(self.MEM_FORMAT, self._mmap)
+
+    def get_features(self):
+        rf = chirp_common.RadioFeatures()
+        rf.has_bank = False
+        rf.has_dtcs_polarity = False
+        rf.has_nostep_tuning = True
+        rf.valid_modes = list(set(self.MODES))
+        rf.valid_tmodes = list(self.TMODES)
+        rf.valid_duplexes = list(self.DUPLEX)
+        rf.valid_tuning_steps = list(self.STEPSFM)
+        rf.valid_bands = self.VALID_BANDS
+        rf.valid_skips = ["", "S"]
+        rf.valid_power_levels = self.POWER_LEVELS
+        rf.valid_characters = "".join(self.CHARSET)
+        rf.valid_name_length = 8
+        rf.valid_special_chans = sorted(self.SPECIAL_MEMORIES.keys())
+        rf.memory_bounds = (1, 200)
+        rf.can_odd_split = True
+        rf.has_ctone = False
+        rf.has_settings = True
+        return rf
+
+    def get_raw_memory(self, number):
+        return repr(self._memobj.memory[number-1])
+
+    def _get_duplex(self, mem, _mem):
+        if _mem.is_duplex == 1:
+            mem.duplex = self.DUPLEX[_mem.duplex]
+        else:
+            mem.duplex = ""
+
+    def _get_tmode(self, mem, _mem):
+        mem.tmode = self.TMODES[_mem.tmode]
+        mem.rtone = chirp_common.TONES[_mem.tone]
+        mem.dtcs = chirp_common.DTCS_CODES[_mem.dcs]
+
+    def _set_duplex(self, mem, _mem):
+        _mem.duplex = self.DUPLEX.index(mem.duplex)
+        _mem.is_duplex = mem.duplex != ""
+
+    def _set_tmode(self, mem, _mem):
+        _mem.tmode = self.TMODES.index(mem.tmode)
+        # have to put this bit to 0 otherwise we get strange display in tone
+        # frequency (menu 83). See bug #88 and #163
+        _mem.unknown_toneflag = 0
+        _mem.tone = chirp_common.TONES.index(mem.rtone)
+        _mem.dcs = chirp_common.DTCS_CODES.index(mem.dtcs)
+
+    def get_memory(self, number):
+        if isinstance(number, str):
+            return self._get_special(number)
+        elif number < 0:
+            # I can't stop delete operation from loosing extd_number but
+            # I know how to get it back
+            return self._get_special(self.SPECIAL_MEMORIES_REV[number])
+        else:
+            return self._get_normal(number)
+
+    def set_memory(self, memory):
+        if memory.number < 0:
+            return self._set_special(memory)
+        else:
+            return self._set_normal(memory)
+
+    def _get_special(self, number):
+        mem = chirp_common.Memory()
+        mem.number = self.SPECIAL_MEMORIES[number]
+        mem.extd_number = number
+
+        if mem.number in range(self.FIRST_VFOA_INDEX,
+                               self.LAST_VFOA_INDEX - 1,
+                               -1):
+            _mem = self._memobj.vfoa[-self.LAST_VFOA_INDEX + mem.number]
+            immutable = ["number", "skip", "rtone", "ctone", "extd_number",
+                         "name", "dtcs_polarity", "power", "comment"]
+        elif mem.number in range(self.FIRST_VFOB_INDEX,
+                                 self.LAST_VFOB_INDEX - 1,
+                                 -1):
+            _mem = self._memobj.vfob[-self.LAST_VFOB_INDEX + mem.number]
+            immutable = ["number", "skip", "rtone", "ctone", "extd_number",
+                         "name", "dtcs_polarity", "power", "comment"]
+        elif mem.number in range(-2, -6, -1):
+            _mem = self._memobj.home[5 + mem.number]
+            immutable = ["number", "skip", "rtone", "ctone", "extd_number",
+                         "dtcs_polarity", "power", "comment"]
+        elif mem.number == -1:
+            _mem = self._memobj.qmb
+            immutable = ["number", "skip", "rtone", "ctone", "extd_number",
+                         "name", "dtcs_polarity", "power", "comment"]
+        elif mem.number in self.SPECIAL_PMS.values():
+            bitindex = -self.LAST_PMS_INDEX + mem.number
+            used = (self._memobj.pmsvisible >> bitindex) & 0x01
+            valid = (self._memobj.pmsfilled >> bitindex) & 0x01
+            if not used:
+                mem.empty = True
+            if not valid:
+                mem.empty = True
+                return mem
+            _mem = self._memobj.pms[-self.LAST_PMS_INDEX + mem.number]
+            immutable = ["number", "skip", "rtone", "ctone", "extd_number", 
+                         "dtcs", "tmode", "cross_mode", "dtcs_polarity", 
+                         "power", "duplex", "offset", "comment"]
+        else:
+            raise Exception("Sorry, special memory index %i " % mem.number +
+                            "unknown you hit a bug!!")
+
+        mem = self._get_memory(mem, _mem)
+        mem.immutable = immutable
+
+        return mem
+
+    def _set_special(self, mem):
+        if mem.empty and not mem.number in self.SPECIAL_PMS.values():
+            # can't delete special memories!
+            raise Exception("Sorry, special memory can't be deleted")
+
+        cur_mem = self._get_special(self.SPECIAL_MEMORIES_REV[mem.number])
+
+        # TODO add frequency range check for vfo and home memories
+        if mem.number in range(self.FIRST_VFOA_INDEX,
+                               self.LAST_VFOA_INDEX -1,
+                               -1):
+            _mem = self._memobj.vfoa[-self.LAST_VFOA_INDEX + mem.number]
+        elif mem.number in range(self.FIRST_VFOB_INDEX,
+                                 self.LAST_VFOB_INDEX -1,
+                                 -1):
+            _mem = self._memobj.vfob[-self.LAST_VFOB_INDEX + mem.number]
+        elif mem.number in range(-2, -6, -1):
+            _mem = self._memobj.home[5 + mem.number]
+        elif mem.number == -1:
+            _mem = self._memobj.qmb
+        elif mem.number in self.SPECIAL_PMS.values():
+            # this case has to be last because 817 pms keys overlap with
+            # 857 derived class other special memories
+            bitindex = -self.LAST_PMS_INDEX + mem.number
+            wasused = (self._memobj.pmsvisible >> bitindex) & 0x01
+            wasvalid = (self._memobj.pmsfilled >> bitindex) & 0x01
+            if mem.empty:
+                if wasvalid and not wasused:
+                    # pylint get confused by &= operator
+                    self._memobj.pmsfilled = self._memobj.pmsfilled & \
+                        ~ (1 << bitindex)
+                # pylint get confused by &= operator
+                self._memobj.pmsvisible = self._memobj.pmsvisible & \
+                    ~ (1 << bitindex)
+                return
+            # pylint get confused by |= operator
+            self._memobj.pmsvisible = self._memobj.pmsvisible | 1 << bitindex
+            self._memobj.pmsfilled = self._memobj.pmsfilled | 1 << bitindex
+            _mem = self._memobj.pms[-self.LAST_PMS_INDEX + mem.number]
+        else:
+            raise Exception("Sorry, special memory index %i " % mem.number +
+                            "unknown you hit a bug!!")
+
+        for key in cur_mem.immutable:
+            if cur_mem.__dict__[key] != mem.__dict__[key]:
+                raise errors.RadioError("Editing field `%s' " % key +
+                                        "is not supported on this channel")
+
+        self._set_memory(mem, _mem)
+
+    def _get_normal(self, number):
+        _mem = self._memobj.memory[number-1]
+        used = (self._memobj.visible[(number-1)/8] >> (number-1)%8) & 0x01
+        valid = (self._memobj.filled[(number-1)/8] >> (number-1)%8) & 0x01
+
+        mem = chirp_common.Memory()
+        mem.number = number
+        if not used:
+            mem.empty = True
+        if not valid:
+            mem.empty = True
+            return mem
+
+        return self._get_memory(mem, _mem)
+
+    def _set_normal(self, mem):
+        _mem = self._memobj.memory[mem.number-1]
+        wasused = (self._memobj.visible[(mem.number - 1) / 8] >>
+                       (mem.number - 1) % 8) & 0x01
+        wasvalid = (self._memobj.filled[(mem.number - 1) / 8] >>
+                        (mem.number - 1) % 8) & 0x01
+
+        if mem.empty:
+            if mem.number == 1:
+                # as Dan says "yaesus are not good about that :("
+                # if you ulpoad an empty image you can brick your radio
+                raise Exception("Sorry, can't delete first memory") 
+            if wasvalid and not wasused:
+                self._memobj.filled[(mem.number-1) / 8] &= \
+                    ~(1 << (mem.number - 1) % 8)
+                _mem.set_raw("\xFF" * (_mem.size() / 8)) # clean up
+            self._memobj.visible[(mem.number-1) / 8] &= \
+                ~(1 << (mem.number - 1) % 8)
+            return
+        if not wasvalid:
+            _mem.set_raw("\x00" * (_mem.size() / 8)) # clean up
+        
+        self._memobj.visible[(mem.number - 1) / 8] |= 1 << (mem.number - 1) % 8
+        self._memobj.filled[(mem.number - 1) / 8] |= 1 << (mem.number - 1) % 8
+        self._set_memory(mem, _mem)
+
+    def _get_memory(self, mem, _mem):
+        mem.freq = int(_mem.freq) * 10
+        mem.offset = int(_mem.offset) * 10
+        self._get_duplex(mem, _mem)
+        mem.mode = self.MODES[_mem.mode]
+        if mem.mode == "FM":
+            if _mem.is_fm_narrow == 1:
+                mem.mode = "NFM"
+            mem.tuning_step = self.STEPSFM[_mem.fm_step]
+        elif mem.mode == "AM":
+            mem.tuning_step = self.STEPSAM[_mem.am_step]
+        elif mem.mode == "CW" or mem.mode == "CWR":
+            if _mem.is_cwdig_narrow == 1:
+                mem.mode = "N" + mem.mode
+            mem.tuning_step = self.STEPSSSB[_mem.ssb_step]
+        else:
+            try:
+                mem.tuning_step = self.STEPSSSB[_mem.ssb_step]
+            except IndexError:
+                pass
+        mem.skip = _mem.skip and "S" or ""
+        self._get_tmode(mem, _mem)
+
+        if _mem.tag_on_off == 1:
+            for i in _mem.name:
+                if i == "\xFF":
+                    break
+                mem.name += self.CHARSET[i]
+            mem.name = mem.name.rstrip()
+        else:
+            mem.name = ""
+
+        mem.extra = RadioSettingGroup("extra", "Extra")
+        ipo = RadioSetting("ipo", "IPO",
+                           RadioSettingValueBoolean(bool(_mem.ipo)))
+        ipo.set_doc("Bypass preamp")
+        mem.extra.append(ipo)
+        
+        att = RadioSetting("att", "ATT",
+                           RadioSettingValueBoolean(bool(_mem.att)))
+        att.set_doc("10dB front end attenuator")
+        mem.extra.append(att)
+
+        return mem
+
+    def _set_memory(self, mem, _mem):
+        if len(mem.name) > 0:     # not supported in chirp
+                                  # so I make label visible if have one
+            _mem.tag_on_off = 1
+        else:
+            _mem.tag_on_off = 0
+        _mem.tag_default = 0       # never use default label "CH-nnn"
+        self._set_duplex(mem, _mem)
+        if mem.mode[0] == "N": # is it narrow?
+            _mem.mode = self.MODES.index(mem.mode[1:])
+            # here I suppose it's safe to set both
+            _mem.is_fm_narrow = _mem.is_cwdig_narrow = 1       
+        else:
+            _mem.mode = self.MODES.index(mem.mode)
+            # here I suppose it's safe to set both
+            _mem.is_fm_narrow = _mem.is_cwdig_narrow = 0       
+        i = 0
+        for lo, hi in self.VALID_BANDS:
+            if mem.freq > lo and mem.freq < hi:
+                break 
+            i += 1
+        _mem.freq_range = i
+        # all this should be safe also when not in split but ... 
+        if mem.duplex == "split":
+            _mem.tx_mode = _mem.mode
+            i = 0
+            for lo, hi in self.VALID_BANDS:
+                if mem.offset >= lo and mem.offset < hi:
+                    break 
+                i += 1
+            _mem.tx_freq_range = i
+        _mem.skip = mem.skip == "S"
+        self._set_tmode(mem, _mem)
+        try:
+            _mem.ssb_step = self.STEPSSSB.index(mem.tuning_step)
+        except ValueError:
+            pass
+        try:
+            _mem.am_step = self.STEPSAM.index(mem.tuning_step)
+        except ValueError:
+            pass
+        try:
+            _mem.fm_step = self.STEPSFM.index(mem.tuning_step)
+        except ValueError:
+            pass
+        _mem.rit = 0	# not supported in chirp
+        _mem.freq = mem.freq / 10
+        _mem.offset = mem.offset / 10
+        for i in range(0, 8):
+            _mem.name[i] = self.CHARSET.index(mem.name.ljust(8)[i])
+        
+        for setting in mem.extra:
+            setattr(_mem, setting.get_name(), setting.value)
+
+    def validate_memory(self, mem):
+        msgs = yaesu_clone.YaesuCloneModeRadio.validate_memory(self, mem)
+
+        lo, hi = self.VALID_BANDS[2]    # this is fm broadcasting
+        if mem.freq >= lo and mem.freq <= hi:
+            if mem.mode != "FM":
+                msgs.append(chirp_common.ValidationError(
+                        "Only FM is supported in this band"))
+        # TODO check that step is valid in current mode
+        return msgs
+
+    @classmethod
+    def match_model(cls, filedata, filename):
+        return len(filedata) == cls._memsize
+
+    def get_settings(self):
+        _settings = self._memobj.settings
+        basic = RadioSettingGroup("basic", "Basic")
+        cw = RadioSettingGroup("cw", "CW")
+        packet = RadioSettingGroup("packet", "Digital & packet")
+        panel = RadioSettingGroup("panel", "Panel settings")
+        extended = RadioSettingGroup("extended", "Extended")
+        antenna = RadioSettingGroup("antenna", "Antenna selection")
+        panelcontr = RadioSettingGroup("panelcontr", "Panel controls")
+        top = RadioSettingGroup("top", "All Settings", basic, cw, packet,
+                                panelcontr, panel, extended, antenna)
+
+        rs = RadioSetting("ars_144", "144 ARS",
+                          RadioSettingValueBoolean(_settings.ars_144))
+        basic.append(rs)
+        rs = RadioSetting("ars_430", "430 ARS",
+                          RadioSettingValueBoolean(_settings.ars_144))
+        basic.append(rs)
+        rs = RadioSetting("pkt9600_mic", "Paket 9600 mic level",
+                          RadioSettingValueInteger(0, 100, _settings.am_mic))
+        packet.append(rs)
+        options = ["enable", "disable"]
+        rs = RadioSetting("disable_amfm_dial", "AM&FM Dial",
+                          RadioSettingValueList(options,
+                                        options[_settings.disable_amfm_dial]))
+        panel.append(rs)
+        rs = RadioSetting("am_mic", "AM mic level",
+                          RadioSettingValueInteger(0, 100, _settings.am_mic))
+        basic.append(rs)
+        options = ["OFF", "1h", "2h", "3h", "4h", "5h", "6h"]
+        rs = RadioSetting("apo_time", "APO time",
+                          RadioSettingValueList(options,
+                                        options[_settings.apo_time]))
+        basic.append(rs)
+        options = ["OFF", "Range", "All"]
+        rs = RadioSetting("arts_beep", "ARTS beep",
+                          RadioSettingValueList(options,
+                                        options[_settings.arts_beep]))
+        basic.append(rs)
+        options = ["OFF", "ON", "Auto"]
+        rs = RadioSetting("backlight", "Backlight",
+                          RadioSettingValueList(options,
+                                        options[_settings.backlight]))
+        panel.append(rs)
+        options = ["6h", "8h", "10h"]
+        rs = RadioSetting("batt_chg", "Battery charge",
+                          RadioSettingValueList(options,
+                                        options[_settings.batt_chg]))
+        basic.append(rs)
+        options = ["440Hz", "880Hz"]
+        rs = RadioSetting("beep_freq", "Beep frequency",
+                          RadioSettingValueList(options,
+                                        options[_settings.beep_freq]))
+        panel.append(rs)
+        rs = RadioSetting("beep_volume", "Beep volume",
+                          RadioSettingValueInteger(0, 100, _settings.beep_volume))
+        panel.append(rs)
+        options = ["4800", "9600", "38400"]
+        rs = RadioSetting("cat_rate", "CAT rate",
+                          RadioSettingValueList(options,
+                                        options[_settings.cat_rate]))
+        basic.append(rs)
+        options = ["Blue", "Amber", "Violet"]
+        rs = RadioSetting("color", "Color",
+                          RadioSettingValueList(options,
+                                        options[_settings.color]))
+        panel.append(rs)
+        rs = RadioSetting("contrast", "Contrast",
+                          RadioSettingValueInteger(1, 12,_settings.contrast-1))
+        panel.append(rs)
+        rs = RadioSetting("cw_delay", "CW delay (*10 ms)",
+                          RadioSettingValueInteger(1, 250, _settings.cw_delay))
+        cw.append(rs)
+        rs = RadioSetting("cw_id", "CW id",
+                          RadioSettingValueBoolean(_settings.cw_id))
+        cw.append(rs)
+        options = ["Normal", "Reverse"]
+        rs = RadioSetting("cw_paddle", "CW paddle",
+                          RadioSettingValueList(options,
+                                        options[_settings.cw_paddle]))
+        cw.append(rs)
+        options = ["%i Hz" % i for i in range(300,1001,50)]
+        rs = RadioSetting("cw_pitch", "CW pitch",
+                          RadioSettingValueList(options,
+                                        options[_settings.cw_pitch]))
+        cw.append(rs)
+        options = ["%i wpm" % i for i in range(4,61)]
+        rs = RadioSetting("cw_speed", "CW speed",
+                          RadioSettingValueList(options,
+                                        options[_settings.cw_speed]))
+        cw.append(rs)
+        options = ["1:%1.1f" % (i/10) for i in range(25,46,1)]
+        rs = RadioSetting("cw_weight", "CW weight",
+                          RadioSettingValueList(options,
+                                        options[_settings.cw_weight]))
+        cw.append(rs)
+        rs = RadioSetting("dig_disp", "Dig disp (*10 Hz)",
+                          RadioSettingValueInteger(-300, 300, _settings.dig_disp))
+        packet.append(rs)
+        rs = RadioSetting("dig_mic", "Dig mic",
+                          RadioSettingValueInteger(0, 100, _settings.dig_mic))
+        packet.append(rs)
+        options = ["RTTY", "PSK31-L", "PSK31-U", "USER-L", "USER-U"]
+        rs = RadioSetting("dig_mode", "Dig mode",
+                          RadioSettingValueList(options,
+                                        options[_settings.dig_mode]))
+        packet.append(rs)
+        rs = RadioSetting("dig_shift", "Dig shift (*10 Hz)",
+                          RadioSettingValueInteger(-300, 300, _settings.dig_shift))
+        packet.append(rs)
+        rs = RadioSetting("fm_mic", "FM mic",
+                          RadioSettingValueInteger(0, 100, _settings.fm_mic))
+        basic.append(rs)
+        options = ["Dial", "Freq", "Panel"]
+        rs = RadioSetting("lock_mode", "Lock mode",
+                          RadioSettingValueList(options,
+                                        options[_settings.lock_mode]))
+        panel.append(rs)
+        options = ["Fine", "Coarse"]
+        rs = RadioSetting("main_step", "Main step",
+                          RadioSettingValueList(options,
+                                        options[_settings.main_step]))
+        panel.append(rs)
+        rs = RadioSetting("mem_group", "Mem group",
+                          RadioSettingValueBoolean(_settings.mem_group))
+        basic.append(rs)
+        rs = RadioSetting("mic_key", "Mic key",
+                          RadioSettingValueBoolean(_settings.mic_key))
+        cw.append(rs)
+        rs = RadioSetting("mic_scan", "Mic scan",
+                          RadioSettingValueBoolean(_settings.mic_scan))
+        basic.append(rs)
+        options = ["Off", "SSB", "CW"]
+        rs = RadioSetting("op_filter", "Optional filter",
+                          RadioSettingValueList(options,
+                                        options[_settings.op_filter]))
+        basic.append(rs)
+        rs = RadioSetting("pkt_mic", "Packet mic",
+                          RadioSettingValueInteger(0, 100, _settings.pkt_mic))
+        packet.append(rs)
+        options = ["1200", "9600"]
+        rs = RadioSetting("pkt_rate", "Packet rate",
+                          RadioSettingValueList(options,
+                                        options[_settings.pkt_rate]))
+        packet.append(rs)
+        options = ["Off", "3 sec", "5 sec", "10 sec"]
+        rs = RadioSetting("resume_scan", "Resume scan",
+                          RadioSettingValueList(options,
+                                        options[_settings.resume_scan]))
+        basic.append(rs)
+        options = ["Cont", "Chk"]
+        rs = RadioSetting("scope", "Scope",
+                          RadioSettingValueList(options,
+                                        options[_settings.scope]))
+        basic.append(rs)
+        rs = RadioSetting("sidetone", "Sidetone",
+                          RadioSettingValueInteger(0, 100, _settings.sidetone))
+        cw.append(rs)
+        options = ["RF-Gain", "Squelch"]
+        rs = RadioSetting("sql_rf_gain", "Squelch/RF-Gain",
+                          RadioSettingValueList(options,
+                                        options[_settings.sql_rf_gain]))
+        panel.append(rs)
+        rs = RadioSetting("ssb_mic", "SSB Mic",
+                          RadioSettingValueInteger(0, 100, _settings.ssb_mic))
+        basic.append(rs)
+        options = ["%i" % i for i in range(0, 21)]
+        options[0] = "Off"
+        rs = RadioSetting("tot_time", "Time-out timer",
+                          RadioSettingValueList(options,
+                                        options[_settings.tot_time]))
+        basic.append(rs)
+        rs = RadioSetting("vox_delay", "VOX delay (*100 ms)",
+                          RadioSettingValueInteger(1, 25, _settings.vox_delay))
+        basic.append(rs)
+        rs = RadioSetting("vox_gain", "VOX Gain",
+                          RadioSettingValueInteger(0, 100, _settings.vox_gain))
+        basic.append(rs)
+        rs = RadioSetting("extended_menu", "Extended menu",
+                          RadioSettingValueBoolean(_settings.extended_menu))
+        extended.append(rs)
+        options = ["Tn-Rn", "Tn-Riv", "Tiv-Rn", "Tiv-Riv"]
+        rs = RadioSetting("dcs_inv", "DCS coding",
+                          RadioSettingValueList(options,
+                                        options[_settings.dcs_inv]))
+        extended.append(rs)
+        rs = RadioSetting("r_lsb_car", "LSB Rx carrier point (*10 Hz)",
+                          RadioSettingValueInteger(-30, 30, _settings.r_lsb_car))
+        extended.append(rs)
+        rs = RadioSetting("r_usb_car", "USB Rx carrier point (*10 Hz)",
+                          RadioSettingValueInteger(-30, 30, _settings.r_usb_car))
+        extended.append(rs)
+        rs = RadioSetting("t_lsb_car", "LSB Tx carrier point (*10 Hz)",
+                          RadioSettingValueInteger(-30, 30, _settings.t_lsb_car))
+        extended.append(rs)
+        rs = RadioSetting("t_usb_car", "USB Tx carrier point (*10 Hz)",
+                          RadioSettingValueInteger(-30, 30, _settings.t_usb_car))
+        extended.append(rs)
+
+        options = ["Hi", "L3", "L2", "L1"]
+        rs = RadioSetting("tx_power", "TX power",
+                          RadioSettingValueList(options,
+                                        options[_settings.tx_power]))
+        basic.append(rs)
+
+        options = ["Front", "Rear"]
+        rs = RadioSetting("hf_antenna", "HF",
+                          RadioSettingValueList(options,
+                                        options[_settings.hf_antenna]))
+        antenna.append(rs)
+        rs = RadioSetting("sixm_antenna", "6M",
+                          RadioSettingValueList(options,
+                                        options[_settings.sixm_antenna]))
+        antenna.append(rs)
+        rs = RadioSetting("bc_antenna", "Broadcasting",
+                          RadioSettingValueList(options,
+                                        options[_settings.bc_antenna]))
+        antenna.append(rs)
+        rs = RadioSetting("air_antenna", "Air band",
+                          RadioSettingValueList(options,
+                                        options[_settings.air_antenna]))
+        antenna.append(rs)
+        rs = RadioSetting("vhf_antenna", "VHF",
+                          RadioSettingValueList(options,
+                                        options[_settings.vhf_antenna]))
+        antenna.append(rs)
+        rs = RadioSetting("uhf_antenna", "UHF",
+                          RadioSettingValueList(options,
+                                        options[_settings.uhf_antenna]))
+        antenna.append(rs)
+
+        s = RadioSettingValueString(0, 7, 
+                            ''.join([self._CALLSIGN_CHARSET[x] for x in 
+                                        self._memobj.callsign]))
+        s.set_charset(self._CALLSIGN_CHARSET)
+        rs = RadioSetting("callsign", "Callsign", s)
+        cw.append(rs)
+
+        rs = RadioSetting("spl", "Split",
+                          RadioSettingValueBoolean(_settings.spl))
+        panelcontr.append(rs)
+        options = ["None", "Up", "Down"]
+        rs = RadioSetting("scn_mode", "Scan mode",
+                          RadioSettingValueList(options,
+                                        options[_settings.scn_mode]))
+        panelcontr.append(rs)
+        rs = RadioSetting("pri", "Priority",
+                          RadioSettingValueBoolean(_settings.pri))
+        panelcontr.append(rs)
+        rs = RadioSetting("dw", "Dual watch",
+                          RadioSettingValueBoolean(_settings.dw))
+        panelcontr.append(rs)
+        rs = RadioSetting("art", "Auto-range transponder",
+                          RadioSettingValueBoolean(_settings.art))
+        panelcontr.append(rs)
+        rs = RadioSetting("nb", "Noise blanker",
+                          RadioSettingValueBoolean(_settings.nb))
+        panelcontr.append(rs)
+        options = ["Auto", "Fast", "Slow", "Off"]
+        rs = RadioSetting("agc", "AGC",
+                          RadioSettingValueList(options,
+                                        options[_settings.agc]))
+        panelcontr.append(rs)
+        options = ["PWR", "ALC", "SWR", "MOD"]
+        rs = RadioSetting("pwr_meter_mode", "Power meter mode",
+                          RadioSettingValueList(options,
+                                        options[_settings.pwr_meter_mode]))
+        panelcontr.append(rs)
+        rs = RadioSetting("vox", "Vox",
+                          RadioSettingValueBoolean(_settings.vox))
+        panelcontr.append(rs)
+        rs = RadioSetting("bk", "Semi break-in",
+                          RadioSettingValueBoolean(_settings.bk))
+        cw.append(rs)
+        rs = RadioSetting("kyr", "Keyer",
+                          RadioSettingValueBoolean(_settings.kyr))
+        cw.append(rs)
+        options = ["enabled", "disabled"]
+        rs = RadioSetting("fst", "Fast",
+                          RadioSettingValueList(options,
+                                        options[_settings.fst]))
+        panelcontr.append(rs)
+        options = ["enabled", "disabled"]
+        rs = RadioSetting("lock", "Lock",
+                          RadioSettingValueList(options,
+                                        options[_settings.lock]))
+        panelcontr.append(rs)
+
+        return top
+
+    def set_settings(self, settings):
+        _settings = self._memobj.settings
+        for element in settings:
+            if not isinstance(element, RadioSetting):
+                self.set_settings(element)
+                continue
+            try:
+                if "." in element.get_name():
+                    bits = element.get_name().split(".")
+                    obj = self._memobj
+                    for bit in bits[:-1]:
+                        obj = getattr(obj, bit)
+                    setting = bits[-1]
+                else:
+                    obj = _settings
+                    setting = element.get_name()
+                if os.getenv("CHIRP_DEBUG"):
+	                print "Setting %s(%s) <= %s" % (setting, 
+                                    getattr(obj, setting), element.value)
+                if setting == "contrast":
+                    setattr(obj, setting, int(element.value)+1)
+                elif setting == "callsign":
+                    self._memobj.callsign = \
+                        [self._CALLSIGN_CHARSET_REV[x] for x in 
+                                                        str(element.value)]
+                else:
+                    setattr(obj, setting, element.value)
+            except Exception, e:
+                print element.get_name()
+                raise
+
+ at directory.register
+class FT817NDRadio(FT817Radio):
+    """Yaesu FT-817ND"""
+    MODEL = "FT-817ND"
+
+    _model = ""
+    _memsize = 6521
+    # block 9 (130 Bytes long) is to be repeted 40 times
+    _block_lengths = [ 2, 40, 208, 182, 208, 182, 198, 53, 130, 118, 130]
+
+ at directory.register
+class FT817NDUSRadio(FT817Radio):
+    """Yaesu FT-817ND (US version)"""
+    # seems that radios configured for 5MHz operations send one paket
+    # more than others so we have to distinguish sub models
+    MODEL = "FT-817ND (US)"
+
+    _model = ""
+    _memsize = 6651
+    # block 9 (130 Bytes long) is to be repeted 40 times
+    _block_lengths = [ 2, 40, 208, 182, 208, 182, 198, 53, 130, 118, 130, 130]
+
+    SPECIAL_60M = {
+        "M-601" : -42,
+        "M-602" : -41,
+        "M-603" : -40,
+        "M-604" : -39,
+        "M-605" : -38,
+        }
+    LAST_SPECIAL60M_INDEX = -42
+
+    SPECIAL_MEMORIES = dict(FT817Radio.SPECIAL_MEMORIES)
+    SPECIAL_MEMORIES.update(SPECIAL_60M)
+
+    SPECIAL_MEMORIES_REV = dict(zip(SPECIAL_MEMORIES.values(),
+                                    SPECIAL_MEMORIES.keys()))
+
+    def _get_special_60m(self, number):
+        mem = chirp_common.Memory()
+        mem.number = self.SPECIAL_60M[number]
+        mem.extd_number = number
+
+        _mem = self._memobj.sixtymeterchannels[-self.LAST_SPECIAL60M_INDEX +
+                                                mem.number]
+
+        mem = self._get_memory(mem, _mem)
+
+        mem.immutable = ["number", "skip", "rtone", "ctone",
+                         "extd_number", "name", "dtcs", "tmode", "cross_mode",
+                         "dtcs_polarity", "power", "duplex", "offset",
+                         "comment", "empty"]
+
+        return mem
+
+    def _set_special_60m(self, mem):
+        if mem.empty:
+            # can't delete 60M memories!
+            raise Exception("Sorry, 60M memory can't be deleted")
+
+        cur_mem = self._get_special_60m(self.SPECIAL_MEMORIES_REV[mem.number])
+
+        for key in cur_mem.immutable:
+            if cur_mem.__dict__[key] != mem.__dict__[key]:
+                raise errors.RadioError("Editing field `%s' " % key +
+                                        "is not supported on M-60x channels")
+
+        if mem.mode not in ["USB", "LSB", "CW", "CWR", "NCW", "NCWR", "DIG"]:
+            raise errors.RadioError("Mode {mode} is not valid "
+                                    "in 60m channels".format(mode=mem.mode))
+        _mem = self._memobj.sixtymeterchannels[-self.LAST_SPECIAL60M_INDEX +
+                                                mem.number]
+        self._set_memory(mem, _mem)
+
+    def get_memory(self, number):
+        if number in self.SPECIAL_60M.keys():
+            return self._get_special_60m(number)
+        elif number < 0 and \
+                self.SPECIAL_MEMORIES_REV[number] in self.SPECIAL_60M.keys():
+            # I can't stop delete operation from loosing extd_number but
+            # I know how to get it back
+            return self._get_special_60m(self.SPECIAL_MEMORIES_REV[number])
+        else:
+            return FT817Radio.get_memory(self, number)
+
+    def set_memory(self, memory):
+        if memory.number in self.SPECIAL_60M.values():
+            return self._set_special_60m(memory)
+        else:
+            return FT817Radio.set_memory(self, memory)
+
+    def get_settings(self):
+        top = FT817Radio.get_settings(self)
+        basic = getattr(top, "basic")
+        rs = RadioSetting("emergency", "Emergency",
+                          RadioSettingValueBoolean(self._memobj.settings.emergency))
+        basic.append(rs)
+        return top
+
diff --git a/chirp/ft857.py b/chirp/ft857.py
new file mode 100644
index 0000000..57db2d0
--- /dev/null
+++ b/chirp/ft857.py
@@ -0,0 +1,341 @@
+#
+# Copyright 2012 Filippi Marco <iz3gme.marco at gmail.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/>.
+
+"""FT857 - FT857/US management module"""
+
+from chirp import ft817, chirp_common, errors, directory
+
+ at directory.register
+class FT857Radio(ft817.FT817Radio):
+    """Yaesu FT-857/897"""
+    MODEL = "FT-857/897"
+    _model = ""
+
+    TMODES = {
+        0x04 : "Tone",
+        0x05 : "TSQL",
+        # 0x08 : "DTCS Enc", not supported in UI yet
+        0x0a : "DTCS",
+        0xff : "Cross",
+        0x00 : "",
+    }
+    TMODES_REV = dict(zip(TMODES.values(), TMODES.keys()))
+
+    CROSS_MODES = {
+        0x01 : "->Tone",
+        0x02 : "->DTCS",
+        # 0x04 : "Tone->", not supported in UI yet
+        0x05 : "Tone->Tone",
+        0x06 : "Tone->DTCS",
+        0x08 : "DTCS->",
+        0x09 : "DTCS->Tone",
+        0x0a : "DTCS->DTCS",
+    }
+    CROSS_MODES_REV = dict(zip(CROSS_MODES.values(), CROSS_MODES.keys()))
+
+    _memsize = 7341
+    # block 9 (140 Bytes long) is to be repeted 40 times 
+    # should be 42 times but this way I can use original 817 functions
+    _block_lengths = [ 2, 82, 252, 196, 252, 196, 212, 55, 140, 140, 140,
+                       38, 176]
+    # warning ranges has to be in this exact order
+    VALID_BANDS = [(100000, 33000000), (33000000, 56000000),
+                   (76000000, 108000000), (108000000, 137000000),
+                   (137000000, 164000000), (420000000, 470000000)]
+
+    MEM_FORMAT = """
+        struct mem_struct{
+        u8   tag_on_off:1,
+            tag_default:1,
+            unknown1:3,
+            mode:3;
+        u8   duplex:2,
+            is_duplex:1,
+            is_cwdig_narrow:1,
+            is_fm_narrow:1,
+            freq_range:3;
+        u8   skip:1,
+            unknokwn1_1:1,
+            ipo:1,
+            att:1,
+            unknown2:4;
+        u8   ssb_step:2,
+            am_step:3,
+            fm_step:3;
+        u8   unknown3:3,
+            is_split_tone:1,
+            tmode:4;
+        u8   unknown4:2,
+            tx_mode:3,
+            tx_freq_range:3;
+        u8   unknown5:1,
+            unknown_toneflag:1,
+            tone:6;
+        u8   unknown6:1,
+            unknown_rxtoneflag:1,
+            rxtone:6;
+        u8   unknown7:1,
+            dcs:7;
+        u8   unknown8:1,
+            rxdcs:7;
+        ul16 rit;
+        u32 freq;
+        u32 offset;
+        u8   name[8];
+        };
+        
+        #seekto 0x54;
+        struct mem_struct vfoa[16];
+        struct mem_struct vfob[16];
+        struct mem_struct home[4];
+        struct mem_struct qmb;
+        struct mem_struct mtqmb;
+        struct mem_struct mtune;
+        
+        #seekto 0x4a9;
+        u8 visible[25];
+        ul16 pmsvisible;
+        
+        #seekto 0x4c4;
+        u8 filled[25];
+        ul16 pmsfilled;
+        
+        #seekto 0x4df;
+        struct mem_struct memory[200];
+        struct mem_struct pms[10];
+        
+        #seekto 0x1CAD;
+        struct mem_struct sixtymeterchannels[5];
+    
+    """
+
+    # WARNING Index are hard wired in memory management code !!!
+    SPECIAL_MEMORIES = {
+        "VFOa-1.8M" : -37,
+        "VFOa-3.5M" : -36,
+        "VFOa-5M" : -35,
+        "VFOa-7M" : -34,
+        "VFOa-10M" : -33,
+        "VFOa-14M" : -32,
+        "VFOa-18M" : -31,
+        "VFOa-21M" : -30,
+        "VFOa-24M" : -29,
+        "VFOa-28M" : -28,
+        "VFOa-50M" : -27,
+        "VFOa-FM" : -26,
+        "VFOa-AIR" : -25,
+        "VFOa-144" : -24,
+        "VFOa-430" : -23,
+        "VFOa-HF" : -22,
+        "VFOb-1.8M" : -21,
+        "VFOb-3.5M" : -20,
+        "VFOb-5M" : -19,
+        "VFOb-7M" : -18,
+        "VFOb-10M" : -17,
+        "VFOb-14M" : -16,
+        "VFOb-18M" : -15,
+        "VFOb-21M" : -14,
+        "VFOb-24M" : -13,
+        "VFOb-28M" : -12,
+        "VFOb-50M" : -11,
+        "VFOb-FM" : -10,
+        "VFOb-AIR" : -9,
+        "VFOb-144M" : -8,
+        "VFOb-430M" : -7,
+        "VFOb-HF" : -6,
+        "HOME HF" : -5,
+        "HOME 50M" : -4,
+        "HOME 144M" : -3,
+        "HOME 430M" : -2,
+        "QMB" : -1,
+    }
+    FIRST_VFOB_INDEX = -6
+    LAST_VFOB_INDEX = -21
+    FIRST_VFOA_INDEX = -22
+    LAST_VFOA_INDEX = -37
+
+    SPECIAL_PMS = {
+        "PMS-1L" : -47,
+        "PMS-1U" : -46,
+        "PMS-2L" : -45,
+        "PMS-2U" : -44,
+        "PMS-3L" : -43,
+        "PMS-3U" : -42,
+        "PMS-4L" : -41,
+        "PMS-4U" : -40,
+        "PMS-5L" : -39,
+        "PMS-5U" : -38,
+    }
+    LAST_PMS_INDEX = -47
+
+    SPECIAL_MEMORIES.update(SPECIAL_PMS)
+
+    SPECIAL_MEMORIES_REV = dict(zip(SPECIAL_MEMORIES.values(),
+                                    SPECIAL_MEMORIES.keys()))
+
+    def get_features(self):
+        rf = ft817.FT817Radio.get_features(self)
+        rf.has_cross = True
+        rf.has_ctone = True
+        rf.has_rx_dtcs = True
+        rf.valid_tmodes = self.TMODES_REV.keys()
+        rf.valid_cross_modes = self.CROSS_MODES_REV.keys()
+        rf.has_settings = False # not implemented yet
+        return rf
+
+    def _get_duplex(self, mem, _mem):
+        # radio set is_duplex only for + and - but not for split
+        # at the same time it does not complain if we set it same way 817 does
+        # (so no set_duplex here)
+        mem.duplex = self.DUPLEX[_mem.duplex]
+
+    def _get_tmode(self, mem, _mem):
+        if not _mem.is_split_tone:
+            mem.tmode = self.TMODES[int(_mem.tmode)]
+        else:
+            mem.tmode = "Cross"
+            mem.cross_mode = self.CROSS_MODES[int(_mem.tmode)]
+
+        if mem.tmode == "Tone":
+             mem.rtone = mem.ctone = chirp_common.TONES[_mem.tone]
+        elif mem.tmode == "TSQL":
+             mem.rtone = mem.ctone = chirp_common.TONES[_mem.tone]
+        elif mem.tmode == "DTCS Enc": # UI does not support it yet but
+                                      # this code has alreay been tested
+             mem.dtcs = mem.rx_dtcs = chirp_common.DTCS_CODES[_mem.dcs]
+        elif mem.tmode == "DTCS":
+             mem.dtcs = mem.rx_dtcs = chirp_common.DTCS_CODES[_mem.dcs]
+        elif mem.tmode == "Cross":
+            mem.ctone = chirp_common.TONES[_mem.rxtone]
+            # don't want to fail for this
+            try:
+                mem.rtone = chirp_common.TONES[_mem.tone]
+            except IndexError:
+                mem.rtone = chirp_common.TONES[0]
+            mem.dtcs = chirp_common.DTCS_CODES[_mem.dcs]
+            mem.rx_dtcs = chirp_common.DTCS_CODES[_mem.rxdcs]
+
+    def _set_tmode(self, mem, _mem):
+        if mem.tmode != "Cross":
+            _mem.is_split_tone = 0
+            _mem.tmode = self.TMODES_REV[mem.tmode]
+        else:
+            _mem.tmode = self.CROSS_MODES_REV[mem.cross_mode]
+            _mem.is_split_tone = 1
+
+        if mem.tmode == "Tone":
+            _mem.tone = _mem.rxtone = chirp_common.TONES.index(mem.rtone)
+        elif mem.tmode == "TSQL":
+            _mem.tone = _mem.rxtone = chirp_common.TONES.index(mem.ctone)
+        elif mem.tmode == "DTCS Enc": # UI does not support it yet but
+                                      # this code has alreay been tested
+            _mem.dcs = _mem.rxdcs = chirp_common.DTCS_CODES.index(mem.dtcs)
+        elif mem.tmode == "DTCS":
+            _mem.dcs = _mem.rxdcs = chirp_common.DTCS_CODES.index(mem.rx_dtcs)
+        elif mem.tmode == "Cross":
+            _mem.tone = chirp_common.TONES.index(mem.rtone)
+            _mem.rxtone = chirp_common.TONES.index(mem.ctone)
+            _mem.dcs = chirp_common.DTCS_CODES.index(mem.dtcs)
+            _mem.rxdcs = chirp_common.DTCS_CODES.index(mem.rx_dtcs)
+        # have to put this bit to 0 otherwise we get strange display in tone
+        # frequency (menu 83). See bug #88 and #163
+        _mem.unknown_toneflag = 0
+        # dunno if there's the same problem here but to be safe ...
+        _mem.unknown_rxtoneflag = 0
+
+ at directory.register
+class FT857USRadio(FT857Radio):
+    """Yaesu FT857/897 (US version)"""
+    # seems that radios configured for 5MHz operations send one paket more
+    # than others so we have to distinguish sub models
+    MODEL = "FT-857/897 (US)"
+
+    _model = ""
+    _memsize = 7481
+    # block 9 (140 Bytes long) is to be repeted 40 times 
+    # should be 42 times but this way I can use original 817 functions
+    _block_lengths = [ 2, 82, 252, 196, 252, 196, 212, 55, 140, 140, 140, 38,
+                       176, 140]
+
+    SPECIAL_60M = {
+        "M-601" : -52,
+        "M-602" : -51,
+        "M-603" : -50,
+        "M-604" : -49,
+        "M-605" : -48,
+        }
+    LAST_SPECIAL60M_INDEX = -52
+    
+    SPECIAL_MEMORIES = dict(FT857Radio.SPECIAL_MEMORIES)
+    SPECIAL_MEMORIES.update(SPECIAL_60M)
+
+    SPECIAL_MEMORIES_REV = dict(zip(SPECIAL_MEMORIES.values(),
+                                    SPECIAL_MEMORIES.keys()))
+
+    # this is identical to the one in FT817ND_US_Radio but we inherit from 857
+    def _get_special_60m(self, number):
+        mem = chirp_common.Memory()
+        mem.number = self.SPECIAL_60M[number]
+        mem.extd_number = number
+
+        _mem = self._memobj.sixtymeterchannels[-self.LAST_SPECIAL60M_INDEX +
+                                                mem.number]
+
+        mem = self._get_memory(mem, _mem)
+
+        mem.immutable = ["number", "skip", "rtone", "ctone",
+                         "extd_number", "name", "dtcs", "tmode", "cross_mode",
+                         "dtcs_polarity", "power", "duplex", "offset",
+                         "comment", "empty"]
+
+        return mem
+
+    # this is identical to the one in FT817ND_US_Radio but we inherit from 857
+    def _set_special_60m(self, mem):
+        if mem.empty:
+            # can't delete 60M memories!
+            raise Exception("Sorry, 60M memory can't be deleted")
+
+        cur_mem = self._get_special_60m(self.SPECIAL_MEMORIES_REV[mem.number])
+
+        for key in cur_mem.immutable:
+            if cur_mem.__dict__[key] != mem.__dict__[key]:
+                raise errors.RadioError("Editing field `%s' " % key +
+                                        "is not supported on M-60x channels")
+
+        if mem.mode not in ["USB", "LSB", "CW", "CWR", "NCW", "NCWR", "DIG"]:
+            raise errors.RadioError("Mode {mode} is not valid "
+                                    "in 60m channels".format(mode=mem.mode))
+        _mem = self._memobj.sixtymeterchannels[-self.LAST_SPECIAL60M_INDEX +
+                                                mem.number]
+        self._set_memory(mem, _mem)
+
+    def get_memory(self, number):
+        if number in self.SPECIAL_60M.keys():
+            return self._get_special_60m(number)
+        elif number < 0 and \
+                self.SPECIAL_MEMORIES_REV[number] in self.SPECIAL_60M.keys():
+            # I can't stop delete operation from loosing extd_number but
+            # I know how to get it back
+            return self._get_special_60m(self.SPECIAL_MEMORIES_REV[number])
+        else:
+            return FT857Radio.get_memory(self, number)
+
+    def set_memory(self, memory):
+        if memory.number in self.SPECIAL_60M.values():
+            return self._set_special_60m(memory)
+        else:
+            return FT857Radio.set_memory(self, memory)
diff --git a/chirp/generic_csv.py b/chirp/generic_csv.py
new file mode 100644
index 0000000..4ca06d9
--- /dev/null
+++ b/chirp/generic_csv.py
@@ -0,0 +1,243 @@
+# 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 os
+import csv
+
+from chirp import chirp_common, errors, directory
+
+class OmittedHeaderError(Exception):
+    """Internal exception to signal that a column has been omitted"""
+    pass
+
+def get_datum_by_header(headers, data, header):
+    """Return the column corresponding to @headers[@header] from @data"""
+    if header not in headers:
+        raise OmittedHeaderError("Header %s not provided" % header)
+
+    try:
+        return data[headers.index(header)]
+    except IndexError:
+        raise OmittedHeaderError("Header %s not provided on this line" % \
+                                     header)
+
+def write_memory(writer, mem):
+    """Write @mem using @writer if not empty"""
+    if mem.empty:
+        return
+    writer.writerow(mem.to_csv())
+
+ at directory.register
+class CSVRadio(chirp_common.FileBackedRadio, chirp_common.IcomDstarSupport):
+    """A driver for Generic CSV files"""
+    VENDOR = "Generic"
+    MODEL = "CSV"
+    FILE_EXTENSION = "csv"
+
+    ATTR_MAP = {
+        "Location"     : (int,   "number"),
+        "Name"         : (str,   "name"),
+        "Frequency"    : (chirp_common.parse_freq, "freq"),
+        "Duplex"       : (str,   "duplex"),
+        "Offset"       : (chirp_common.parse_freq, "offset"),
+        "Tone"         : (str,   "tmode"),
+        "rToneFreq"    : (float, "rtone"),
+        "cToneFreq"    : (float, "ctone"),
+        "DtcsCode"     : (int,   "dtcs"),
+        "DtcsPolarity" : (str,   "dtcs_polarity"),
+        "Mode"         : (str,   "mode"),
+        "TStep"        : (float, "tuning_step"),
+        "Skip"         : (str,   "skip"),
+        "URCALL"       : (str,   "dv_urcall"),
+        "RPT1CALL"     : (str,   "dv_rpt1call"),
+        "RPT2CALL"     : (str,   "dv_rpt2call"),
+        "Comment"      : (str,   "comment"),
+        }
+
+    def _blank(self):
+        self.errors = []
+        self.memories = []
+        for i in range(0, 1000):
+            mem = chirp_common.Memory()
+            mem.number = i
+            mem.empty = True
+            self.memories.append(mem)
+
+    def __init__(self, pipe):
+        chirp_common.FileBackedRadio.__init__(self, None)
+        self.memories = []
+
+        self._filename = pipe
+        if self._filename and os.path.exists(self._filename):
+            self.load()
+        else:
+            self._blank()
+
+    def get_features(self):
+        rf = chirp_common.RadioFeatures()
+        rf.has_bank = False
+        rf.requires_call_lists = False
+        rf.has_implicit_calls = False
+        rf.memory_bounds = (0, len(self.memories))
+        rf.has_infinite_number = True
+        rf.has_nostep_tuning = True
+        rf.has_comment = True
+
+        rf.valid_modes = list(chirp_common.MODES)
+        rf.valid_tmodes = list(chirp_common.TONE_MODES)
+        rf.valid_duplexes = ["", "-", "+", "split", "off"]
+        rf.valid_tuning_steps = list(chirp_common.TUNING_STEPS)
+        rf.valid_bands = [(1, 10000000000)]
+        rf.valid_skips = ["", "S"]
+        rf.valid_characters = chirp_common.CHARSET_ASCII
+        rf.valid_name_length = 999
+
+        return rf
+
+    def _parse_csv_data_line(self, headers, line):
+        mem = chirp_common.Memory()
+        try:
+            if get_datum_by_header(headers, line, "Mode") == "DV":
+                mem = chirp_common.DVMemory()
+        except OmittedHeaderError:
+            pass
+
+        for header, (typ, attr) in self.ATTR_MAP.items():
+            try:
+                val = get_datum_by_header(headers, line, header)
+                if not val and typ == int:
+                    val = None
+                else:
+                    val = typ(val)
+                if hasattr(mem, attr):
+                    setattr(mem, attr, val)
+            except OmittedHeaderError, e:
+                pass
+            except Exception, e:
+                raise Exception("[%s] %s" % (attr, e))
+
+        return mem
+
+    def load(self, filename=None):
+        if filename is None and self._filename is None:
+            raise errors.RadioError("Need a location to load from")
+
+        if filename:
+            self._filename = filename
+
+        self._blank()
+
+        f = file(self._filename, "rU")
+        header = f.readline().strip()
+
+        f.seek(0, 0)
+        reader = csv.reader(f, delimiter=chirp_common.SEPCHAR, quotechar='"')
+
+        good = 0
+        lineno = 0
+        for line in reader:
+            lineno += 1
+            if lineno == 1:
+                header = line
+                continue
+
+            if len(header) > len(line):
+                print "Line %i has %i columns, expected %i" % (lineno,
+                                                               len(line),
+                                                               len(header))
+                self.errors.append("Column number mismatch on line %i" % lineno)
+                continue
+
+            try:
+                mem = self._parse_csv_data_line(header, line)
+                if mem.number is None:
+                    raise Exception("Invalid Location field" % lineno)
+            except Exception, e:
+                print "Line %i: %s" % (lineno, e)
+                self.errors.append("Line %i: %s" % (lineno, e))
+                continue
+
+            self._grow(mem.number)
+            self.memories[mem.number] = mem
+            good += 1
+
+        if not good:
+            print self.errors
+            raise errors.InvalidDataError("No channels found")
+
+    def save(self, filename=None):
+        if filename is None and self._filename is None:
+            raise errors.RadioError("Need a location to save to")
+
+        if filename:
+            self._filename = filename
+
+        f = file(self._filename, "wb")
+        writer = csv.writer(f, delimiter=chirp_common.SEPCHAR)
+        writer.writerow(chirp_common.Memory.CSV_FORMAT)
+
+        for mem in self.memories:
+            write_memory(writer, mem)
+
+        f.close()
+
+    # MMAP compatibility
+    def save_mmap(self, filename):
+        return self.save(filename)
+
+    def load_mmap(self, filename):
+        return self.load(filename)
+
+    def get_memories(self, lo=0, hi=999):
+        return [x for x in self.memories if x.number >= lo and x.number <= hi]
+
+    def get_memory(self, number):
+        try:
+            return self.memories[number]
+        except:
+            raise errors.InvalidMemoryLocation("No such memory %s" % number)
+
+    def _grow(self, target):
+        delta = target - len(self.memories)
+        if delta < 0:
+            return
+
+        delta += 1
+        
+        for i in range(len(self.memories), len(self.memories) + delta + 1):
+            mem = chirp_common.Memory()
+            mem.empty = True
+            mem.number = i
+            self.memories.append(mem)
+
+    def set_memory(self, newmem):
+        self._grow(newmem.number)
+        self.memories[newmem.number] = newmem
+
+    def erase_memory(self, number):
+        mem = chirp_common.Memory()
+        mem.number = number
+        mem.empty = True
+        self.memories[number] = mem
+
+    def get_raw_memory(self, number):
+        return ",".join(chirp_common.Memory.CSV_FORMAT) + \
+            os.linesep + \
+            ",".join(self.memories[number].to_csv())
+
+    @classmethod
+    def match_model(cls, _filedata, filename):
+        """Match files ending in .CSV"""
+        return filename.lower().endswith("." + cls.FILE_EXTENSION)
diff --git a/chirp/generic_tpe.py b/chirp/generic_tpe.py
new file mode 100644
index 0000000..8fa949c
--- /dev/null
+++ b/chirp/generic_tpe.py
@@ -0,0 +1,46 @@
+# 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/>.
+
+import UserDict
+from chirp import chirp_common, directory, generic_csv
+
+class TpeMap(UserDict.UserDict):
+    """Pretend we're a dict"""
+    def items(self):
+        return [
+            ("Sequence Number" , (int, "number")),
+            ("Location"        , (str, "comment")),
+            ("Call Sign"       , (str, "name")),
+            ("Output Frequency", (chirp_common.parse_freq, "freq")),
+            ("Input Frequency" , (str, "duplex")),
+            ("CTCSS Tones"     , (lambda v: "Tone" 
+                                  if float(v) in chirp_common.TONES
+                                  else "", "tmode")),
+            ("CTCSS Tones"     , (lambda v: float(v)
+                                  if float(v) in chirp_common.TONES
+                                  else 88.5, "rtone")),
+            ("CTCSS Tones"     , (lambda v: float(v)
+                                  if float(v) in chirp_common.TONES
+                                  else 88.5, "ctone")),
+        ]
+
+ at directory.register
+class TpeRadio(generic_csv.CSVRadio):
+    """Generic ARRL Travel Plus"""
+    VENDOR = "ARRL"
+    MODEL = "Travel Plus"
+    FILE_EXTENSION = "tpe"
+
+    ATTR_MAP = TpeMap()
diff --git a/chirp/generic_xml.py b/chirp/generic_xml.py
new file mode 100644
index 0000000..1153c63
--- /dev/null
+++ b/chirp/generic_xml.py
@@ -0,0 +1,143 @@
+# 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 os
+import libxml2
+
+from chirp import chirp_common, errors, xml_ll, platform, directory
+
+def validate_doc(doc):
+    """Validate the document"""
+    basepath = platform.get_platform().executable_path()
+    path = os.path.abspath(os.path.join(basepath, "chirp.xsd"))
+    if not os.path.exists(path):
+        path = "/usr/share/chirp/chirp.xsd"         
+
+    try:
+        ctx = libxml2.schemaNewParserCtxt(path)
+        schema = ctx.schemaParse()
+    except libxml2.parserError, e:
+        print "Unable to load schema: %s" % e
+        print "Path: %s" % path
+        raise errors.RadioError("Unable to load schema")
+
+    del ctx
+
+    errs = []
+    warnings = []
+
+    def _err(msg, *_args):
+        errs.append("ERROR: %s" % msg)
+
+    def _wrn(msg, *_args):
+        print "WARNING: %s" % msg
+        warnings.append("WARNING: %s" % msg)
+
+    validctx = schema.schemaNewValidCtxt()
+    validctx.setValidityErrorHandler(_err, _wrn)
+    err = validctx.schemaValidateDoc(doc)
+    print os.linesep.join(warnings)
+    if err:
+        print "---DOC---\n%s\n------" % doc.serialize(format=1)
+        print os.linesep.join(errs)
+        raise errors.RadioError("Schema error")
+
+def default_banks():
+    """Return an empty set of banks"""
+    banks = []
+
+    for i in range(0, 26):
+        banks.append("Bank-%s" % (chr(ord("A") + i)))
+
+    return banks
+
+ at directory.register
+class XMLRadio(chirp_common.FileBackedRadio, chirp_common.IcomDstarSupport):
+    """Generic XML driver"""
+    VENDOR = "Generic"
+    MODEL = "XML"
+    FILE_EXTENSION = "chirp"
+
+    def __init__(self, pipe):
+        chirp_common.FileBackedRadio.__init__(self, None)
+        self._filename = pipe
+        if self._filename and os.path.exists(self._filename):
+            self.doc = libxml2.parseFile(self._filename)
+            validate_doc(self.doc)
+        else:
+            self.doc = libxml2.newDoc("1.0")
+            radio = self.doc.newChild(None, "radio", None)
+            radio.newChild(None, "memories", None)
+            radio.newChild(None, "banks", None)
+            radio.newProp("version", "0.1.1")
+
+    def get_features(self):
+        rf = chirp_common.RadioFeatures()
+        rf.has_bank = False
+        #rf.has_bank_index = True
+        rf.requires_call_lists = False
+        rf.has_implicit_calls = False
+        rf.memory_bounds = (0, 1000)
+        rf.valid_characters = chirp_common.CHARSET_ASCII
+        rf.valid_name_length = 999
+        rf.valid_tmodes = ["", "Tone", "TSQL", "DTCS"]
+        return rf
+        
+    def load(self, filename=None):
+        if not self._filename and not filename:
+            raise errors.RadioError("Need a location to load from")
+
+        if filename:
+            self._filename = filename
+
+        self.doc = libxml2.parseFile(self._filename)
+        validate_doc(self.doc)
+
+    def save(self, filename=None):
+        if not self._filename and not filename:
+            raise errors.RadioError("Need a location to save to")
+
+        if filename:
+            self._filename = filename
+
+        f = file(self._filename, "w")
+        f.write(self.doc.serialize(format=1))
+        f.close()
+
+    def get_memories(self, lo=0, hi=999):
+        mems = []
+        for i in range(lo, hi):
+            try:
+                mems.append(xml_ll.get_memory(self.doc, i))
+            except errors.InvalidMemoryLocation:
+                pass
+
+        return mems
+    
+    def get_memory(self, number):
+        mem = xml_ll.get_memory(self.doc, number)
+
+        return mem
+
+    def set_memory(self, mem):
+        xml_ll.set_memory(self.doc, mem)
+
+    def erase_memory(self, number):
+        xml_ll.del_memory(self.doc, number)
+
+    @classmethod
+    def match_model(cls, _filedata, filename):
+        """Match this driver if the extension matches"""
+        return filename.lower().endswith("." + cls.FILE_EXTENSION)
diff --git a/chirp/ic208.py b/chirp/ic208.py
new file mode 100644
index 0000000..627607c
--- /dev/null
+++ b/chirp/ic208.py
@@ -0,0 +1,261 @@
+# Copyright 2013 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/>.
+
+from chirp import chirp_common, icf, errors, directory
+from chirp import bitwise
+
+MEM_FORMAT = """
+struct memory {
+  u24 freq;
+  u16 offset;  
+  u8  power:2,
+      rtone:6;
+  u8  duplex:2,
+      ctone:6;
+  u8  unknown1:1,
+      dtcs:7;
+  u8  tuning_step:4,
+      unknown2:4;
+  u8  unknown3;
+  u8  alt_mult:1,
+      unknown4:1,
+      is_fm:1,
+      is_wide:1,
+      unknown5:2,
+      tmode:2;
+  u16 dtcs_polarity:2,
+      usealpha:1,
+      empty:1,
+      name1:6,
+      name2:6;
+  u24 name3:6,
+      name4:6,
+      name5:6,
+      name6:6;
+};
+
+struct memory memory[510];
+
+struct {
+  u8 unknown1:1,
+     empty:1,
+     pskip:1,
+     skip:1,
+     bank:4;
+} flags[512];
+
+struct memory call[2];
+
+"""
+
+MODES = ["AM", "FM", "NFM", "NAM"]
+TMODES = ["", "Tone", "TSQL", "DTCS"]
+DUPLEX = ["", "", "-", "+"]
+DTCS_POL = ["NN", "NR", "RN", "RR"]
+STEPS = [5.0, 10.0, 12.5, 15, 20.0, 25.0, 30.0, 50.0, 100.0, 200.0]
+POWER = [chirp_common.PowerLevel("High", watts=50),
+         chirp_common.PowerLevel("Low", watts=5),
+         chirp_common.PowerLevel("Mid", watts=15),
+         ]
+
+IC208_SPECIAL = []
+for i in range(1, 6):
+    IC208_SPECIAL.append("%iA" % i)
+    IC208_SPECIAL.append("%iB" % i)
+
+ALPHA_CHARSET = " ABCDEFGHIJKLMNOPQRSTUVWXYZ"
+NUMERIC_CHARSET = "0123456789+-=*/()|"
+
+def get_name(_mem):
+    """Decode the name from @_mem"""
+    def _get_char(val):
+        if val == 0:
+            return " "
+        elif val & 0x20:
+            return ALPHA_CHARSET[val & 0x1F]
+        else:
+            return NUMERIC_CHARSET[val & 0x0F]
+
+    name_bytes = [_mem.name1, _mem.name2, _mem.name3,
+                  _mem.name4, _mem.name5, _mem.name6]
+    name = ""
+    for val in name_bytes:
+        name += _get_char(val)
+
+    return name.rstrip()
+
+def set_name(_mem, name):
+    """Encode @name in @_mem"""
+    def _get_index(char):
+        if char == " ":
+            return 0
+        elif char.isalpha():
+            return ALPHA_CHARSET.index(char) | 0x20
+        else:
+            return NUMERIC_CHARSET.index(char) | 0x10
+
+    name = name.ljust(6)[:6]
+
+    _mem.usealpha = bool(name.strip())
+
+    # The element override calling convention makes this harder to automate.
+    # It's just six, so do it manually
+    _mem.name1 = _get_index(name[0])
+    _mem.name2 = _get_index(name[1])
+    _mem.name3 = _get_index(name[2])
+    _mem.name4 = _get_index(name[3])
+    _mem.name5 = _get_index(name[4])
+    _mem.name6 = _get_index(name[5])
+
+ at directory.register
+class IC208Radio(icf.IcomCloneModeRadio):
+    """Icom IC800"""
+    VENDOR = "Icom"
+    MODEL = "IC-208H"
+
+    _model = "\x26\x32\x00\x01"
+    _memsize = 0x2600
+    _endframe = "Icom Inc\x2e30"
+    _can_hispeed = True
+
+    _memories = []
+
+    _ranges = [(0x0000, 0x2600, 32)]
+
+    def get_features(self):
+        rf = chirp_common.RadioFeatures()
+        rf.memory_bounds = (1, 500)
+        rf.has_bank = True
+        rf.valid_tuning_steps = list(STEPS)
+        rf.valid_tmodes = list(TMODES)
+        rf.valid_modes = list(MODES)
+        rf.valid_duplexes = list(DUPLEX)
+        rf.valid_power_levels = list(POWER)
+        rf.valid_skips = ["", "S", "P"]
+        rf.valid_bands = [(118000000, 173995000),
+                          (230000000, 549995000),
+                          (810000000, 999990000)]
+        rf.valid_special_chans = ["C1", "C2"] + sorted(IC208_SPECIAL)
+        return rf
+
+    def get_raw_memory(self, number):
+        _mem, _flg, index = self._get_memory(number)
+        return repr(_mem) + repr(_flg)
+
+    def process_mmap(self):
+        self._memobj = bitwise.parse(MEM_FORMAT, self._mmap)
+
+
+    def _get_bank(self, loc):
+        _flg = self._memobj.flags[loc-1]
+        if _flg.bank >= 0x0A:
+            return None
+        else:
+            return _flg.bank
+
+    def _set_bank(self, loc, bank):
+        _flg = self._memobj.flags[loc-1]
+        if bank is None:
+            _flg.bank = 0x0A
+        else:
+            _flg.bank = bank
+
+    def _get_memory(self, number):
+        if isinstance(number, str):
+            if "A" in number or "B" in number:
+                index = 501 + IC208_SPECIAL.index(number)
+                _mem = self._memobj.memory[index - 1]
+                _flg = self._memobj.flags[index - 1]
+            else:
+                index = int(number[1]) - 1
+                _mem = self._memobj.call[index]
+                _flg = self._memobj.flags[510 + index]
+                index = index + -10
+        elif number <= 0:
+            index = 10 - abs(number)
+            _mem = self._memobj.call[index]
+            _flg = self._memobj.flags[index + 510]
+        else:
+            index = number
+            _mem = self._memobj.memory[number - 1]
+            _flg = self._memobj.flags[number - 1]
+
+        return _mem, _flg, index
+
+
+    def get_memory(self, number):
+        _mem, _flg, index = self._get_memory(number)
+
+        mem = chirp_common.Memory()
+        mem.number = index
+        if isinstance(number, str):
+            mem.extd_number = number
+        else:
+            mem.skip = _flg.pskip and "P" or _flg.skip and "S" or ""
+
+        if _flg.empty:
+            mem.empty = True
+            return mem
+
+        mult = _mem.alt_mult and 6250 or 5000
+        mem.freq = int(_mem.freq) * mult
+        mem.offset = int(_mem.offset) * 5000
+        mem.rtone = chirp_common.TONES[_mem.rtone]
+        mem.ctone = chirp_common.TONES[_mem.ctone]
+        mem.dtcs = chirp_common.DTCS_CODES[_mem.dtcs]
+        mem.dtcs_polarity = DTCS_POL[_mem.dtcs_polarity]
+        mem.duplex = DUPLEX[_mem.duplex]
+        mem.tmode = TMODES[_mem.tmode]
+        mem.mode = ((not _mem.is_wide and "N" or "") +
+                    (_mem.is_fm and "FM" or "AM"))
+        mem.tuning_step = STEPS[_mem.tuning_step]
+        mem.name = get_name(_mem)
+        mem.power = POWER[_mem.power]
+
+        return mem
+
+    def set_memory(self, mem):
+        _mem, _flg, index = self._get_memory(mem.number)
+
+        if mem.empty:
+            _flg.empty = True
+            self._set_bank(mem.number, None)
+            return
+
+        if _flg.empty:
+            _mem.set_raw("\x00" * 16)
+        _flg.empty = False
+
+        _mem.alt_mult = chirp_common.is_fractional_step(mem.freq)
+        _mem.freq = mem.freq / (_mem.alt_mult and 6250 or 5000)
+        _mem.offset = mem.offset / 5000
+        _mem.rtone = chirp_common.TONES.index(mem.rtone)
+        _mem.ctone = chirp_common.TONES.index(mem.ctone)
+        _mem.dtcs = chirp_common.DTCS_CODES.index(mem.dtcs)
+        _mem.dtcs_polarity = DTCS_POL.index(mem.dtcs_polarity)
+        _mem.duplex = DUPLEX.index(mem.duplex)
+        _mem.tmode = TMODES.index(mem.tmode)
+        _mem.is_fm = "FM" in mem.mode
+        _mem.is_wide = mem.mode[0] != "N"
+        _mem.tuning_step = STEPS.index(mem.tuning_step)
+        set_name(_mem, mem.name)
+        try:
+            _mem.power = POWER.index(mem.power)
+        except Exception:
+            pass
+        if not isinstance(mem.number, str):
+            _flg.skip = mem.skip == "S"
+            _flg.pskip = mem.skip == "P"
+
diff --git a/chirp/ic2100.py b/chirp/ic2100.py
new file mode 100644
index 0000000..b10b6d3
--- /dev/null
+++ b/chirp/ic2100.py
@@ -0,0 +1,252 @@
+# Copyright 2010 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/>.
+
+from chirp import chirp_common, icf, util, directory
+from chirp import bitwise, memmap
+
+MEM_FORMAT = """
+struct {
+  bbcd  freq[2];
+  u8    freq_10khz:4,
+        freq_1khz:3,
+        zero:1;
+  u8    unknown1;
+  bbcd  offset[2];
+  u8    is_12_5:1,
+        unknownbits:3,
+        duplex:2,
+        tmode:2;
+  u8    ctone;
+  u8    rtone;
+  char  name[6];
+  u8    unknown3;
+} memory[100];
+
+#seekto 0x0640;
+struct {
+  bbcd  freq[2];
+  u8    freq_10khz:4,
+        freq_1khz:3,
+        zero:1;
+  u8    unknown1;
+  bbcd  offset[2];
+  u8    is_12_5:1,
+        unknownbits:3,
+        duplex:2,
+        tmode:2;
+  u8    ctone;
+  u8    rtone;
+} special[7];
+
+#seekto 0x0680;
+struct {
+  bbcd  freq[2];
+  u8    freq_10khz:4,
+        freq_1khz:3,
+        zero:1;
+  u8    unknown1;
+  bbcd  offset[2];
+  u8    is_12_5:1,
+        unknownbits:3,
+        duplex:2,
+        tmode:2;
+  u8    ctone;
+  u8    rtone;
+} call[2];
+
+#seekto 0x06F0;
+struct {
+  u8 flagbits;
+} skipflags[14];
+
+#seekto 0x0700;
+struct {
+  u8 flagbits;
+} usedflags[14];
+
+"""
+
+TMODES = ["", "Tone", "", "TSQL"]
+DUPLEX = ["", "", "+", "-"]
+STEPS =  [5.0, 10.0, 12.5, 15.0, 20.0, 25.0, 30.0, 50.0]
+
+def _get_special():
+    special = { "C": 506 }
+    for i in range(0, 3):
+        ida = "%iA" % (i + 1)
+        idb = "%iB" % (i + 1)
+        num = 500 + (i * 2)
+        special[ida] = num
+        special[idb] = num + 1
+
+    return special
+
+def _get_freq(mem):
+    freq = (int(mem.freq) * 100000) + \
+        (mem.freq_10khz * 10000) + \
+        (mem.freq_1khz * 1000)
+
+    if mem.is_12_5:
+        if chirp_common.is_12_5(freq):
+            pass
+        elif mem.freq_1khz == 2:
+            freq += 500
+        elif mem.freq_1khz == 5:
+            freq += 2500
+        elif mem.freq_1khz == 7:
+            freq += 500
+        else:
+            raise Exception("Unable to resolve 12.5kHz: %i" % freq)
+
+    return freq
+
+def _set_freq(mem, freq):
+    mem.freq = freq / 100000
+    mem.freq_10khz = (freq / 10000) % 10
+    khz = (freq / 1000) % 10
+    mem.freq_1khz = khz
+    mem.is_12_5 = chirp_common.is_12_5(freq)
+
+def _get_offset(mem):
+    raw = memmap.MemoryMap(mem.get_raw())
+    if ord(raw[5]) & 0x0A:
+        raw[5] = ord(raw[5]) & 0xF0
+        mem.set_raw(raw.get_packed())
+        offset = int(mem.offset) * 1000 + 5000
+        raw[5] = ord(raw[5]) | 0x0A
+        mem.set_raw(raw.get_packed())
+        return offset
+    else:
+        return int(mem.offset) * 1000
+
+def _set_offset(mem, offset):
+    if (offset % 10) == 5000:
+        extra = 0x0A
+        offset -= 5000
+    else:
+        extra = 0x00
+
+    mem.offset = offset / 1000
+    raw = memmap.MemoryMap(mem.get_raw())
+    raw[5] = ord(raw[5]) | extra
+    mem.set_raw(raw.get_packed())
+
+def _wipe_memory(mem, char):
+    mem.set_raw(char * (mem.size() / 8))
+
+ at directory.register
+class IC2100Radio(icf.IcomCloneModeRadio):
+    """Icom IC-2100"""
+    VENDOR = "Icom"
+    MODEL = "IC-2100H"
+
+    _model = "\x20\x88\x00\x01"
+    _memsize = 2016
+    _endframe = "Icom Inc\x2e"
+
+    _ranges = [(0x0000, 0x07E0, 32)]
+
+    def get_features(self):
+        rf = chirp_common.RadioFeatures()
+        rf.memory_bounds = (1, 100)
+        rf.has_dtcs = False
+        rf.has_dtcs_polarity = False
+        rf.has_bank = False
+        rf.has_tuning_step = False
+        rf.has_mode = False
+        rf.valid_modes = ["FM"]
+        rf.valid_tmodes = list(TMODES)
+        rf.valid_duplexes = list(DUPLEX)
+        rf.valid_tuning_steps = list(STEPS)
+        rf.valid_bands = [(118000000, 174000000)]
+        rf.valid_skips = ["", "S"]
+        rf.valid_special_chans = sorted(_get_special().keys())
+        return rf
+
+    def process_mmap(self):
+        self._memobj = bitwise.parse(MEM_FORMAT, self._mmap)
+
+    def get_memory(self, number):
+        mem = chirp_common.Memory()
+
+        if isinstance(number, str):
+            if number == "C":
+                number = _get_special()[number]
+                _mem = self._memobj.call[0]
+            else:
+                number = _get_special()[number]
+                _mem = self._memobj.special[number - 500]
+            empty = False
+        else:
+            number -= 1
+            _mem = self._memobj.memory[number]
+            _emt = self._memobj.usedflags[number / 8].flagbits
+            empty = (1 << (number % 8)) & int(_emt)
+            if not empty:
+                mem.name = str(_mem.name).rstrip()
+            _skp = self._memobj.skipflags[number / 8].flagbits
+            isskip = (1 << (number % 8)) & int(_skp)
+
+        mem.number = number + 1
+
+        if number <= 100:
+            mem.skip = isskip and "S" or ""
+        else:
+            mem.extd_number = util.get_dict_rev(_get_special(), number)
+            mem.immutable = ["number", "skip", "extd_number"]
+
+        if empty:
+            mem.empty = True
+            return mem
+
+        mem.freq = _get_freq(_mem)
+        mem.offset = _get_offset(_mem)
+        mem.rtone = chirp_common.TONES[_mem.rtone]
+        mem.ctone = chirp_common.TONES[_mem.ctone]
+        mem.tmode = TMODES[_mem.tmode]
+        mem.duplex = DUPLEX[_mem.duplex]
+        
+        return mem
+
+    def set_memory(self, mem):
+        if mem.number == "C":
+            _mem = self._memobj.call[0]
+        elif isinstance(mem.number, str):
+            _mem = self._memobj.special[_get_special[mem.number] - 500]
+        else:
+            number = mem.number - 1
+            _mem = self._memobj.memory[number]
+            _emt = self._memobj.usedflags[number / 8].flagbits
+            mask = 1 << (number % 8)
+            if mem.empty:
+                _emt |= mask
+            else:
+                _emt &= ~mask
+            _skp = self._memobj.skipflags[number / 8].flagbits
+            if mem.skip == "S":
+                _skp |= mask
+            else:
+                _skp &= ~mask
+            _mem.name = mem.name.ljust(6)
+
+        _set_freq(_mem, mem.freq)
+        _set_offset(_mem, mem.offset)
+        _mem.rtone = chirp_common.TONES.index(mem.rtone)
+        _mem.ctone = chirp_common.TONES.index(mem.ctone)
+        _mem.tmode = TMODES.index(mem.tmode)
+        _mem.duplex = DUPLEX.index(mem.duplex)
+
+    def get_raw_memory(self, number):
+        return repr(self._memobj.memory[number])
diff --git a/chirp/ic2200.py b/chirp/ic2200.py
new file mode 100644
index 0000000..d6b1fe2
--- /dev/null
+++ b/chirp/ic2200.py
@@ -0,0 +1,292 @@
+# 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/>.
+
+from chirp import chirp_common, icf, util, directory
+from chirp import bitwise
+
+MEM_FORMAT = """
+struct {
+  ul16  freq;
+  ul16  offset;
+  char name[6];
+  u8   unknown1:2,
+       rtone:6;
+  u8   unknown2:2,
+       ctone:6;
+  u8   unknown3:1,
+       dtcs:7;
+  u8   unknown4:4,
+       tuning_step:4;
+  u8   unknown5[3];
+  u8   unknown6:4,
+       urcall:4;
+  u8   r1call:4,
+       r2call:4;
+  u8   unknown7:1,
+       digital_code:7;
+  u8   is_625:1,
+       unknown8:1,
+       mode_am:1,
+       mode_narrow:1,
+       power:2,
+       tmode:2;
+  u8   dtcs_polarity:2,
+       duplex:2,
+       unknown10:4;
+  u8   unknown11;
+  u8   mode_dv:1,
+       unknown12:7;
+} memory[207];
+
+#seekto 0x1370;
+struct {
+  u8 unknown:2,
+     empty:1,
+     skip:1,
+     bank:4;
+} flags[207];
+
+#seekto 0x15F0;
+struct {
+  char call[8];
+} mycalls[6];
+
+struct {
+  char call[8];
+} urcalls[6];
+
+struct {
+  char call[8];
+} rptcalls[6];
+
+"""
+
+TMODES = ["", "Tone", "TSQL", "DTCS"]
+DUPLEX = ["", "-", "+"]
+DTCSP  = ["NN", "NR", "RN", "RR"]
+STEPS =  [5.0, 10.0, 12.5, 15.0, 20.0, 25.0, 30.0, 50.0]
+
+POWER_LEVELS = [chirp_common.PowerLevel("High", watts=65),
+                chirp_common.PowerLevel("Mid", watts=25),
+                chirp_common.PowerLevel("MidLow", watts=10),
+                chirp_common.PowerLevel("Low", watts=5)]
+
+def _get_special():
+    special = { "C" : 206 }
+    for i in range(0, 3):
+        ida = "%iA" % (i+1)
+        idb = "%iB" % (i+1)
+        num = 200 + i * 2
+        special[ida] = num
+        special[idb] = num + 1
+
+    return special
+
+def _wipe_memory(mem, char):
+    mem.set_raw(char * (mem.size() / 8))
+
+ at directory.register
+class IC2200Radio(icf.IcomCloneModeRadio, chirp_common.IcomDstarSupport):
+    """Icom IC-2200"""
+    VENDOR = "Icom"
+    MODEL = "IC-2200H"
+
+    _model = "\x26\x98\x00\x01"
+    _memsize = 6848
+    _endframe = "Icom Inc\x2eD8"
+    _can_hispeed = True
+
+    _memories = []
+
+    _ranges = [(0x0000, 0x1340, 32),
+               (0x1340, 0x1360, 16),
+               (0x1360, 0x136B,  8),
+
+               (0x1370, 0x1380, 16),
+               (0x1380, 0x15E0, 32),
+               (0x15E0, 0x1600, 16),
+               (0x1600, 0x1640, 32),
+               (0x1640, 0x1660, 16),
+               (0x1660, 0x1680, 32),
+
+               (0x16E0, 0x1860, 32),
+
+               (0x1880, 0x1AB0, 32),
+
+               (0x1AB8, 0x1AC0,  8),
+               ]
+
+    MYCALL_LIMIT  = (0, 6)
+    URCALL_LIMIT  = (0, 6)
+    RPTCALL_LIMIT = (0, 6)
+
+    def _get_bank(self, loc):
+        _flag = self._memobj.flags[loc]
+        if _flag.bank == 0x0A:
+            return None
+        else:
+            return _flag.bank
+
+    def _set_bank(self, loc, bank):
+        _flag = self._memobj.flags[loc]
+        if bank is None:
+            _flag.bank = 0x0A
+        else:
+            _flag.bank = bank
+        
+    def get_features(self):
+        rf = chirp_common.RadioFeatures()
+        rf.memory_bounds = (0, 199)
+        rf.valid_modes = ["FM", "NFM", "AM", "NAM", "DV"]
+        rf.valid_tmodes = list(TMODES)
+        rf.valid_duplexes = list(DUPLEX)
+        rf.valid_tuning_steps = list(STEPS)
+        rf.valid_bands = [(118000000, 174000000)]
+        rf.valid_skips = ["", "S"]
+        rf.valid_power_levels = POWER_LEVELS
+        rf.valid_special_chans = sorted(_get_special().keys())
+        rf.has_settings = True
+
+        return rf
+
+    def process_mmap(self):
+        self._memobj = bitwise.parse(MEM_FORMAT, self._mmap)
+
+    def get_memory(self, number):
+        if isinstance(number, str):
+            number = _get_special()[number]
+
+        _mem = self._memobj.memory[number]
+        _flag = self._memobj.flags[number]
+
+        if _mem.mode_dv and not _flag.empty:
+            mem = chirp_common.DVMemory()
+            mem.dv_urcall   = \
+                str(self._memobj.urcalls[_mem.urcall].call).rstrip()
+            mem.dv_rpt1call = \
+                str(self._memobj.rptcalls[_mem.r1call].call).rstrip()
+            mem.dv_rpt2call = \
+                str(self._memobj.rptcalls[_mem.r2call].call).rstrip()
+        else:
+            mem = chirp_common.Memory()
+
+        mem.number = number
+        if number < 200:
+            mem.skip = _flag.skip and "S" or ""
+        else:
+            mem.extd_number = util.get_dict_rev(_get_special(), number)
+            mem.immutable = ["number", "skip", "bank", "bank_index",
+                             "extd_number"]
+
+        if _flag.empty:
+            mem.empty = True
+            mem.power = POWER_LEVELS[0]
+            return mem
+
+        mult = _mem.is_625 and 6250 or 5000
+        mem.freq = (_mem.freq * mult)
+        mem.offset = (_mem.offset * mult)
+        mem.rtone = chirp_common.TONES[_mem.rtone]
+        mem.ctone = chirp_common.TONES[_mem.ctone]
+        mem.tmode = TMODES[_mem.tmode]
+        mem.duplex = DUPLEX[_mem.duplex]
+        mem.mode = _mem.mode_dv and "DV" or _mem.mode_am and "AM" or "FM"
+        if _mem.mode_narrow:
+            mem.mode = "N%s" % mem.mode
+        mem.dtcs = chirp_common.DTCS_CODES[_mem.dtcs]
+        mem.dtcs_polarity = DTCSP[_mem.dtcs_polarity]
+        mem.tuning_step = STEPS[_mem.tuning_step]
+        mem.name = str(_mem.name).replace("\x0E", "").rstrip()
+        mem.power = POWER_LEVELS[_mem.power]
+
+        return mem
+
+    def get_memories(self, lo=0, hi=199):
+
+        return [m for m in self._memories if m.number >= lo and m.number <= hi]
+
+    def set_memory(self, mem):
+        if isinstance(mem.number, str):
+            number = _get_special()[mem.number]
+        else:
+            number = mem.number
+
+        _mem = self._memobj.memory[number]
+        _flag = self._memobj.flags[number]
+
+        was_empty = int(_flag.empty)
+
+        _flag.empty = mem.empty
+        if mem.empty:
+            _wipe_memory(_mem, "\xFF")
+            return
+
+        if was_empty:
+            _wipe_memory(_mem, "\x00")
+
+        _mem.unknown8 = 0
+        _mem.is_625 = chirp_common.is_fractional_step(mem.freq)
+        mult = _mem.is_625 and 6250 or 5000
+        _mem.freq = mem.freq / mult
+        _mem.offset = mem.offset / mult
+        _mem.rtone = chirp_common.TONES.index(mem.rtone)
+        _mem.ctone = chirp_common.TONES.index(mem.ctone)
+        _mem.tmode = TMODES.index(mem.tmode)
+        _mem.duplex = DUPLEX.index(mem.duplex)
+        _mem.mode_dv = mem.mode == "DV"
+        _mem.mode_am = mem.mode.endswith("AM")
+        _mem.mode_narrow = mem.mode.startswith("N")
+        _mem.dtcs = chirp_common.DTCS_CODES.index(mem.dtcs)
+        _mem.dtcs_polarity = DTCSP.index(mem.dtcs_polarity)
+        _mem.tuning_step = STEPS.index(mem.tuning_step)
+        _mem.name = mem.name.ljust(6)
+        if mem.power:
+            _mem.power = POWER_LEVELS.index(mem.power)
+        else:
+            _mem.power = 0
+
+        if number < 200:
+            _flag.skip = mem.skip != ""
+
+        if isinstance(mem, chirp_common.DVMemory):
+            urcalls = self.get_urcall_list()
+            rptcalls = self.get_repeater_call_list()
+            _mem.urcall = urcalls.index(mem.dv_urcall)
+            _mem.r1call = rptcalls.index(mem.dv_rpt1call)
+            _mem.r2call = rptcalls.index(mem.dv_rpt2call)
+
+    def get_raw_memory(self, number):
+        return repr(self._memobj.memory[number])
+
+    def get_urcall_list(self):
+        return [str(x.call).rstrip() for x in self._memobj.urcalls]
+
+    def get_repeater_call_list(self):
+        return [str(x.call).rstrip() for x in self._memobj.rptcalls]
+
+    def get_mycall_list(self):
+        return [str(x.call).rstrip() for x in self._memobj.mycalls]
+
+    def set_urcall_list(self, calls):
+        for i in range(*self.URCALL_LIMIT):
+            self._memobj.urcalls[i].call = calls[i].ljust(8)
+
+    def set_repeater_call_list(self, calls):
+        for i in range(*self.RPTCALL_LIMIT):
+            self._memobj.rptcalls[i].call = calls[i].ljust(8)
+
+    def set_mycall_list(self, calls):
+        for i in range(*self.MYCALL_LIMIT):
+            self._memobj.mycalls[i].call = calls[i].ljust(8)
diff --git a/chirp/ic2720.py b/chirp/ic2720.py
new file mode 100644
index 0000000..24d499a
--- /dev/null
+++ b/chirp/ic2720.py
@@ -0,0 +1,190 @@
+# Copyright 2011 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/>.
+
+from chirp import chirp_common, icf, directory
+from chirp import bitwise
+
+MEM_FORMAT = """
+struct {
+    u32 freq;
+    u32 offset;
+    u8 unknown1:2,
+       rtone:6;
+    u8 unknown2:2,
+       ctone:6;
+    u8 unknown3:1,
+       dtcs:7;
+    u8 unknown4:2,
+       unknown5:2,
+       tuning_step:4;
+    u8 unknown6:2,
+       tmode:2,
+       duplex:2,
+       unknown7:2;
+    u8 power:2,
+       is_fm:1,
+       unknown8:1,
+       dtcs_polarity:2,
+       unknown9:2;
+    u8 unknown[2];
+} memory[200];
+
+#seekto 0x0E20;
+u8 skips[25];
+
+#seekto 0x0EB0;
+u8 used[25];
+
+#seekto 0x0E40;
+struct {
+  u8 bank_even:4,
+     bank_odd:4;
+} banks[100];
+"""
+
+TMODES = ["", "Tone", "TSQL", "DTCS"]
+POWER = ["High", "Low", "Med"]
+DTCS_POLARITY = ["NN", "NR", "RN", "RR"]
+STEPS = [5.0, 10.0, 12.5, 15, 20, 25, 30, 50]
+MODES = ["FM", "AM"]
+DUPLEX = ["", "", "-", "+"]
+POWER_LEVELS_VHF = [chirp_common.PowerLevel("High", watts=50),
+                    chirp_common.PowerLevel("Low", watts=5),
+                    chirp_common.PowerLevel("Mid", watts=15)]
+POWER_LEVELS_UHF = [chirp_common.PowerLevel("High", watts=35),
+                    chirp_common.PowerLevel("Low", watts=5),
+                    chirp_common.PowerLevel("Mid", watts=15)]
+
+ at directory.register
+class IC2720Radio(icf.IcomCloneModeRadio):
+    """Icom IC-2720"""
+    VENDOR = "Icom"
+    MODEL = "IC-2720H"
+    
+    _model = "\x24\x92\x00\x01"
+    _memsize = 5152
+    _endframe = "Icom Inc\x2eA0"
+
+    _ranges = [(0x0000, 0x1400, 32)]
+
+    def _get_bank(self, loc):
+        _bank = self._memobj.banks[loc / 2]
+        if loc % 2:
+            bank = _bank.bank_odd
+        else:
+            bank = _bank.bank_even
+
+        if bank == 0x0A:
+            return None
+        else:
+            return bank
+
+    def _set_bank(self, loc, index):
+        _bank = self._memobj.banks[loc / 2]
+        if index is None:
+            index = 0x0A
+        if loc % 2:
+            _bank.bank_odd = index
+        else:
+            _bank.bank_even = index
+
+    def get_features(self):
+        rf = chirp_common.RadioFeatures()
+        rf.has_name = False
+        rf.memory_bounds = (0, 199)
+        rf.valid_modes = list(MODES)
+        rf.valid_tmodes = list(TMODES)
+        rf.valid_duplexes = list(set(DUPLEX))
+        rf.valid_tuning_steps = list(STEPS)
+        rf.valid_bands = [(118000000, 999990000)]
+        rf.valid_skips = ["", "S"]
+        rf.valid_power_levels = POWER_LEVELS_VHF
+        return rf
+
+    def process_mmap(self):
+        self._memobj = bitwise.parse(MEM_FORMAT, self._mmap)
+
+    def get_raw_memory(self, number):
+        return repr(self._memobj.memory[number])
+
+    def get_memory(self, number):
+        bitpos = (1 << (number % 8))
+        bytepos = (number / 8)
+
+        _mem = self._memobj.memory[number]
+        _skp = self._memobj.skips[bytepos]
+        _usd = self._memobj.used[bytepos]
+
+        mem = chirp_common.Memory()
+        mem.number = number
+
+        if _usd & bitpos:
+            mem.empty = True
+            return mem
+
+        mem.freq = int(_mem.freq)
+        mem.offset = int(_mem.offset)
+        mem.rtone = chirp_common.TONES[_mem.rtone]
+        mem.ctone = chirp_common.TONES[_mem.ctone]
+        mem.dtcs = chirp_common.DTCS_CODES[_mem.dtcs]
+        mem.tmode = TMODES[_mem.tmode]
+        mem.dtcs_polarity = DTCS_POLARITY[_mem.dtcs_polarity]
+        mem.tuning_step = STEPS[_mem.tuning_step]
+        mem.mode = _mem.is_fm and "FM" or "AM"
+        mem.duplex = DUPLEX[_mem.duplex]
+
+        mem.skip = (_skp & bitpos) and "S" or ""
+
+        if int(mem.freq / 100000000) == 1:
+            mem.power = POWER_LEVELS_VHF[_mem.power]
+        else:
+            mem.power = POWER_LEVELS_UHF[_mem.power]
+
+        return mem
+
+    def set_memory(self, mem):
+        bitpos = (1 << (mem.number % 8))
+        bytepos = (mem.number / 8)
+
+        _mem = self._memobj.memory[mem.number]
+        _skp = self._memobj.skips[bytepos]
+        _usd = self._memobj.used[bytepos]
+        
+        if mem.empty:
+            _usd |= bitpos
+            self._set_bank(mem.number, None)
+            return
+        _usd &= ~bitpos
+
+        _mem.freq = mem.freq
+        _mem.offset = mem.offset
+        _mem.rtone = chirp_common.TONES.index(mem.rtone)
+        _mem.ctone = chirp_common.TONES.index(mem.ctone)
+        _mem.dtcs = chirp_common.DTCS_CODES.index(mem.dtcs)
+        _mem.tmode = TMODES.index(mem.tmode)
+        _mem.dtcs_polarity = DTCS_POLARITY.index(mem.dtcs_polarity)
+        _mem.tuning_step = STEPS.index(mem.tuning_step)
+        _mem.is_fm = mem.mode == "FM"
+        _mem.duplex = DUPLEX.index(mem.duplex)
+        
+        if mem.skip == "S":
+            _skp |= bitpos
+        else:
+            _skp &= ~bitpos
+
+        if mem.power:
+            _mem.power = POWER_LEVELS_VHF.index(mem.power)
+        else:
+            _mem.power = 0
diff --git a/chirp/ic2820.py b/chirp/ic2820.py
new file mode 100644
index 0000000..6475713
--- /dev/null
+++ b/chirp/ic2820.py
@@ -0,0 +1,350 @@
+# Copyright 2010 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/>.
+
+from chirp import chirp_common, icf, util, directory
+from chirp import bitwise
+
+MEM_FORMAT = """
+struct {
+  u32  freq;
+  u32  offset;
+  char urcall[8];
+  char r1call[8];
+  char r2call[8];
+  u8   unknown1;
+  u8   unknown2:1,
+       duplex:2,
+       tmode:3,
+       unknown3:2;
+  u16  ctone:6,
+       rtone:6,
+       tune_step:4;
+  u16  dtcs:7,
+       mode:3,
+       unknown4:6;
+  u8   unknown5:1,
+       digital_code:7;
+  u8   unknown6:2,
+       dtcs_polarity:2,
+       unknown7:4;
+  char name[8];
+} memory[522];
+
+#seekto 0x61E0;
+u8 used_flags[66];
+
+#seekto 0x6222;
+u8 skip_flags[65];
+u8 pskip_flags[65];
+
+#seekto 0x62A4;
+struct {
+  u8 bank;
+  u8 index;
+} bank_info[500];
+
+#seekto 0x66C0;
+struct {
+  char name[8];
+} bank_names[26];
+
+#seekto 0x6970;
+struct {
+  char call[8];
+  u8 unknown[4];
+} mycall[6];
+
+#seekto 0x69B8;
+struct {
+  char call[8];
+} urcall[60];
+
+struct {
+  char call[8];
+} rptcall[60];
+
+"""
+
+TMODES = ["", "Tone", "??0", "TSQL", "??1", "??2", "DTCS"]
+DUPLEX = ["", "-", "+", "+"] # Not sure about index 3
+MODES  = ["FM", "NFM", "AM", "??", "DV"]
+DTCSP  = ["NN", "NR", "RN", "RR"]
+
+MEM_LOC_SIZE = 48
+
+class IC2820Bank(icf.IcomNamedBank):
+    """An IC2820 bank"""
+    def get_name(self):
+        _banks = self._model._radio._memobj.bank_names
+        return str(_banks[self.index].name).rstrip()
+
+    def set_name(self, name):
+        _banks = self._model._radio._memobj.bank_names
+        _banks[self.index].name = str(name).ljust(8)[:8]
+
+def _get_special():
+    special = {"C0" : 500 + 20,
+               "C1" : 500 + 21}
+
+    for i in range(0, 10):
+        ida = "%iA" % i
+        idb = "%iB" % i
+        special[ida] = 500 + i * 2
+        special[idb] = 500 + i * 2 + 1
+
+    return special
+
+def _resolve_memory_number(number):
+    if isinstance(number, str):
+        return _get_special()[number]
+    else:
+        return number
+
+def _wipe_memory(mem, char):
+    mem.set_raw(char * (mem.size() / 8))
+
+ at directory.register
+class IC2820Radio(icf.IcomCloneModeRadio, chirp_common.IcomDstarSupport):
+    """Icom IC-2820"""
+    VENDOR = "Icom"
+    MODEL = "IC-2820H"
+
+    _model = "\x29\x70\x00\x01"
+    _memsize = 44224
+    _endframe = "Icom Inc\x2e68"
+
+    _ranges = [(0x0000, 0x6960, 32),
+               (0x6960, 0x6980, 16),
+               (0x6980, 0x7160, 32),
+               (0x7160, 0x7180, 16),
+               (0x7180, 0xACC0, 32),]
+
+    _num_banks = 26
+    _bank_class = IC2820Bank
+    _can_hispeed = True
+
+    MYCALL_LIMIT = (1, 7)
+    URCALL_LIMIT = (1, 61)
+    RPTCALL_LIMIT = (1, 61)
+
+    _memories = {}
+
+    def _get_bank(self, loc):
+        _bank = self._memobj.bank_info[loc]
+        if _bank.bank == 0xFF:
+            return None
+        else:
+            return _bank.bank
+
+    def _set_bank(self, loc, bank):
+        _bank = self._memobj.bank_info[loc]
+        if bank is None:
+            _bank.bank = 0xFF
+        else:
+            _bank.bank = bank
+
+    def _get_bank_index(self, loc):
+        _bank = self._memobj.bank_info[loc]
+        return _bank.index
+
+    def _set_bank_index(self, loc, index):
+        _bank = self._memobj.bank_info[loc]
+        _bank.index = index
+
+    def get_features(self):
+        rf = chirp_common.RadioFeatures()
+        rf.has_settings = True
+        rf.has_bank_index = True
+        rf.has_bank_names = True
+        rf.requires_call_lists = False
+        rf.memory_bounds = (0, 499)
+        rf.valid_modes = list(MODES)
+        rf.valid_tmodes = list(TMODES)
+        rf.valid_duplexes = list(set(DUPLEX))
+        rf.valid_tuning_steps = list(chirp_common.TUNING_STEPS)
+        rf.valid_bands = [(118000000, 999990000)]
+        rf.valid_skips = ["", "S", "P"]
+        rf.valid_characters = chirp_common.CHARSET_ALPHANUMERIC
+        rf.valid_name_length = 8
+        rf.valid_special_chans = sorted(_get_special().keys())
+
+        return rf
+
+    def process_mmap(self):
+        self._memobj = bitwise.parse(MEM_FORMAT, self._mmap)
+
+    def get_memory(self, number):
+        number = _resolve_memory_number(number)
+
+        bitpos = (1 << (number % 8))
+        bytepos = number / 8
+
+        _mem = self._memobj.memory[number]
+        _used = self._memobj.used_flags[bytepos]
+
+        is_used = ((_used & bitpos) == 0)
+
+        if is_used and MODES[_mem.mode] == "DV":
+            mem = chirp_common.DVMemory()
+            mem.dv_urcall = str(_mem.urcall).rstrip()
+            mem.dv_rpt1call = str(_mem.r1call).rstrip()
+            mem.dv_rpt2call = str(_mem.r2call).rstrip()
+        else:
+            mem = chirp_common.Memory()
+
+        mem.number = number
+        if number < 500:
+            _skip = self._memobj.skip_flags[bytepos]
+            _pskip = self._memobj.pskip_flags[bytepos]
+            if _skip & bitpos:
+                mem.skip = "S"
+            elif _pskip & bitpos:
+                mem.skip = "P"
+        else:
+            mem.extd_number = util.get_dict_rev(_get_special(), number)
+            mem.immutable = ["number", "skip", "bank", "bank_index",
+                             "extd_number"]
+
+        if not is_used:
+            mem.empty = True
+            return mem
+
+        mem.freq = int(_mem.freq)
+        mem.offset = int(_mem.offset)
+        mem.rtone = chirp_common.TONES[_mem.rtone]
+        mem.ctone = chirp_common.TONES[_mem.ctone]
+        mem.tmode = TMODES[_mem.tmode]
+        mem.duplex = DUPLEX[_mem.duplex]
+        mem.mode = MODES[_mem.mode]
+        mem.dtcs = chirp_common.DTCS_CODES[_mem.dtcs]
+        mem.dtcs_polarity = DTCSP[_mem.dtcs_polarity]
+        if _mem.tune_step > 8:
+            mem.tuning_step = 5.0 # Sometimes TS is garbage?
+        else:
+            mem.tuning_step = chirp_common.TUNING_STEPS[_mem.tune_step]
+        mem.name = str(_mem.name).rstrip()
+
+        return mem
+
+    def set_memory(self, mem):
+        bitpos = (1 << (mem.number % 8))
+        bytepos = mem.number / 8
+
+        _mem = self._memobj.memory[mem.number]
+        _used = self._memobj.used_flags[bytepos]
+
+        was_empty = _used & bitpos
+
+        if mem.number < 500:
+            skip = self._memobj.skip_flags[bytepos]
+            pskip = self._memobj.pskip_flags[bytepos]
+            if mem.skip == "S":
+                skip |= bitpos
+            else:
+                skip &= ~bitpos
+            if mem.skip == "P":
+                pskip |= bitpos
+            else:
+                pskip &= ~bitpos
+
+        if mem.empty:
+            _used |= bitpos
+            _wipe_memory(_mem, "\xFF")
+            self._set_bank(mem.number, None)
+            return
+
+        _used &= ~bitpos
+        if was_empty:
+            _wipe_memory(_mem, "\x00")
+
+        _mem.freq = mem.freq
+        _mem.offset = mem.offset
+        _mem.rtone = chirp_common.TONES.index(mem.rtone)
+        _mem.ctone = chirp_common.TONES.index(mem.ctone)
+        _mem.tmode = TMODES.index(mem.tmode)
+        _mem.duplex = DUPLEX.index(mem.duplex)
+        _mem.mode = MODES.index(mem.mode)
+        _mem.dtcs = chirp_common.DTCS_CODES.index(mem.dtcs)
+        _mem.dtcs_polarity = DTCSP.index(mem.dtcs_polarity)
+        _mem.tune_step = chirp_common.TUNING_STEPS.index(mem.tuning_step)
+        _mem.name = mem.name.ljust(8)
+
+        if isinstance(mem, chirp_common.DVMemory):
+            _mem.urcall = mem.dv_urcall.ljust(8)
+            _mem.r1call = mem.dv_rpt1call.ljust(8)
+            _mem.r2call = mem.dv_rpt2call.ljust(8)
+            
+    def get_raw_memory(self, number):
+        return repr(self._memobj.memory[number])
+    
+    def get_urcall_list(self):
+        _calls = self._memobj.urcall
+        calls = []
+
+        for i in range(*self.URCALL_LIMIT):
+            calls.append(str(_calls[i-1].call))
+
+        return calls
+
+    def get_repeater_call_list(self):
+        _calls = self._memobj.rptcall
+        calls = []
+
+        for i in range(*self.RPTCALL_LIMIT):
+            calls.append(str(_calls[i-1].call))
+
+        return calls
+
+    def get_mycall_list(self):
+        _calls = self._memobj.mycall
+        calls = []
+        
+        for i in range(*self.MYCALL_LIMIT):
+            calls.append(str(_calls[i-1].call))
+
+        return calls
+
+    def set_urcall_list(self, calls):
+        _calls = self._memobj.urcall
+
+        for i in range(*self.URCALL_LIMIT):
+            try:
+                call = calls[i-1]
+            except IndexError:
+                call = " " * 8
+
+            _calls[i-1].call = call.ljust(8)[:8]
+
+    def set_repeater_call_list(self, calls):
+        _calls = self._memobj.rptcall
+
+        for i in range(*self.RPTCALL_LIMIT):
+            try:
+                call = calls[i-1]
+            except IndexError:
+                call = " " * 8
+
+            _calls[i-1].call = call.ljust(8)[:8]
+
+    def set_mycall_list(self, calls):
+        _calls = self._memobj.mycall
+
+        for i in range(*self.MYCALL_LIMIT):
+            try:
+                call = calls[i-1]
+            except IndexError:
+                call = " " * 8
+
+            _calls[i-1].call = call.ljust(8)[:8]
diff --git a/chirp/ic9x.py b/chirp/ic9x.py
new file mode 100644
index 0000000..cc388a6
--- /dev/null
+++ b/chirp/ic9x.py
@@ -0,0 +1,417 @@
+# 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 time
+import threading
+
+from chirp import chirp_common, errors, ic9x_ll, icf, util, directory
+from chirp import bitwise
+
+IC9XA_SPECIAL = {}
+IC9XB_SPECIAL = {}
+
+for i in range(0, 25):
+    idA = "%iA" % i
+    idB = "%iB" % i
+    Anum = 800 + i * 2
+    Bnum = 400 + i * 2
+
+    IC9XA_SPECIAL[idA] = Anum
+    IC9XA_SPECIAL[idB] = Bnum
+
+    IC9XB_SPECIAL[idA] = Bnum
+    IC9XB_SPECIAL[idB] = Bnum + 1
+
+IC9XA_SPECIAL["C0"] = IC9XB_SPECIAL["C0"] = -1
+IC9XA_SPECIAL["C1"] = IC9XB_SPECIAL["C1"] = -2
+
+IC9X_SPECIAL = {
+    0 : {},
+    1 : IC9XA_SPECIAL,
+    2 : IC9XB_SPECIAL,
+}
+
+CHARSET = chirp_common.CHARSET_ALPHANUMERIC + \
+    "!\"#$%&'()*+,-./:;<=>?@[\]^_`{|}~"
+
+LOCK = threading.Lock()
+
+class IC9xBank(icf.IcomNamedBank):
+    """Icom 9x Bank"""
+    def get_name(self):
+        banks = self._model._radio._ic9x_get_banks()
+        return banks[self.index]
+
+    def set_name(self, name):
+        banks = self._model._radio._ic9x_get_banks()
+        banks[self.index] = name
+        self._model._radio._ic9x_set_banks(banks)
+
+ at directory.register
+class IC9xRadio(icf.IcomLiveRadio):
+    """Base class for Icom IC-9x radios"""
+    MODEL = "IC-91/92AD"
+
+    _model = "ic9x" # Fake model info for detect.py
+    vfo = 0
+    __last = 0
+    _upper = 300
+
+    _num_banks = 26
+    _bank_class = IC9xBank
+
+    def _get_bank(self, loc):
+        mem = self.get_memory(loc)
+        return mem._bank
+
+    def _set_bank(self, loc, bank):
+        mem = self.get_memory(loc)
+        mem._bank = bank
+        self.set_memory(mem)
+
+    def _get_bank_index(self, loc):
+        mem = self.get_memory(loc)
+        return mem._bank_index
+
+    def _set_bank_index(self, loc, index):
+        mem = self.get_memory(loc)
+        mem._bank_index = index
+        self.set_memory(mem)
+
+    def __init__(self, *args, **kwargs):
+        icf.IcomLiveRadio.__init__(self, *args, **kwargs)
+
+        if self.pipe:
+            self.pipe.setTimeout(0.1)
+
+        self.__memcache = {}
+        self.__bankcache = {}
+
+        global LOCK
+        self._lock = LOCK
+
+    def _maybe_send_magic(self):
+        if (time.time() - self.__last) > 1:
+            print "Sending magic"
+            ic9x_ll.send_magic(self.pipe)
+        self.__last = time.time()
+
+    def get_memory(self, number):
+        if isinstance(number, str):
+            try:
+                number = IC9X_SPECIAL[self.vfo][number]
+            except KeyError:
+                raise errors.InvalidMemoryLocation("Unknown channel %s" % \
+                                                       number)
+
+        if number < -2 or number > 999:
+            raise errors.InvalidValueError("Number must be between 0 and 999")
+
+        if self.__memcache.has_key(number):
+            return self.__memcache[number]
+
+        self._lock.acquire()
+        try:
+            self._maybe_send_magic()
+            mem = ic9x_ll.get_memory(self.pipe, self.vfo, number)
+        except errors.InvalidMemoryLocation:
+            mem = chirp_common.Memory()
+            mem.number = number
+            if number < self._upper:
+                mem.empty = True
+        except:
+            self._lock.release()
+            raise
+
+        self._lock.release()
+
+        if number > self._upper or number < 0:
+            mem.extd_number = util.get_dict_rev(IC9X_SPECIAL,
+                                                [self.vfo][number])
+            mem.immutable = ["number", "skip", "bank", "bank_index",
+                             "extd_number"]
+
+        self.__memcache[mem.number] = mem
+
+        return mem
+
+    def get_raw_memory(self, number):
+        self._lock.acquire()
+        try:
+            ic9x_ll.send_magic(self.pipe)
+            mframe = ic9x_ll.get_memory_frame(self.pipe, self.vfo, number)
+        except:
+            self._lock.release()
+            raise
+
+        self._lock.release()
+
+        return repr(bitwise.parse(ic9x_ll.MEMORY_FRAME_FORMAT, mframe))
+
+    def get_memories(self, lo=0, hi=None):
+        if hi is None:
+            hi = self._upper            
+
+        memories = []
+
+        for i in range(lo, hi + 1):
+            try:
+                print "Getting %i" % i
+                mem = self.get_memory(i)
+                if mem:
+                    memories.append(mem)
+                print "Done: %s" % mem
+            except errors.InvalidMemoryLocation:
+                pass
+            except errors.InvalidDataError, e:
+                print "Error talking to radio: %s" % e
+                break
+
+        return memories
+        
+    def set_memory(self, _memory):
+        # Make sure we mirror the DV-ness of the new memory we're
+        # setting, and that we capture the Bank value of any currently
+        # stored memory (unless the special type is provided) and
+        # communicate that to the low-level routines with the special
+        # subclass
+        if isinstance(_memory, ic9x_ll.IC9xMemory) or \
+                 isinstance(_memory, ic9x_ll.IC9xDVMemory):
+            memory = _memory
+        else:
+            if isinstance(_memory, chirp_common.DVMemory):
+                memory = ic9x_ll.IC9xDVMemory()
+                memory.clone(self.get_memory(_memory.number))
+            else:
+                memory = ic9x_ll.IC9xMemory()
+                memory.clone(self.get_memory(_memory.number))
+
+            memory.clone(_memory)
+
+        self._lock.acquire()
+        self._maybe_send_magic()
+        try:
+            if memory.empty:
+                ic9x_ll.erase_memory(self.pipe, self.vfo, memory.number)
+            else:
+                ic9x_ll.set_memory(self.pipe, self.vfo, memory)
+            memory = ic9x_ll.get_memory(self.pipe, self.vfo, memory.number)
+        except:
+            self._lock.release()
+            raise
+
+        self._lock.release()
+
+        self.__memcache[memory.number] = memory
+
+    def _ic9x_get_banks(self):
+        if len(self.__bankcache.keys()) == 26:
+            return [self.__bankcache[k] for k in
+                    sorted(self.__bankcache.keys())]
+
+        self._lock.acquire()
+        try:
+            self._maybe_send_magic()
+            banks = ic9x_ll.get_banks(self.pipe, self.vfo)
+        except:
+            self._lock.release()
+            raise
+
+        self._lock.release()
+
+        i = 0
+        for bank in banks:
+            self.__bankcache[i] = bank
+            i += 1
+
+        return banks
+        
+    def _ic9x_set_banks(self, banks):
+
+        if len(banks) != len(self.__bankcache.keys()):
+            raise errors.InvalidDataError("Invalid bank list length (%i:%i)" %\
+                                              (len(banks),
+                                               len(self.__bankcache.keys())))
+
+        cached_names = [str(self.__bankcache[x]) \
+                            for x in sorted(self.__bankcache.keys())]
+
+        need_update = False
+        for i in range(0, 26):
+            if banks[i] != cached_names[i]:
+                need_update = True
+                self.__bankcache[i] = banks[i]
+                print "Updating %s: %s -> %s" % (chr(i + ord("A")),
+                                                 cached_names[i],
+                                                 banks[i])
+
+        if need_update:
+            self._lock.acquire()
+            try:
+                self._maybe_send_magic()
+                ic9x_ll.set_banks(self.pipe, self.vfo, banks)
+            except:
+                self._lock.release()
+                raise
+
+            self._lock.release()
+
+    def get_sub_devices(self):
+        return [IC9xRadioA(self.pipe), IC9xRadioB(self.pipe)]
+
+    def get_features(self):
+        rf = chirp_common.RadioFeatures()
+        rf.has_sub_devices = True
+        rf.valid_special_chans = IC9X_SPECIAL[self.vfo].keys()
+    
+        return rf
+
+class IC9xRadioA(IC9xRadio):
+    """IC9x Band A subdevice"""
+    VARIANT = "Band A"
+    vfo = 1
+    _upper = 849
+
+    def get_features(self):
+        rf = chirp_common.RadioFeatures()
+        rf.has_bank = True
+        rf.has_bank_index = True
+        rf.has_bank_names = True
+        rf.memory_bounds = (0, self._upper)
+        rf.valid_modes = ["FM", "WFM", "AM"]
+        rf.valid_tmodes = ["", "Tone", "TSQL", "DTCS"]
+        rf.valid_duplexes = ["", "-", "+"]
+        rf.valid_tuning_steps = list(chirp_common.TUNING_STEPS)
+        rf.valid_bands = [(500000, 9990000000)]
+        rf.valid_skips = ["", "S", "P"]
+        rf.valid_characters = CHARSET
+        rf.valid_name_length = 8
+        return rf
+
+class IC9xRadioB(IC9xRadio, chirp_common.IcomDstarSupport):
+    """IC9x Band B subdevice"""
+    VARIANT = "Band B"
+    vfo = 2
+    _upper = 399
+
+    MYCALL_LIMIT = (1, 7)
+    URCALL_LIMIT = (1, 61)
+    RPTCALL_LIMIT = (1, 61)
+
+    def get_features(self):
+        rf = chirp_common.RadioFeatures()
+        rf.has_bank = True
+        rf.has_bank_index = True
+        rf.has_bank_names = True
+        rf.requires_call_lists = False
+        rf.memory_bounds = (0, self._upper)
+        rf.valid_modes = ["FM", "NFM", "AM", "DV"]
+        rf.valid_tmodes = ["", "Tone", "TSQL", "DTCS"]
+        rf.valid_duplexes = ["", "-", "+"]
+        rf.valid_tuning_steps = list(chirp_common.TUNING_STEPS)
+        rf.valid_bands = [(118000000, 174000000), (350000000, 470000000)]
+        rf.valid_skips = ["", "S", "P"]
+        rf.valid_characters = CHARSET
+        rf.valid_name_length = 8
+        return rf
+
+    def __init__(self, *args, **kwargs):
+        IC9xRadio.__init__(self, *args, **kwargs)
+
+        self.__rcalls = []
+        self.__mcalls = []
+        self.__ucalls = []
+
+    def __get_call_list(self, cache, cstype, ulimit):
+        if cache:
+            return cache
+
+        calls = []
+
+        self._maybe_send_magic()
+        for i in range(ulimit - 1):
+            call = ic9x_ll.get_call(self.pipe, cstype, i+1)
+            calls.append(call)
+
+        return calls
+
+    def __set_call_list(self, cache, cstype, ulimit, calls):
+        for i in range(ulimit - 1):
+            blank = " " * 8
+
+            try:
+                acall = cache[i]
+            except IndexError:
+                acall = blank
+
+            try:
+                bcall = calls[i]
+            except IndexError:
+                bcall = blank
+            
+            if acall == bcall:
+                continue # No change to this one
+
+            self._maybe_send_magic()
+            ic9x_ll.set_call(self.pipe, cstype, i+1, calls[i])
+
+        return calls
+
+    def get_mycall_list(self):
+        self.__mcalls = self.__get_call_list(self.__mcalls,
+                                             ic9x_ll.IC92MyCallsignFrame,
+                                             self.MYCALL_LIMIT[1])
+        return self.__mcalls
+        
+    def get_urcall_list(self):
+        self.__ucalls = self.__get_call_list(self.__ucalls,
+                                             ic9x_ll.IC92YourCallsignFrame,
+                                             self.URCALL_LIMIT[1])
+        return self.__ucalls
+
+    def get_repeater_call_list(self):
+        self.__rcalls = self.__get_call_list(self.__rcalls,
+                                             ic9x_ll.IC92RepeaterCallsignFrame,
+                                             self.RPTCALL_LIMIT[1])
+        return self.__rcalls
+
+    def set_mycall_list(self, calls):
+        self.__mcalls = self.__set_call_list(self.__mcalls,
+                                             ic9x_ll.IC92MyCallsignFrame,
+                                             self.MYCALL_LIMIT[1],
+                                             calls)
+
+    def set_urcall_list(self, calls):
+        self.__ucalls = self.__set_call_list(self.__ucalls,
+                                             ic9x_ll.IC92YourCallsignFrame,
+                                             self.URCALL_LIMIT[1],
+                                             calls)
+
+    def set_repeater_call_list(self, calls):
+        self.__rcalls = self.__set_call_list(self.__rcalls,
+                                             ic9x_ll.IC92RepeaterCallsignFrame,
+                                             self.RPTCALL_LIMIT[1],
+                                             calls)
+
+def _test():
+    import serial
+    ser = IC9xRadioB(serial.Serial(port="/dev/ttyUSB1",
+                                   baudrate=38400, timeout=0.1))
+    print ser.get_urcall_list()
+    print "-- FOO --"
+    ser.set_urcall_list(["K7TAY", "FOOBAR", "BAZ"])
+
+if __name__ == "__main__":
+    _test()
diff --git a/chirp/ic9x_icf.py b/chirp/ic9x_icf.py
new file mode 100644
index 0000000..89b3beb
--- /dev/null
+++ b/chirp/ic9x_icf.py
@@ -0,0 +1,76 @@
+# Copyright 2010 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/>.
+
+from chirp import chirp_common, icf, ic9x_icf_ll, util, directory, errors
+
+ at directory.register
+class IC9xICFRadio(chirp_common.CloneModeRadio):
+    VENDOR = "Icom"
+    MODEL = "IC-91/92AD"
+    VARIANT = "ICF File"
+    _model = None
+
+    _upper = 1200
+
+    def get_features(self):
+        rf = chirp_common.RadioFeatures()
+        rf.has_bank = False
+        rf.memory_bounds = (0, self._upper)
+        rf.has_sub_devices = True
+        rf.valid_modes = ["FM", "AM"]
+        if "A" in self.VARIANT:
+            rf.valid_modes.append("WFM")
+        else:
+            rf.valid_modes.append("DV")
+            rf.valid_modes.append("NFM")
+        return rf
+
+    def get_raw_memory(self, number):
+        raw = ic9x_icf_ll.get_raw_memory(self._mmap, number).get_packed()
+        return util.hexprint(raw)
+
+    def get_memory(self, number):
+        return ic9x_icf_ll.get_memory(self._mmap, number)
+
+    def load_mmap(self, filename):
+        _mdata, self._mmap = icf.read_file(filename)
+
+    def get_sub_devices(self):
+        return [IC9xICFRadioA(self._mmap),
+                IC9xICFRadioB(self._mmap)]
+
+class IC9xICFRadioA(IC9xICFRadio):
+    VARIANT = "ICF File Band A"
+
+    _upper = 800
+
+    def get_memory(self, number):
+        if number > self._upper:
+            raise errors.InvalidMemoryLocation("Number must be <800")
+
+        return ic9x_icf_ll.get_memory(self._mmap, number)
+
+class IC9xICFRadioB(IC9xICFRadio):
+    VARIANT = "ICF File Band B"
+
+    _upper = 400
+
+    def get_memory(self, number):
+        if number > self._upper:
+            raise errors.InvalidMemoryLocation("Number must be <400")
+
+        mem = ic9x_icf_ll.get_memory(self._mmap, 850 + number)
+        mem.number = number
+        return mem
diff --git a/chirp/ic9x_icf_ll.py b/chirp/ic9x_icf_ll.py
new file mode 100644
index 0000000..30376d7
--- /dev/null
+++ b/chirp/ic9x_icf_ll.py
@@ -0,0 +1,139 @@
+# Copyright 2010 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 struct
+from chirp import chirp_common
+from chirp.memmap import MemoryMap
+
+MEM_LOC_SIZE_A = 20
+MEM_LOC_SIZE_B = MEM_LOC_SIZE_A + 1 + (3 * 8)
+
+POS_FREQ     = 0
+POS_OFFSET   = 3
+POS_TONE     = 5
+POS_MODE     = 6
+POS_DTCS     = 7
+POS_TS       = 8
+POS_DTCSPOL  = 11
+POS_DUPLEX   = 11
+POS_NAME     = 12
+
+def get_mem_offset(number):
+    """Get the offset into the memory map for memory @number"""
+    if number < 850:
+        return MEM_LOC_SIZE_A * number
+    else:
+        return (MEM_LOC_SIZE_A * 850) + (MEM_LOC_SIZE_B * (number - 850))
+
+def get_raw_memory(mmap, number):
+    """Return a raw representation of memory @number"""
+    offset = get_mem_offset(number)
+    if number >= 850:
+        size = MEM_LOC_SIZE_B
+    else:
+        size = MEM_LOC_SIZE_A
+    return MemoryMap(mmap[offset:offset+size])
+
+def get_freq(mmap):
+    """Return the memory frequency"""
+    if ord(mmap[10]) & 0x10:
+        mult = 6250
+    else:
+        mult = 5000
+    val, = struct.unpack(">I", "\x00" + mmap[POS_FREQ:POS_FREQ+3])
+    return val * mult
+
+def get_offset(mmap):
+    """Return the memory offset"""
+    val, = struct.unpack(">H", mmap[POS_OFFSET:POS_OFFSET+2])
+    return val * 5000
+
+def get_rtone(mmap):
+    """Return the memory rtone"""
+    val = (ord(mmap[POS_TONE]) & 0xFC) >> 2
+    return chirp_common.TONES[val]
+
+def get_ctone(mmap):
+    """Return the memory ctone"""
+    val = (ord(mmap[POS_TONE]) & 0x03) | ((ord(mmap[POS_TONE+1]) & 0xF0) >> 4)
+    return chirp_common.TONES[val]
+
+def get_dtcs(mmap):
+    """Return the memory dtcs value"""
+    val = ord(mmap[POS_DTCS]) >> 1
+    return chirp_common.DTCS_CODES[val]
+
+def get_mode(mmap):
+    """Return the memory mode"""
+    val = ord(mmap[POS_MODE]) & 0x07
+
+    modemap = ["FM", "NFM", "WFM", "AM", "DV", "FM"]
+
+    return modemap[val]
+
+def get_ts(mmap):
+    """Return the memory tuning step"""
+    val = (ord(mmap[POS_TS]) & 0xF0) >> 4
+    if val == 14:
+        return 5.0 # Coerce "Auto" to 5.0
+
+    icf_ts = list(chirp_common.TUNING_STEPS)
+    icf_ts.insert(2, 8.33)
+    icf_ts.insert(3, 9.00)
+    icf_ts.append(100.0)
+    icf_ts.append(125.0)
+    icf_ts.append(200.0)
+
+    return icf_ts[val]
+
+def get_dtcs_polarity(mmap):
+    """Return the memory dtcs polarity"""
+    val = (ord(mmap[POS_DTCSPOL]) & 0x03)
+
+    pols = ["NN", "NR", "RN", "RR"]
+
+    return pols[val]
+
+def get_duplex(mmap):
+    """Return the memory duplex"""
+    val = (ord(mmap[POS_DUPLEX]) & 0x0C) >> 2
+
+    dup = ["", "-", "+", ""]
+
+    return dup[val]
+
+def get_name(mmap):
+    """Return the memory name"""
+    return mmap[POS_NAME:POS_NAME+8]
+
+def get_memory(_mmap, number):
+    """Get memory @number from global memory map @_mmap"""
+    mmap = get_raw_memory(_mmap, number)
+    mem = chirp_common.Memory()
+    mem.number = number
+    mem.freq = get_freq(mmap)
+    mem.offset = get_offset(mmap)
+    mem.rtone = get_rtone(mmap)
+    mem.ctone = get_ctone(mmap)
+    mem.dtcs = get_dtcs(mmap)
+    mem.mode = get_mode(mmap)
+    mem.tuning_step = get_ts(mmap)
+    mem.dtcs_polarity = get_dtcs_polarity(mmap)
+    mem.duplex = get_duplex(mmap)
+    mem.name = get_name(mmap)
+
+    mem.empty = mem.freq == 0
+
+    return mem
diff --git a/chirp/ic9x_ll.py b/chirp/ic9x_ll.py
new file mode 100644
index 0000000..42a40ed
--- /dev/null
+++ b/chirp/ic9x_ll.py
@@ -0,0 +1,547 @@
+# 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 struct
+
+from chirp import chirp_common, util, errors, bitwise
+from chirp.memmap import MemoryMap
+
+TUNING_STEPS = [
+    5.0, 6.25, 8.33,  9.0, 10.0, 12.5, 15, 20, 25, 30, 50, 100, 125, 200
+    ]
+
+MODES = ["FM", "NFM", "WFM", "AM", "DV"]
+DUPLEX = ["", "-", "+"]
+TMODES = ["", "Tone", "TSQL", "TSQL", "DTCS", "DTCS"]
+DTCS_POL = ["NN", "NR", "RN", "RR"]
+
+MEM_LEN = 34
+DV_MEM_LEN = 60
+
+# Dirty hack until I clean up this IC9x mess
+class IC9xMemory(chirp_common.Memory):
+    """A dirty hack to stash bank information in a memory"""
+    _bank = None
+    _bank_index = 0
+    def __init__(self):
+        chirp_common.Memory.__init__(self)
+class IC9xDVMemory(chirp_common.DVMemory):
+    """See above dirty hack"""
+    _bank = None
+    _bank_index = 0
+    def __init__(self):
+        chirp_common.DVMemory.__init__(self)
+
+def _ic9x_parse_frames(buf):
+    frames = []
+
+    while "\xfe\xfe" in buf:
+        try:
+            start = buf.index("\xfe\xfe")
+            end = buf[start:].index("\xfd") + start + 1
+        except Exception, e:
+            print "No trailing bit"
+            break
+
+        framedata = buf[start:end]
+        buf = buf[end:]
+
+        try:
+            frame = IC92Frame()
+            frame.from_raw(framedata[2:-1])
+            frames.append(frame)
+        except errors.InvalidDataError, e:
+            print "Broken frame: %s" % e
+
+        #print "Parsed %i frames" % len(frames)
+
+    return frames
+
+def ic9x_send(pipe, buf):
+    """Send @buf to @pipe, wrapped in a header and trailer.  Attempt to read
+    any response frames, which are returned as a list"""
+
+    # Add header and trailer
+    realbuf = "\xfe\xfe" + buf + "\xfd"
+
+    #print "Sending:\n%s" % util.hexprint(realbuf)
+
+    pipe.write(realbuf)
+    pipe.flush()
+
+    data = ""
+    while True:
+        buf = pipe.read(4096)
+        if not buf:
+            break
+
+        data += buf
+
+    return _ic9x_parse_frames(data)
+
+class IC92Frame:
+    """IC9x frame base class"""
+    def get_vfo(self):
+        """Return the vfo number"""
+        return ord(self._map[0])
+
+    def set_vfo(self, vfo):
+        """Set the vfo number"""
+        self._map[0] = chr(vfo)
+
+    def from_raw(self, data):
+        """Construct the frame from raw data"""
+        self._map = MemoryMap(data)
+
+    def from_frame(self, frame):
+        """Construct the frame by copying another frame"""
+        self._map = MemoryMap(frame.get_raw())
+
+    def __init__(self, subcmd=0, flen=0, cmd=0x1A):
+        self._map = MemoryMap("\x00" * (4 + flen))
+        self._map[0] = "\x01\x80" + chr(cmd) + chr(subcmd)
+
+    def get_payload(self):
+        """Return the entire payload (sans header)"""
+        return self._map[4:]
+
+    def get_raw(self):
+        """Return the raw version of the frame"""
+        return self._map.get_packed()
+
+    def __str__(self):
+        string = "Frame VFO=%i (len = %i)\n" % (self.get_vfo(),
+                                                len(self.get_payload()))
+        string += util.hexprint(self.get_payload())
+        string += "\n"
+
+        return string
+
+    def send(self, pipe, verbose=False):
+        """Send the frame to the radio via @pipe"""
+        if verbose:
+            print "Sending:\n%s" % util.hexprint(self.get_raw())
+
+        response = ic9x_send(pipe, self.get_raw())
+
+        if len(response) == 0:
+            raise errors.InvalidDataError("No response from radio")
+
+        return response[0]
+
+    def __setitem__(self, start, value):
+        self._map[start+4] = value
+
+    def __getitem__(self, index):
+        return self._map[index+4]
+
+    def __getslice__(self, start, end):
+        return self._map[start+4:end+4]
+    
+class IC92GetBankFrame(IC92Frame):
+    """A frame for requesting bank information"""
+    def __init__(self):
+        IC92Frame.__init__(self, 0x09)
+
+    def send(self, pipe, verbose=False):
+        rframes = ic9x_send(pipe, self.get_raw())
+
+        if len(rframes) == 0:
+            raise errors.InvalidDataError("No response from radio")
+
+        return rframes
+
+class IC92BankFrame(IC92Frame):
+    """A frame for bank information"""
+    def __init__(self):
+        # 1 byte for identifier
+        # 8 bytes for name
+        IC92Frame.__init__(self, 0x0B, 9)
+
+    def get_name(self):
+        """Return the bank name"""
+        return self[1:]
+
+    def get_identifier(self):
+        """Return the letter for the bank (A-Z)"""
+        return self[0]
+
+    def set_name(self, name):
+        """Set the bank name"""
+        self[1] = name[:8].ljust(8)
+
+    def set_identifier(self, ident):
+        """Set the letter for the bank (A-Z)"""
+        self[0] = ident[0]
+
+class IC92MemClearFrame(IC92Frame):
+    """A frame for clearing (erasing) a memory"""
+    def __init__(self, loc):
+        # 2 bytes for location
+        # 1 byte for 0xFF
+        IC92Frame.__init__(self, 0x00, 4)
+
+        self[0] = struct.pack(">BHB", 1, int("%i" % loc, 16), 0xFF)
+
+class IC92MemGetFrame(IC92Frame):
+    """A frame for requesting a memory"""
+    def __init__(self, loc, iscall=False):
+        # 2 bytes for location
+        IC92Frame.__init__(self, 0x00, 3)
+
+        if iscall:
+            call = 2
+        else:
+            call = 1
+
+        self[0] = struct.pack(">BH", call, int("%i" % loc, 16))
+
+class IC92GetCallsignFrame(IC92Frame):
+    """A frame for getting callsign information"""
+    def __init__(self, calltype, number):
+        IC92Frame.__init__(self, calltype, 1, 0x1D)
+
+        self[0] = chr(number)
+
+class IC92CallsignFrame(IC92Frame):
+    """A frame to communicate callsign information"""
+    command = 0 # Invalid
+    width = 8
+
+    def __init__(self, number=0, callsign=""):
+        # 1 byte for index
+        # $width bytes for callsign
+        IC92Frame.__init__(self, self.command, self.width+1, 0x1D)
+
+        self[0] = chr(number) + callsign[:self.width].ljust(self.width)
+
+    def get_callsign(self):
+        """Return the actual callsign"""
+        return self[1:self.width+1].rstrip()
+
+class IC92YourCallsignFrame(IC92CallsignFrame):
+    """URCALL frame"""
+    command = 6 # Your
+
+class IC92RepeaterCallsignFrame(IC92CallsignFrame):
+    """RPTCALL frame"""
+    command = 7 # Repeater
+
+class IC92MyCallsignFrame(IC92CallsignFrame):
+    """MYCALL frame"""
+    command = 8 # My
+    width = 12 # 4 bytes for /STID
+
+MEMORY_FRAME_FORMAT = """
+struct {
+  u8 vfo;
+  bbcd number[2];
+  lbcd freq[5];
+  lbcd offset[4];
+  u8 unknown8;
+  bbcd rtone[2];
+  bbcd ctone[2];
+  bbcd dtcs[2];
+  u8 unknown9[2];
+  u8 unknown2:1,
+     mode:3,
+     tuning_step:4;
+  u8 unknown1:3,
+     tmode: 3,
+     duplex: 2;
+  u8 unknown5:4,
+     dtcs_polarity:2,
+     pskip:1,
+     skip:1;
+  char bank;
+  bbcd bank_index[1];
+  char name[8];
+  u8 unknown10;
+  u8 digital_code;
+  char rpt2call[8];
+  char rpt1call[8];
+  char urcall[8];
+} mem[1];
+"""
+
+class IC92MemoryFrame(IC92Frame):
+    """A frame for communicating memory information"""
+    def __init__(self):
+        IC92Frame.__init__(self, 0, DV_MEM_LEN)
+
+        # For good measure, here is a whole, valid memory block
+        # at 146.010 FM.  Since the 9x will complain if any bits
+        # are invalid, it's easiest to start with a known-good one
+        # since we don't set everything.
+        self[0] = \
+            "\x01\x00\x03\x00\x00\x01\x46\x01" + \
+            "\x00\x00\x60\x00\x00\x08\x85\x08" + \
+            "\x85\x00\x23\x22\x80\x06\x00\x00" + \
+            "\x00\x00\x20\x20\x20\x20\x20\x20" + \
+            "\x20\x20\x00\x00\x20\x20\x20\x20" + \
+            "\x20\x20\x20\x20\x4b\x44\x37\x52" + \
+            "\x45\x58\x20\x43\x43\x51\x43\x51" + \
+            "\x43\x51\x20\x20"
+
+    def set_vfo(self, vfo):
+        IC92Frame.set_vfo(self, vfo)
+        if vfo == 1:
+            self._map.truncate(MEM_LEN + 4)
+
+    def set_iscall(self, iscall):
+        """This frame refers to a call channel if @iscall is True"""
+        if iscall:
+            self[0] = 2
+        else:
+            self[0] = 1
+
+    def get_iscall(self):
+        """Return True if this frame refers to a call channel"""
+        return ord(self[0]) == 2
+
+    def set_memory(self, mem):
+        """Take Memory object @mem and configure the frame accordingly"""
+        if mem.number < 0:
+            self.set_iscall(True)
+            mem.number = abs(mem.number) - 1
+            print "Memory is %i (call %s)" % (mem.number, self.get_iscall())
+
+        _mem = bitwise.parse(MEMORY_FRAME_FORMAT, self).mem
+
+        _mem.number = mem.number
+
+        _mem.freq = mem.freq
+        _mem.offset = mem.offset
+        _mem.rtone = int(mem.rtone * 10)
+        _mem.ctone = int(mem.ctone * 10)
+        _mem.dtcs = int(mem.dtcs)
+        _mem.mode = MODES.index(mem.mode)
+        _mem.tuning_step = TUNING_STEPS.index(mem.tuning_step)
+        _mem.duplex = DUPLEX.index(mem.duplex)
+        _mem.tmode = TMODES.index(mem.tmode)
+        _mem.dtcs_polarity = DTCS_POL.index(mem.dtcs_polarity)
+
+        if mem._bank is not None:
+            _mem.bank = chr(ord("A") + mem._bank)
+            _mem.bank_index = mem._bank_index
+
+        _mem.skip = mem.skip == "S"
+        _mem.pskip = mem.skip == "P"
+
+        _mem.name = mem.name.ljust(8)[:8]
+
+        if mem.mode == "DV":
+            _mem.urcall = mem.dv_urcall.upper().ljust(8)[:8]
+            _mem.rpt1call = mem.dv_rpt1call.upper().ljust(8)[:8]
+            _mem.rpt2call = mem.dv_rpt2call.upper().ljust(8)[:8]
+            _mem.digital_code = mem.dv_code
+
+    def get_memory(self):
+        """Return a Memory object based on the contents of the frame"""
+        _mem = bitwise.parse(MEMORY_FRAME_FORMAT, self).mem
+
+        if MODES[_mem.mode] == "DV":
+            mem = IC9xDVMemory()
+        else:
+            mem = IC9xMemory()
+
+        mem.number = int(_mem.number)
+        if self.get_iscall():
+            mem.number = -1 - mem.number
+
+        mem.freq = int(_mem.freq)
+        mem.offset = int(_mem.offset)
+        mem.rtone = int(_mem.rtone) / 10.0
+        mem.ctone = int(_mem.ctone) / 10.0
+        mem.dtcs = int(_mem.dtcs)
+        mem.mode = MODES[int(_mem.mode)]
+        mem.tuning_step = TUNING_STEPS[int(_mem.tuning_step)]
+        mem.duplex = DUPLEX[int(_mem.duplex)]
+        mem.tmode = TMODES[int(_mem.tmode)]
+        mem.dtcs_polarity = DTCS_POL[int(_mem.dtcs_polarity)]
+
+        if int(_mem.bank) != 0:
+            mem._bank = ord(str(_mem.bank)) - ord("A")
+            mem._bank_index = int(_mem.bank_index)
+
+        if _mem.skip:
+            mem.skip = "S"
+        elif _mem.pskip:
+            mem.skip = "P"
+        else:
+            mem.skip = ""
+
+        mem.name = str(_mem.name).rstrip()
+
+        if mem.mode == "DV":
+            mem.dv_urcall = str(_mem.urcall).rstrip()
+            mem.dv_rpt1call = str(_mem.rpt1call).rstrip()
+            mem.dv_rpt2call = str(_mem.rpt2call).rstrip()
+            mem.dv_code = int(_mem.digital_code)
+
+        return mem
+
+def _send_magic_4800(pipe):
+    cmd = "\x01\x80\x19"
+    magic = ("\xFE" * 25) + cmd
+    for _i in [0, 1]:
+        resp = ic9x_send(pipe, magic)
+        if resp:
+            return resp[0].get_raw()[0] == "\x80"
+    return True
+
+def _send_magic_38400(pipe):
+    cmd = "\x01\x80\x19"
+    #rsp = "\x80\x01\x19"
+    magic = ("\xFE" * 400) + cmd
+    for _i in [0, 1]:
+        resp = ic9x_send(pipe, magic)
+        if resp:
+            return resp[0].get_raw()[0] == "\x80"
+    return False
+
+def send_magic(pipe):
+    """Send the magic incantation to wake up an ic9x radio"""
+    if pipe.getBaudrate() == 38400:
+        resp = _send_magic_38400(pipe)
+        if resp:
+            return
+        print "Switching from 38400 to 4800"
+        pipe.setBaudrate(4800)
+        resp = _send_magic_4800(pipe)
+        pipe.setBaudrate(38400)
+        if resp:
+            return
+        raise errors.RadioError("Radio not responding")
+    elif pipe.getBaudrate() == 4800:
+        resp = _send_magic_4800(pipe)
+        if resp:
+            return
+        print "Switching from 4800 to 38400"
+        pipe.setBaudrate(38400)
+        resp = _send_magic_38400(pipe)
+        if resp:
+            return
+        pipe.setBaudrate(4800)
+        raise errors.RadioError("Radio not responding")
+    else:
+        raise errors.InvalidDataError("Radio in unknown state (%i)" % \
+                                          pipe.getBaudrate())    
+
+def get_memory_frame(pipe, vfo, number):
+    """Get the memory frame for @vfo and @number via @pipe"""
+    if number < 0:
+        number = abs(number + 1)
+        call = True
+    else:
+        call = False
+
+    frame = IC92MemGetFrame(number, call)
+    frame.set_vfo(vfo)
+
+    return frame.send(pipe)
+
+def get_memory(pipe, vfo, number):
+    """Get a memory object for @vfo and @number via @pipe"""
+    rframe = get_memory_frame(pipe, vfo, number)
+
+    if len(rframe.get_payload()) < 1:
+        raise errors.InvalidMemoryLocation("No response from radio")
+
+    if rframe.get_payload()[3] == '\xff':
+        raise errors.InvalidMemoryLocation("Radio says location is empty")
+
+    mf = IC92MemoryFrame()
+    mf.from_frame(rframe)
+
+    return mf.get_memory()
+
+def set_memory(pipe, vfo, memory):
+    """Set memory @memory on @vfo via @pipe"""
+    frame = IC92MemoryFrame()
+    frame.set_memory(memory)
+    frame.set_vfo(vfo)
+
+    #print "Sending (%i):" % (len(frame.get_raw()))
+    #print util.hexprint(frame.get_raw())
+
+    rframe = frame.send(pipe)
+
+    if rframe.get_raw()[2] != "\xfb":
+        raise errors.InvalidDataError("Radio reported error:\n%s" %\
+                                          util.hexprint(rframe.get_payload()))
+
+def erase_memory(pipe, vfo, number):
+    """Erase memory @number on @vfo via @pipe"""
+    frame = IC92MemClearFrame(number)
+    frame.set_vfo(vfo)
+
+    rframe = frame.send(pipe)
+    if rframe.get_raw()[2] != "\xfb":
+        raise errors.InvalidDataError("Radio reported error")
+
+def get_banks(pipe, vfo):
+    """Get banks for @vfo via @pipe"""
+    frame = IC92GetBankFrame()
+    frame.set_vfo(vfo)
+
+    rframes = frame.send(pipe)
+
+    if vfo == 1:
+        base = 180
+    else:
+        base = 237
+
+    banks = []
+
+    for i in range(base, base+26):
+        bframe = IC92BankFrame()
+        bframe.from_frame(rframes[i])
+
+        banks.append(bframe.get_name().rstrip())
+    
+    return banks
+
+def set_banks(pipe, vfo, banks):
+    """Set banks for @vfo via @pipe"""
+    for i in range(0, 26):
+        bframe = IC92BankFrame()
+        bframe.set_vfo(vfo)
+        bframe.set_identifier(chr(i + ord("A")))
+        bframe.set_name(banks[i])
+
+        rframe = bframe.send(pipe)
+        if rframe.get_payload() != "\xfb":
+            raise errors.InvalidDataError("Radio reported error")
+
+def get_call(pipe, cstype, number):
+    """Get @cstype callsign @number via @pipe"""
+    cframe = IC92GetCallsignFrame(cstype.command, number)
+    cframe.set_vfo(2)
+    rframe = cframe.send(pipe)
+
+    cframe = IC92CallsignFrame()
+    cframe.from_frame(rframe)
+
+    return cframe.get_callsign()
+
+def set_call(pipe, cstype, number, call):
+    """Set @cstype @call at position @number via @pipe"""
+    cframe = cstype(number, call)
+    cframe.set_vfo(2)
+    rframe = cframe.send(pipe)
+
+    if rframe.get_payload() != "\xfb":
+        raise errors.RadioError("Radio reported error")
diff --git a/chirp/icf.py b/chirp/icf.py
new file mode 100644
index 0000000..8c00bab
--- /dev/null
+++ b/chirp/icf.py
@@ -0,0 +1,655 @@
+# 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 struct
+import re
+import time
+
+from chirp import chirp_common, errors, util, memmap
+from chirp.settings import RadioSetting, RadioSettingGroup, \
+    RadioSettingValueBoolean
+
+CMD_CLONE_OUT = 0xE2
+CMD_CLONE_IN  = 0xE3
+CMD_CLONE_DAT = 0xE4
+CMD_CLONE_END = 0xE5 
+
+SAVE_PIPE = None
+
+class IcfFrame:
+    """A single ICF communication frame"""
+    src = 0
+    dst = 0
+    cmd = 0
+
+    payload = ""
+
+    def __str__(self):
+        addrs = { 0xEE : "PC",
+                  0xEF : "Radio"}
+        cmds = {0xE0 : "ID",
+                0xE1 : "Model",
+                0xE2 : "Clone out",
+                0xE3 : "Clone in",
+                0xE4 : "Clone data",
+                0xE5 : "Clone end",
+                0xE6 : "Clone result"}
+
+        return "%s -> %s [%s]:\n%s" % (addrs[self.src], addrs[self.dst],
+                                       cmds[self.cmd],
+                                       util.hexprint(self.payload))
+
+    def __init__(self):
+        pass
+
+def parse_frame_generic(data):
+    """Parse an ICF frame of unknown type from the beginning of @data"""
+    frame = IcfFrame()
+
+    frame.src = ord(data[2])
+    frame.dst = ord(data[3])
+    frame.cmd = ord(data[4])
+
+    try:
+        end = data.index("\xFD")
+    except ValueError:
+        return None, data
+
+    frame.payload = data[5:end]
+
+    return frame, data[end+1:]
+
+class RadioStream:
+    """A class to make reading a stream of IcfFrames easier"""
+    def __init__(self, pipe):
+        self.pipe = pipe
+        self.data = ""
+
+    def _process_frames(self):
+        if not self.data.startswith("\xFE\xFE"):
+            raise errors.InvalidDataError("Out of sync with radio")
+        elif len(self.data) < 5:
+            return [] # Not enough data for a full frame
+
+        frames = []
+
+        while self.data:
+            try:
+                cmd = ord(self.data[4])
+            except IndexError:
+                break # Out of data
+
+            try:
+                frame, rest = parse_frame_generic(self.data)
+                if not frame:
+                    break
+                elif frame.src == 0xEE and frame.dst == 0xEF:
+                    # PC echo, ignore
+                    pass
+                else:
+                    frames.append(frame)
+
+                self.data = rest
+            except errors.InvalidDataError, e:
+                print "Failed to parse frame (cmd=%i): %s" % (cmd, e)
+                return []
+
+        return frames
+
+    def get_frames(self, nolimit=False):
+        """Read any pending frames from the stream"""
+        while True:
+            _data = self.pipe.read(64)
+            if not _data:
+                break
+            else:
+                self.data += _data
+
+            if not nolimit and len(self.data) > 128 and "\xFD" in self.data:
+                break # Give us a chance to do some status
+            if len(self.data) > 1024:
+                break # Avoid an endless loop of chewing garbage
+
+        if not self.data:
+            return []
+
+        return self._process_frames()
+
+def get_model_data(pipe, mdata="\x00\x00\x00\x00"):
+    """Query the radio connected to @pipe for its model data"""
+    send_clone_frame(pipe, 0xe0, mdata, raw=True)
+    
+    stream = RadioStream(pipe)
+    frames = stream.get_frames()
+
+    if len(frames) != 1:
+        raise errors.RadioError("Unexpected response from radio")
+
+    return frames[0].payload
+
+def get_clone_resp(pipe, length=None):
+    """Read the response to a clone frame"""
+    def exit_criteria(buf, length):
+        """Stop reading a clone response if we have enough data or encounter
+        the end of a frame"""
+        if length is None:
+            return buf.endswith("\xfd")
+        else:
+            return len(buf) == length
+
+    resp = ""
+    while not exit_criteria(resp, length):
+        resp += pipe.read(1)
+
+    return resp
+
+def send_clone_frame(pipe, cmd, data, raw=False, checksum=False):
+    """Send a clone frame with @cmd and @data to the radio attached
+    to @pipe"""
+    cs = 0
+
+    if raw:
+        hed = data
+    else:
+        hed = ""
+        for byte in data:
+            val = ord(byte)
+            hed += "%02X" % val
+            cs += val
+
+    if checksum:
+        cs = ((cs ^ 0xFFFF) + 1) & 0xFF
+        cs = "%02X" % cs
+    else:
+        cs = ""
+
+    frame = "\xfe\xfe\xee\xef%s%s%s\xfd" % (chr(cmd), hed, cs)
+
+    if SAVE_PIPE:
+        print "Saving data..."
+        SAVE_PIPE.write(frame)
+
+    #print "Sending:\n%s" % util.hexprint(frame)
+    #print "Sending:\n%s" % util.hexprint(hed[6:])
+    if cmd == 0xe4:
+        # Uncomment to avoid cloning to the radio
+        # return frame
+        pass
+    
+    pipe.write(frame)
+
+    return frame
+
+def process_bcd(bcddata):
+    """Convert BCD-encoded data to raw"""
+    data = ""
+    i = 0
+    while i < range(len(bcddata)) and i+1 < len(bcddata):
+        try:
+            val = int("%s%s" % (bcddata[i], bcddata[i+1]), 16)
+            i += 2
+            data += struct.pack("B", val)
+        except ValueError, e:
+            print "Failed to parse byte: %s" % e
+            break
+
+    return data
+
+def process_data_frame(frame, _mmap):
+    """Process a data frame, adding the payload to @_mmap"""
+    _data = process_bcd(frame.payload)
+    if len(_mmap) >= 0x10000:
+        saddr, = struct.unpack(">I", _data[0:4])
+        length, = struct.unpack("B", _data[4])
+        data = _data[5:5+length]
+    else:
+        saddr, = struct.unpack(">H", _data[0:2])
+        length, = struct.unpack("B", _data[2])
+        data = _data[3:3+length]
+
+    try:
+        _mmap[saddr] = data
+    except IndexError:
+        print "Error trying to set %i bytes at %05x (max %05x)" % \
+            (bytes, saddr, len(_mmap))
+    return saddr, saddr + length
+
+def start_hispeed_clone(radio, cmd):
+    """Send the magic incantation to the radio to go fast"""
+    buf = ("\xFE" * 20) + \
+        "\xEE\xEF\xE8" + \
+        radio.get_model() + \
+        "\x00\x00\x02\x01\xFD"
+    print "Starting HiSpeed:\n%s" % util.hexprint(buf)
+    radio.pipe.write(buf)
+    radio.pipe.flush()
+    resp = radio.pipe.read(128)
+    print "Response:\n%s" % util.hexprint(resp)
+
+    print "Switching to 38400 baud"
+    radio.pipe.setBaudrate(38400)
+
+    buf = ("\xFE" * 14) + \
+        "\xEE\xEF" + \
+        chr(cmd) + \
+        radio.get_model()[:3] + \
+        "\x00\xFD"
+    print "Starting HiSpeed Clone:\n%s" % util.hexprint(buf)
+    radio.pipe.write(buf)
+    radio.pipe.flush()
+
+def _clone_from_radio(radio):
+    md = get_model_data(radio.pipe)
+
+    if md[0:4] != radio.get_model():
+        print "This model: %s" % util.hexprint(md[0:4])
+        print "Supp model: %s" % util.hexprint(radio.get_model())
+        raise errors.RadioError("I can't talk to this model")
+
+    if radio.is_hispeed():
+        start_hispeed_clone(radio, CMD_CLONE_OUT)
+    else:
+        send_clone_frame(radio.pipe, CMD_CLONE_OUT, radio.get_model(), raw=True)
+
+    print "Sent clone frame"
+
+    stream = RadioStream(radio.pipe)
+
+    addr = 0
+    _mmap = memmap.MemoryMap(chr(0x00) * radio.get_memsize())
+    last_size = 0
+    while True:
+        frames = stream.get_frames()
+        if not frames:
+            break
+
+        for frame in frames:
+            if frame.cmd == CMD_CLONE_DAT:
+                src, dst = process_data_frame(frame, _mmap)
+                if last_size != (dst - src):
+                    print "ICF Size change from %i to %i at %04x" % (last_size,
+                                                                     dst - src,
+                                                                     src)
+                    last_size = dst - src
+                if addr != src:
+                    print "ICF GAP %04x - %04x" % (addr, src)
+                addr = dst
+            elif frame.cmd == CMD_CLONE_END:
+                print "End frame (%i):\n%s" % (len(frame.payload),
+                                               util.hexprint(frame.payload))
+                print "Last addr: %04x" % addr
+
+        if radio.status_fn:
+            status = chirp_common.Status()
+            status.msg = "Cloning from radio"
+            status.max = radio.get_memsize()
+            status.cur = addr
+            radio.status_fn(status)
+
+    return _mmap
+
+def clone_from_radio(radio):
+    """Do a full clone out of the radio's memory"""
+    try:
+        return _clone_from_radio(radio)
+    except Exception, e:
+        raise errors.RadioError("Failed to communicate with the radio: %s" % e)
+
+def send_mem_chunk(radio, start, stop, bs=32):
+    """Send a single chunk of the radio's memory from @start- at stop"""
+    _mmap = radio.get_mmap()
+
+    status = chirp_common.Status()
+    status.msg = "Cloning to radio"
+    status.max = radio.get_memsize()
+
+    for i in range(start, stop, bs):
+        if i + bs < stop:
+            size = bs
+        else:
+            size = stop - i
+
+        if radio.get_memsize() >= 0x10000:
+            chunk = struct.pack(">IB", i, size)
+        else:
+            chunk = struct.pack(">HB", i, size)
+        chunk += _mmap[i:i+size]
+
+        send_clone_frame(radio.pipe,
+                         CMD_CLONE_DAT,
+                         chunk,
+                         checksum=True)
+
+        if radio.status_fn:
+            status.cur = i+bs
+            radio.status_fn(status)
+
+    return True
+
+def _clone_to_radio(radio):
+    global SAVE_PIPE
+
+    # Uncomment to save out a capture of what we actually write to the radio
+    # SAVE_PIPE = file("pipe_capture.log", "w", 0)
+
+    md = get_model_data(radio.pipe)
+
+    if md[0:4] != radio.get_model():
+        raise errors.RadioError("I can't talk to this model")
+
+    # This mimics what the Icom software does, but isn't required and just
+    # takes longer
+    # md = get_model_data(radio.pipe, model=md[0:2]+"\x00\x00")
+    # md = get_model_data(radio.pipe, model=md[0:2]+"\x00\x00")
+
+    stream = RadioStream(radio.pipe)
+
+    if radio.is_hispeed():
+        start_hispeed_clone(radio, CMD_CLONE_IN)
+    else:
+        send_clone_frame(radio.pipe, CMD_CLONE_IN, radio.get_model(), raw=True)
+
+    frames = []
+
+    for start, stop, bs in radio.get_ranges():
+        if not send_mem_chunk(radio, start, stop, bs):
+            break
+        frames += stream.get_frames()
+
+    send_clone_frame(radio.pipe, CMD_CLONE_END, radio.get_endframe(), raw=True)
+
+    if SAVE_PIPE:
+        SAVE_PIPE.close()
+        SAVE_PIPE = None
+
+    for i in range(0, 10):
+        try:
+            frames += stream.get_frames(True)
+            result = frames[-1]
+        except IndexError:
+            print "Waiting for clone result..."
+            time.sleep(0.5)
+
+    if len(frames) == 0:
+        raise errors.RadioError("Did not get clone result from radio")
+
+    return result.payload[0] == '\x00'
+
+def clone_to_radio(radio):
+    """Initiate a full memory clone out to @radio"""
+    try:
+        return _clone_to_radio(radio)
+    except Exception, e:
+        raise errors.RadioError("Failed to communicate with the radio: %s" % e)
+
+def convert_model(mod_str):
+    """Convert an ICF-style model string into what we get from the radio"""
+    data = ""
+    for i in range(0, len(mod_str), 2):
+        hexval = mod_str[i:i+2]
+        intval = int(hexval, 16)
+        data += chr(intval)
+
+    return data
+
+def convert_data_line(line):
+    """Convert an ICF data line to raw memory format"""
+    if line.startswith("#"):
+        return ""
+
+    line = line.strip()
+
+    if len(line) == 38:
+        # Small memory (< 0x10000)
+        size = int(line[4:6], 16)
+        data = line[6:]
+    else:
+        # Large memory (>= 0x10000)
+        size = int(line[8:10], 16)
+        data = line[10:]
+
+    _mmap = ""
+    i = 0
+    while i < (size * 2):
+        try:
+            val = int("%s%s" % (data[i], data[i+1]), 16)
+            i += 2
+            _mmap += struct.pack("B", val)
+        except ValueError, e:
+            print "Failed to parse byte: %s" % e
+            break
+
+    return _mmap
+
+def read_file(filename):
+    """Read an ICF file and return the model string and memory data"""
+    f = file(filename)
+
+    mod_str = f.readline()
+    dat = f.readlines()
+    
+    model = convert_model(mod_str.strip())
+
+    _mmap = ""
+    for line in dat:
+        if not line.startswith("#"):
+            _mmap += convert_data_line(line)
+
+    return model, memmap.MemoryMap(_mmap)
+
+def is_9x_icf(filename):
+    """Returns True if @filename is an IC9x ICF file"""
+    f = file(filename)
+    mdata = f.read(8)
+    f.close()
+
+    return mdata in ["30660000", "28880000"]
+
+def is_icf_file(filename):
+    """Returns True if @filename is an ICF file"""
+    f = file(filename)
+    data = f.readline()
+    data += f.readline()
+    f.close()
+
+    data = data.replace("\n", "").replace("\r", "")
+
+    return bool(re.match("^[0-9]{8}#", data))
+
+class IcomBank(chirp_common.Bank):
+    """A bank that works for all Icom radios"""
+    # Integral index of the bank (not to be confused with per-memory
+    # bank indexes
+    index = 0
+
+class IcomNamedBank(IcomBank):
+    """A bank with an adjustable name"""
+    def set_name(self, name):
+        """Set the name of the bank"""
+        pass
+
+class IcomBankModel(chirp_common.BankModel):
+    """Icom radios all have pretty much the same simple bank model. This
+    central implementation can, with a few icom-specific radio interfaces
+    serve most/all of them"""
+
+    def get_num_banks(self):
+        return self._radio._num_banks
+
+    def get_banks(self):
+        banks = []
+        
+        for i in range(0, self._radio._num_banks):
+            index = chr(ord("A") + i)
+            bank = self._radio._bank_class(self, index, "BANK-%s" % index)
+            bank.index = i
+            banks.append(bank)
+        return banks
+
+    def add_memory_to_bank(self, memory, bank):
+        self._radio._set_bank(memory.number, bank.index)
+
+    def remove_memory_from_bank(self, memory, bank):
+        if self._radio._get_bank(memory.number) != bank.index:
+            raise Exception("Memory %i not in bank %s. Cannot remove." % \
+                                (memory.number, bank))
+
+        self._radio._set_bank(memory.number, None)
+
+    def get_bank_memories(self, bank):
+        memories = []
+        for i in range(*self._radio.get_features().memory_bounds):
+            if self._radio._get_bank(i) == bank.index:
+                memories.append(self._radio.get_memory(i))
+        return memories
+
+    def get_memory_banks(self, memory):
+        index = self._radio._get_bank(memory.number)
+        if index is None:
+            return []
+        else:
+            return [self.get_banks()[index]]
+    
+class IcomIndexedBankModel(IcomBankModel, chirp_common.BankIndexInterface):
+    """Generic bank model for Icom radios with indexed banks"""
+    def get_index_bounds(self):
+        return self._radio._bank_index_bounds
+
+    def get_memory_index(self, memory, bank):
+        return self._radio._get_bank_index(memory.number)
+
+    def set_memory_index(self, memory, bank, index):
+        if bank not in self.get_memory_banks(memory):
+            raise Exception("Memory %i is not in bank %s" % (memory.number,
+                                                             bank))
+
+        if index not in range(*self._radio._bank_index_bounds):
+            raise Exception("Invalid index")
+        self._radio._set_bank_index(memory.number, index)
+
+    def get_next_bank_index(self, bank):
+        indexes = []
+        for i in range(*self._radio.get_features().memory_bounds):
+            if self._radio._get_bank(i) == bank.index:
+                indexes.append(self._radio._get_bank_index(i))
+                
+        for i in range(0, 256):
+            if i not in indexes:
+                return i
+
+        raise errors.RadioError("Out of slots in this bank")
+        
+
+class IcomCloneModeRadio(chirp_common.CloneModeRadio):
+    """Base class for Icom clone-mode radios"""
+    VENDOR = "Icom"
+    BAUDRATE = 9600
+
+    _model = "\x00\x00\x00\x00"  # 4-byte model string
+    _endframe = ""               # Model-unique ending frame
+    _ranges = []                 # Ranges of the mmap to send to the radio
+    _num_banks = 10              # Most simple Icoms have 10 banks, A-J
+    _bank_index_bounds = (0, 99)
+    _bank_class = IcomBank
+    _can_hispeed = False
+
+    @classmethod
+    def is_hispeed(cls):
+        """Returns True if the radio supports hispeed cloning"""
+        return cls._can_hispeed
+
+    @classmethod
+    def get_model(cls):
+        """Returns the Icom model data for this radio"""
+        return cls._model
+
+    @classmethod
+    def get_endframe(cls):
+        """Returns the magic clone end frame for this radio"""
+        return cls._endframe
+
+    @classmethod
+    def get_ranges(cls):
+        """Returns the ranges this radio likes to have in a clone"""
+        return cls._ranges
+
+    def sync_in(self):
+        self._mmap = clone_from_radio(self)
+        self.process_mmap()
+
+    def sync_out(self):
+        clone_to_radio(self)
+
+    def get_bank_model(self):
+        rf = self.get_features()
+        if rf.has_bank:
+            if rf.has_bank_index:
+                return IcomIndexedBankModel(self)
+            else:
+                return IcomBankModel(self)
+        else:
+            return None
+
+    # Icom-specific bank routines
+    def _get_bank(self, loc):
+        """Get the integral bank index of memory @loc, or None"""
+        raise Exception("Not implemented")
+
+    def _set_bank(self, loc, index):
+        """Set the integral bank index of memory @loc to @index, or
+        no bank if None"""
+        raise Exception("Not implemented")
+
+    def get_settings(self):
+        return make_speed_switch_setting(self)
+
+    def set_settings(self, settings):
+        return honor_speed_switch_setting(self, settings)
+
+class IcomLiveRadio(chirp_common.LiveRadio):
+    """Base class for an Icom Live-mode radio"""
+    VENDOR = "Icom"
+    BAUD_RATE = 38400
+
+    _num_banks = 26              # Most live Icoms have 26 banks, A-Z
+    _bank_index_bounds = (0, 99)
+    _bank_class = IcomBank
+
+    def get_bank_model(self):
+        rf = self.get_features()
+        if rf.has_bank:
+            if rf.has_bank_index:
+                return IcomIndexedBankModel(self)
+            else:
+                return IcomBankModel(self)
+        else:
+            return None
+
+def make_speed_switch_setting(radio):
+    if not radio.__class__._can_hispeed:
+        return []
+    drvopts = RadioSettingGroup("drvopts", "Driver Options")
+    rs = RadioSetting("drv_clone_speed", "Use Hi-Speed Clone",
+                      RadioSettingValueBoolean(radio._can_hispeed))
+    drvopts.append(rs)
+    return drvopts
+
+def honor_speed_switch_setting(radio, settings):
+    for element in settings:
+        if element.get_name() == "drvopts":
+            return honor_speed_switch_setting(radio, settings)
+        if element.get_name() == "drv_clone_speed":
+            radio.__class__._can_hispeed = element.value.get_value()
+            return
diff --git a/chirp/icomciv.py b/chirp/icomciv.py
new file mode 100644
index 0000000..71af300
--- /dev/null
+++ b/chirp/icomciv.py
@@ -0,0 +1,413 @@
+
+import struct
+from chirp import chirp_common, icf, util, errors, bitwise, directory
+from chirp.memmap import MemoryMap
+
+DEBUG = True
+
+MEM_FORMAT = """
+bbcd number[2];
+u8   unknown1;
+lbcd freq[5];
+u8   unknown2:5,
+     mode:3;
+"""
+MEM_VFO_FORMAT = """
+u8   vfo;
+bbcd number[2];
+u8   unknown1;
+lbcd freq[5];
+u8   unknown2:5,
+     mode:3;
+u8   unknown1;
+u8   unknown2:2,
+     duplex:2,
+     unknown3:1,
+     tmode:3;
+u8   unknown4;
+bbcd rtone[2];
+u8   unknown5;
+bbcd ctone[2];
+u8   unknown6[2];
+bbcd dtcs;
+u8   unknown[17];
+char name[9];
+"""
+mem_duptone_format = """
+bbcd number[2];
+u8   unknown1;
+lbcd freq[5];
+u8   unknown2:5,
+     mode:3;
+u8   unknown1;
+u8   unknown2:2,
+     duplex:2,
+     unknown3:1,
+     tmode:3;
+u8   unknown4;
+bbcd rtone[2];
+u8   unknown5;
+bbcd ctone[2];
+u8   unknown6[2];
+bbcd dtcs;
+u8   unknown[11];
+char name[9];
+"""
+
+class Frame:
+    """Base class for an ICF frame"""
+    _cmd = 0x00
+    _sub = 0x00
+
+    def __init__(self):
+        self._data = ""
+
+    def set_command(self, cmd, sub):
+        """Set the command number (and optional subcommand)"""
+        self._cmd = cmd
+        self._sub = sub
+
+    def get_data(self):
+        """Return the data payload"""
+        return self._data
+
+    def set_data(self, data):
+        """Set the data payload"""
+        self._data = data
+
+    def send(self, src, dst, serial, willecho=True):
+        """Send the frame over @serial, using @src and @dst addresses"""
+        raw = struct.pack("BBBBBB", 0xFE, 0xFE, src, dst, self._cmd, self._sub)
+        raw += str(self._data) + chr(0xFD)
+
+        if DEBUG:
+            print "%02x -> %02x (%i):\n%s" % (src, dst,
+                                              len(raw), util.hexprint(raw))
+
+        serial.write(raw)
+        if willecho:
+            echo = serial.read(len(raw))
+            if echo != raw and echo:
+                print "Echo differed (%i/%i)" % (len(raw), len(echo))
+                print util.hexprint(raw)
+                print util.hexprint(echo)
+
+    def read(self, serial):
+        """Read the frame from @serial"""
+        data = ""
+        while not data.endswith(chr(0xFD)):
+            char = serial.read(1)
+            if not char:
+                print "Read %i bytes total" % len(data)
+                raise errors.RadioError("Timeout")
+            data += char
+
+        if data == chr(0xFD):
+            raise errors.RadioError("Radio reported error")
+
+        src, dst = struct.unpack("BB", data[2:4])
+        if DEBUG:
+            print "%02x <- %02x:\n%s" % (src, dst, util.hexprint(data))
+
+        self._cmd = ord(data[4])
+        self._sub = ord(data[5])
+        self._data = data[6:-1]
+
+        return src, dst
+
+    def get_obj(self):
+        raise errors.RadioError("Generic frame has no structure")
+
+class MemFrame(Frame):
+    """A memory frame"""
+    _cmd = 0x1A
+    _sub = 0x00
+    _loc = 0
+
+    def set_location(self, loc):
+        """Set the memory location number"""
+        self._loc = loc
+        self._data = struct.pack(">H", int("%04i" % loc, 16))
+
+    def make_empty(self):
+        """Mark as empty so the radio will erase the memory"""
+        self._data = struct.pack(">HB", int("%04i" % self._loc, 16), 0xFF)
+
+    def is_empty(self):
+        """Return True if memory is marked as empty"""
+        return len(self._data) < 5
+
+    def get_obj(self):
+        """Return a bitwise parsed object"""
+        self._data = MemoryMap(str(self._data)) # Make sure we're assignable
+        return bitwise.parse(MEM_FORMAT, self._data)
+
+    def initialize(self):
+        """Initialize to sane values"""
+        self._data = MemoryMap("".join(["\x00"] * (self.get_obj().size() / 8)))
+
+class MultiVFOMemFrame(MemFrame):
+    """A memory frame for radios with multiple VFOs"""
+    def set_location(self, loc, vfo=1):
+        self._loc = loc
+        self._data = struct.pack(">BH", vfo, int("%04i" % loc, 16))
+
+    def get_obj(self):
+        self._data = MemoryMap(str(self._data)) # Make sure we're assignable
+        return bitwise.parse(MEM_VFO_FORMAT, self._data)
+
+class DupToneMemFrame(MemFrame):
+    def get_obj(self):
+        self._data = MemoryMap(str(self._data))
+        return bitwise.parse(mem_duptone_format, self._data)
+
+class IcomCIVRadio(icf.IcomLiveRadio):
+    """Base class for ICOM CIV-based radios"""
+    BAUD_RATE = 19200
+    MODEL = "CIV Radio"
+    _model = "\x00"
+    _template = 0
+
+    def _send_frame(self, frame):
+        return frame.send(ord(self._model), 0xE0, self.pipe,
+                          willecho=self._willecho)
+
+    def _recv_frame(self, frame=None):
+        if not frame:
+            frame = Frame()
+        frame.read(self.pipe)
+        return frame
+
+    def _initialize(self):
+        pass
+
+    def _detect_echo(self):
+        echo_test = "\xfe\xfe\xe0\xe0\xfa\xfd"
+        self.pipe.write(echo_test)
+        resp = self.pipe.read(6)
+        print "Echo:\n%s" % util.hexprint(resp)
+        return resp == echo_test
+
+    def __init__(self, *args, **kwargs):
+        icf.IcomLiveRadio.__init__(self, *args, **kwargs)
+
+        self._classes = {
+            "mem" : MemFrame,
+            }
+
+        if self.pipe:
+            self._willecho = self._detect_echo()
+            print "Interface echo: %s" % self._willecho
+            self.pipe.setTimeout(1)
+
+        #f = Frame()
+        #f.set_command(0x19, 0x00)
+        #self._send_frame(f)
+        #
+        #res = f.read(self.pipe)
+        #if res:
+        #    print "Result: %x->%x (%i)" % (res[0], res[1], len(f.get_data()))
+        #    print util.hexprint(f.get_data())
+        #
+        #self._id = f.get_data()[0]
+        self._rf = chirp_common.RadioFeatures()
+
+        self._initialize()
+
+    def get_features(self):
+        return self._rf
+
+    def _get_template_memory(self):
+        f = self._classes["mem"]()
+        f.set_location(self._template)
+        self._send_frame(f)
+        f.read(self.pipe)
+        return f
+
+    def get_raw_memory(self, number):
+        f = self._classes["mem"]()
+        f.set_location(number)
+        self._send_frame(f)
+        f.read(self.pipe)
+        return repr(f.get_obj())
+
+    def get_memory(self, number):
+        print "Getting %i" % number
+        f = self._classes["mem"]()
+        f.set_location(number)
+        self._send_frame(f)
+
+        mem = chirp_common.Memory()
+        mem.number = number
+
+        f = self._recv_frame(f)
+        if len(f.get_data()) == 0:
+            raise errors.RadioError("Radio reported error")
+        if f.get_data() and f.get_data()[-1] == "\xFF":
+            mem.empty = True
+            return mem
+
+        memobj = f.get_obj()
+        print repr(memobj)
+
+        mem.freq = int(memobj.freq)
+        mem.mode = self._rf.valid_modes[memobj.mode]
+
+        if self._rf.has_name:
+            mem.name = str(memobj.name).rstrip()
+
+        if self._rf.valid_tmodes:
+            mem.tmode = self._rf.valid_tmodes[memobj.tmode]
+
+        if self._rf.has_dtcs:
+            # FIXME
+            mem.dtcs = bitwise.bcd_to_int([memobj.dtcs])
+
+        if "Tone" in self._rf.valid_tmodes:
+            mem.rtone = int(memobj.rtone) / 10.0
+
+        if "TSQL" in self._rf.valid_tmodes and self._rf.has_ctone:
+            mem.ctone = int(memobj.ctone) / 10.0
+
+        if self._rf.valid_duplexes:
+            mem.duplex = self._rf.valid_duplexes[memobj.duplex]
+
+        return mem
+
+    def set_memory(self, mem):
+        f = self._get_template_memory()
+        if mem.empty:
+            f.set_location(mem.number)
+            f.make_empty()
+            self._send_frame(f)
+            return
+
+        #f.set_data(MemoryMap(self.get_raw_memory(mem.number)))
+        #f.initialize()
+
+        memobj = f.get_obj()
+        memobj.number = mem.number
+        memobj.freq = int(mem.freq)
+        memobj.mode = self._rf.valid_modes.index(mem.mode)
+        if self._rf.has_name:
+            memobj.name = mem.name.ljust(9)[:9]
+
+        if self._rf.valid_tmodes:
+            memobj.tmode = self._rf.valid_tmodes.index(mem.tmode)
+
+        if self._rf.valid_duplexes:
+            memobj.duplex = self._rf.valid_duplexes.index(mem.duplex)
+
+        if self._rf.has_ctone:
+            memobj.ctone = int(mem.ctone * 10)
+            memobj.rtone = int(mem.rtone * 10)
+
+        print repr(memobj)
+        self._send_frame(f)
+
+        f = self._recv_frame()
+        print "Result:\n%s" % util.hexprint(f.get_data())
+
+ at directory.register
+class Icom7200Radio(IcomCIVRadio):
+    """Icom IC-7200"""
+    MODEL = "7200"
+    _model = "\x76"
+    _template = 201
+
+    def _initialize(self):
+        self._rf.has_bank = False
+        self._rf.has_dtcs_polarity = False
+        self._rf.has_dtcs = False
+        self._rf.has_ctone = False
+        self._rf.has_offset = False
+        self._rf.has_name = False
+        self._rf.valid_modes = ["LSB", "USB", "AM", "CW", "RTTY"]
+        self._rf.valid_tmodes = []
+        self._rf.valid_duplexes = []
+        self._rf.valid_bands = [(1800000, 59000000)]
+        self._rf.valid_tuning_steps = []
+        self._rf.valid_skips = []
+        self._rf.memory_bounds = (1, 200)
+
+ at directory.register
+class Icom7000Radio(IcomCIVRadio):
+    """Icom IC-7000"""
+    MODEL = "7000"
+    _model = "\x70"
+    _template = 102
+
+    def _initialize(self):
+        self._classes["mem"] = MultiVFOMemFrame
+        self._rf.has_bank = False
+        self._rf.has_dtcs_polarity = True
+        self._rf.has_dtcs = True
+        self._rf.has_ctone = True
+        self._rf.has_offset = False
+        self._rf.has_name = True
+        self._rf.has_tuning_step = False
+        self._rf.valid_modes = ["LSB", "USB", "AM", "CW", "RTTY", "FM"]
+        self._rf.valid_tmodes = ["", "Tone", "TSQL", "DTCS"]
+        self._rf.valid_duplexes = ["", "-", "+"]
+        self._rf.valid_bands = [(30000, 199999999), (400000000, 470000000)]
+        self._rf.valid_tuning_steps = []
+        self._rf.valid_skips = []
+        self._rf.valid_name_length = 9
+        self._rf.valid_characters = chirp_common.CHARSET_ASCII
+        self._rf.memory_bounds = (1, 99)
+
+ at directory.register
+class Icom746Radio(IcomCIVRadio):
+    """Icom IC-746"""
+    MODEL = "746"
+    BAUD_RATE = 9600
+    _model = "\x56"
+    _template = 102
+
+    def _initialize(self):
+        self._classes["mem"] = DupToneMemFrame
+        self._rf.has_bank = False
+        self._rf.has_dtcs_polarity = False
+        self._rf.has_dtcs = False
+        self._rf.has_ctone = True
+        self._rf.has_offset = False
+        self._rf.has_name = True
+        self._rf.has_tuning_step = False
+        self._rf.valid_modes = ["LSB", "USB", "AM", "CW", "RTTY", "FM"]
+        self._rf.valid_tmodes = ["", "Tone", "TSQL"]
+        self._rf.valid_duplexes = ["", "-", "+"]
+        self._rf.valid_bands = [(30000, 199999999)]
+        self._rf.valid_tuning_steps = []
+        self._rf.valid_skips = []
+        self._rf.valid_name_length = 9
+        self._rf.valid_characters = chirp_common.CHARSET_ASCII
+        self._rf.memory_bounds = (1, 99)
+
+CIV_MODELS = {
+    (0x76, 0xE0) : Icom7200Radio,
+    (0x70, 0xE0) : Icom7000Radio,
+    (0x46, 0xE0) : Icom746Radio,
+}
+
+def probe_model(ser):
+    """Probe the radio attatched to @ser for its model"""
+    f = Frame()
+    f.set_command(0x19, 0x00)
+
+    for model, controller in CIV_MODELS.keys():
+        f.send(model, controller, ser)
+        try:
+            f.read(ser)
+        except errors.RadioError:
+            continue
+
+        if len(f.get_data()) == 1:
+            model = ord(f.get_data()[0])
+            return CIV_MODELS[(model, controller)]
+
+        if f.get_data():
+            print "Got data, but not 1 byte:"
+            print util.hexprint(f.get_data())
+            raise errors.RadioError("Unknown response")
+
+    raise errors.RadioError("Unsupported model")
diff --git a/chirp/icq7.py b/chirp/icq7.py
new file mode 100644
index 0000000..a6e2e95
--- /dev/null
+++ b/chirp/icq7.py
@@ -0,0 +1,150 @@
+# Copyright 2010 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/>.
+
+from chirp import chirp_common, icf, directory
+from chirp import bitwise
+from chirp.chirp_common import to_GHz, from_GHz
+
+MEM_FORMAT = """
+struct {
+  bbcd freq[3];
+  u8  fractional:1,
+      unknown:7;
+  bbcd offset[2];
+  u16 ctone:6
+      rtone:6,
+      tune_step:4;
+} memory[200];
+
+#seekto 0x0690;
+struct {
+  u8 tmode:2,
+     duplex:2,
+     skip:1,
+     pskip:1,
+     mode:2;
+} flags[200];
+
+#seekto 0x0690;
+u8 flags_whole[200];
+"""
+
+TMODES = ["", "", "Tone", "TSQL", "TSQL"] # last one is pocket beep
+DUPLEX = ["", "", "-", "+"]
+MODES  = ["FM", "WFM", "AM", "Auto"]
+STEPS =  [5.0, 6.25, 10.0, 12.5, 15.0, 20.0, 25.0, 30.0, 50.0, 100.0]
+
+ at directory.register
+class ICQ7Radio(icf.IcomCloneModeRadio):
+    """Icom IC-Q7A"""
+    VENDOR = "Icom"
+    MODEL = "IC-Q7A"
+
+    _model = "\x19\x95\x00\x01"
+    _memsize = 0x7C0
+    _endframe = "Icom Inc\x2e"
+
+    _ranges = [(0x0000, 0x07C0, 16)]
+
+    def get_features(self):
+        rf = chirp_common.RadioFeatures()
+        rf.memory_bounds = (0, 199)
+        rf.valid_modes = list(MODES)
+        rf.valid_tmodes = list(TMODES)
+        rf.valid_duplexes = list(DUPLEX)
+        rf.valid_tuning_steps = list(STEPS)
+        rf.valid_bands = [(  1000000,  823995000),
+                          (849000000,  868995000),
+                          (894000000, 1309995000)]
+        rf.valid_skips = ["", "S", "P"]
+        rf.has_dtcs = False
+        rf.has_dtcs_polarity = False
+        rf.has_bank = False
+        rf.has_name = False
+        return rf
+
+    def process_mmap(self):
+        self._memobj = bitwise.parse(MEM_FORMAT, self._mmap)
+
+    def get_raw_memory(self, number):
+        return (repr(self._memobj.memory[number]) +
+                repr(self._memobj.flags[number]))
+
+    def get_memory(self, number):
+        _mem = self._memobj.memory[number]
+        _flag = self._memobj.flags[number]
+
+        mem = chirp_common.Memory()
+        mem.number = number
+        if self._memobj.flags_whole[number] == 0xFF:
+            mem.empty = True
+            return mem
+
+        mem.freq = int(_mem.freq) * 1000
+        if _mem.fractional:
+            mem.freq = chirp_common.fix_rounded_step(mem.freq)
+        mem.offset = int(_mem.offset) * 1000
+        try:
+            mem.rtone = chirp_common.TONES[_mem.rtone]
+        except IndexError:
+            mem.rtone = 88.5
+        try:
+            mem.ctone = chirp_common.TONES[_mem.ctone]
+        except IndexError:
+            mem.ctone = 88.5
+        try:
+            mem.tuning_step = STEPS[_mem.tune_step]
+        except IndexError:
+            print "Invalid tune step index %i" % _mem.tune_step
+        mem.tmode = TMODES[_flag.tmode]
+        mem.duplex = DUPLEX[_flag.duplex]
+        if mem.freq < 30000000:
+            mem.mode = "AM"
+        else:
+            mem.mode = MODES[_flag.mode]
+        if _flag.pskip:
+            mem.skip = "P"
+        elif _flag.skip:
+            mem.skip = "S"
+
+        return mem
+
+    def set_memory(self, mem):
+        _mem = self._memobj.memory[mem.number]
+        _flag = self._memobj.flags[mem.number]
+        
+        if mem.empty:
+            self._memobj.flags_whole[mem.number] = 0xFF
+            return
+            
+        _mem.set_raw("\x00" * 8)    
+
+        if mem.freq > to_GHz(1):
+            _mem.freq = (mem.freq / 1000) - to_GHz(1)
+            upper = from_GHz(mem.freq) << 4
+            _mem.freq[0].clr_bits(0xF0)
+            _mem.freq[0].set_bits(upper)
+        else:
+            _mem.freq = mem.freq / 1000
+        _mem.fractional = chirp_common.is_fractional_step(mem.freq)
+        _mem.offset = mem.offset / 1000
+        _mem.rtone = chirp_common.TONES.index(mem.rtone)
+        _mem.ctone = chirp_common.TONES.index(mem.ctone)
+        _mem.tune_step = STEPS.index(mem.tuning_step)
+        _flag.tmode = TMODES.index(mem.tmode)
+        _flag.duplex = DUPLEX.index(mem.duplex)
+        _flag.mode = MODES.index(mem.mode)
+        _flag.skip = mem.skip == "S" and 1 or 0
+        _flag.pskip = mem.skip == "P" and 1 or 0
diff --git a/chirp/ict70.py b/chirp/ict70.py
new file mode 100644
index 0000000..391b129
--- /dev/null
+++ b/chirp/ict70.py
@@ -0,0 +1,222 @@
+# Copyright 2011 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/>.
+
+from chirp import chirp_common, icf, directory
+from chirp import bitwise
+
+MEM_FORMAT = """
+struct {
+  u24 freq;
+  ul16 offset;
+  char name[6];
+  u8 unknown2:2,
+     rtone:6;
+  u8 unknown3:2,
+     ctone:6;
+  u8 unknown4:1,
+     dtcs:7;
+  u8 tuning_step:4,
+     narrow:1,
+     unknown5:1,
+     duplex:2;
+  u8 unknown6:1,
+     power:2,
+     dtcs_polarity:2,
+     tmode:3;
+} memory[300];
+
+#seekto 0x12E0;
+u8 used[38];
+
+#seekto 0x1306;
+u8 skips[38];
+
+#seekto 0x132C;
+u8 pskips[38];
+
+#seekto 0x1360;
+struct {
+  u8 bank;
+  u8 index;
+} banks[300];
+
+#seekto 0x16D0;
+struct {
+  char name[6];
+} bank_names[26];
+
+"""
+
+TMODES = ["", "Tone", "TSQL", "TSQL", "DTCS", "DTCS"]
+DUPLEX = ["", "-", "+"]
+DTCS_POLARITY = ["NN", "NR", "RN", "RR"]
+TUNING_STEPS = [5.0, 5.0, 5.0, 5.0, 10.0, 12.5, 15.0, 20.0, 25.0, 30.0,
+                50.0, 100.0, 125.0, 200.0]
+POWER_LEVELS = [chirp_common.PowerLevel("High", watts=5),
+                chirp_common.PowerLevel("Low", watts=0.5),
+                chirp_common.PowerLevel("Mid", watts=1.0),
+                ]
+
+class ICT70Bank(icf.IcomBank):
+    """ICT70 bank"""
+    def get_name(self):
+        _bank = self._model._radio._memobj.bank_names[self.index]
+        return str(_bank.name).rstrip()
+
+    def set_name(self, name):
+        _bank = self._model._radio._memobj.bank_names[self.index]
+        _bank.name = name.ljust(8)[:8]
+
+ at directory.register
+class ICT70Radio(icf.IcomCloneModeRadio):
+    """Icom IC-T70"""
+    VENDOR = "Icom"
+    MODEL = "IC-T70"
+
+    _model = "\x32\x53\x00\x01"
+    _memsize = 0x19E0
+    _endframe = "Icom Inc\x2eCF"
+
+    _ranges = [(0x0000, 0x19E0, 32)]
+
+    _num_banks = 26
+    _bank_class = ICT70Bank
+    
+    def _get_bank(self, loc):
+        _bank = self._memobj.banks[loc]
+        if _bank.bank != 0xFF:
+            return _bank.bank
+        else:
+            return None
+
+    def _set_bank(self, loc, bank):
+        _bank = self._memobj.banks[loc]
+        if bank is None:
+            _bank.bank = 0xFF
+        else:
+            _bank.bank = bank
+
+    def _get_bank_index(self, loc):
+        _bank = self._memobj.banks[loc]
+        return _bank.index
+
+    def _set_bank_index(self, loc, index):
+        _bank = self._memobj.banks[loc]
+        _bank.index = index
+   
+    def get_features(self):
+        rf = chirp_common.RadioFeatures()
+        rf.memory_bounds = (0, 299)
+        rf.valid_tmodes = TMODES
+        rf.valid_duplexes = DUPLEX
+        rf.valid_power_levels = POWER_LEVELS
+        rf.valid_modes = ["FM", "NFM"]
+        rf.valid_bands = [(136000000, 174000000), (400000000, 479000000)]
+        rf.valid_skips = ["", "S", "P"]
+        rf.valid_tuning_steps = TUNING_STEPS
+        rf.valid_name_length = 6
+        rf.has_ctone = True
+        rf.has_bank = True
+        rf.has_bank_index = True
+        rf.has_bank_names = True
+        return rf
+
+    def process_mmap(self):
+        self._memobj = bitwise.parse(MEM_FORMAT, self._mmap)
+
+    def get_raw_memory(self, number):
+        return repr(self._memobj.memory[number])
+
+    def get_memory(self, number):
+        bit = 1 << (number % 8)
+        byte = int(number / 8)
+
+        _mem = self._memobj.memory[number]
+        _usd = self._memobj.used[byte]
+        _skp = self._memobj.skips[byte]
+        _psk = self._memobj.pskips[byte]
+
+        mem = chirp_common.Memory()
+        mem.number = number
+
+        if _usd & bit:
+            mem.empty = True
+            return mem
+
+        if _mem.freq & 0x800000:
+            mem.freq = (_mem.freq & ~0x800000) * 6250
+        else:
+            mem.freq = _mem.freq * 5000
+        mem.offset = _mem.offset * 5000
+        mem.name = str(_mem.name).rstrip()
+        mem.rtone = chirp_common.TONES[_mem.rtone]
+        mem.ctone = chirp_common.TONES[_mem.ctone]
+        mem.dtcs = chirp_common.DTCS_CODES[_mem.dtcs]
+        mem.tuning_step = TUNING_STEPS[_mem.tuning_step]
+        mem.mode = _mem.narrow and "NFM" or "FM"
+        mem.duplex = DUPLEX[_mem.duplex]
+        mem.power = POWER_LEVELS[_mem.power]
+        mem.dtcs_polarity = DTCS_POLARITY[_mem.dtcs_polarity]
+        mem.tmode = TMODES[_mem.tmode]
+        mem.skip = (_psk & bit and "P") or (_skp & bit and "S") or ""
+        
+        return mem
+
+    def set_memory(self, mem):
+        bit = 1 << (mem.number % 8)
+        byte = int(mem.number / 8)
+
+        _mem = self._memobj.memory[mem.number]
+        _usd = self._memobj.used[byte]
+        _skp = self._memobj.skips[byte]
+        _psk = self._memobj.pskips[byte]
+
+        _mem.set_raw("\x00" * (_mem.size() / 8))
+
+        if mem.empty:
+            _usd |= bit
+            return
+
+        _usd &= ~bit
+
+        if chirp_common.is_12_5(mem.freq):
+            _mem.freq = (mem.freq / 6250) | 0x800000
+        else:
+            _mem.freq = mem.freq / 5000
+        _mem.offset = mem.offset / 5000
+        _mem.name = mem.name.ljust(6)[:6]
+        _mem.rtone = chirp_common.TONES.index(mem.rtone)
+        _mem.ctone = chirp_common.TONES.index(mem.ctone)
+        _mem.dtcs = chirp_common.DTCS_CODES.index(mem.dtcs)
+        _mem.tuning_step = TUNING_STEPS.index(mem.tuning_step)
+        _mem.narrow = mem.mode == "NFM"
+        _mem.duplex = DUPLEX.index(mem.duplex)
+        _mem.dtcs_polarity = DTCS_POLARITY.index(mem.dtcs_polarity)
+        _mem.tmode = TMODES.index(mem.tmode)
+        if mem.power:
+            _mem.power = POWER_LEVELS.index(mem.power)
+        else:
+            _mem.power = 0
+
+        if mem.skip == "S":
+            _skp |= bit
+            _psk &= ~bit
+        elif mem.skip == "P":
+            _skp &= ~bit
+            _psk |= bit
+        else:
+            _skp &= ~bit
+            _psk &= ~bit
+        
diff --git a/chirp/ict7h.py b/chirp/ict7h.py
new file mode 100644
index 0000000..4781652
--- /dev/null
+++ b/chirp/ict7h.py
@@ -0,0 +1,122 @@
+# Copyright 2012 Eric Allen <ericpallen at gmail.com>
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program.  If not, see <http://www.gnu.org/licenses/>.
+
+from chirp import chirp_common, icf, directory
+from chirp import bitwise
+
+mem_format = """
+struct {
+  bbcd freq[2];
+  u8  lastfreq:4,
+      fraction:4;
+  bbcd offset[2];
+  u8  unknown;
+  u8  rtone;
+  u8  ctone;
+} memory[60];
+
+#seekto 0x0270;
+struct {
+  u8 empty:1,
+     tmode:2,
+     duplex:2,
+     unknown3:1,
+     skip:1,
+     unknown4:1;
+} flags[60];
+"""
+
+TMODES = ["", "", "Tone", "TSQL", "TSQL"]  # last one is pocket beep
+DUPLEX = ["", "", "-", "+"]
+MODES = ["FM"]
+STEPS = [5.0, 10.0, 12.5, 15.0, 20.0, 25.0, 30.0, 50.0]
+
+
+ at directory.register
+class ICT7HRadio(icf.IcomCloneModeRadio):
+    VENDOR = "Icom"
+    MODEL = "IC-T7H"
+
+    _model = "\x18\x10\x00\x01"
+    _memsize = 0x03B0
+    _endframe = "Icom Inc\x2e"
+
+    _ranges = [(0x0000, _memsize, 16)]
+
+    def get_features(self):
+        rf = chirp_common.RadioFeatures()
+        rf.memory_bounds = (0, 59)
+        rf.valid_modes = list(MODES)
+        rf.valid_tmodes = list(TMODES)
+        rf.valid_duplexes = list(DUPLEX)
+        rf.valid_tuning_steps = list(STEPS)
+        rf.valid_bands = [(118000000, 174000000),
+                          (400000000, 470000000)]
+        rf.valid_skips = ["", "S"]
+        rf.has_dtcs = False
+        rf.has_dtcs_polarity = False
+        rf.has_bank = False
+        rf.has_name = False
+        rf.has_tuning_step = False
+        return rf
+
+    def process_mmap(self):
+        self._memobj = bitwise.parse(mem_format, self._mmap)
+
+    def get_raw_memory(self, number):
+        return repr(self._memobj.memory[number])
+
+    def get_memory(self, number):
+        _mem = self._memobj.memory[number]
+        _flag = self._memobj.flags[number]
+
+        mem = chirp_common.Memory()
+        mem.number = number
+
+        mem.empty = _flag.empty == 1 and True or False
+
+        mem.freq = int(_mem.freq) * 100000
+        mem.freq += _mem.lastfreq * 10000
+        mem.freq += int((_mem.fraction / 2.0) * 1000)
+
+        mem.offset = int(_mem.offset) * 10000
+        mem.rtone = chirp_common.TONES[_mem.rtone - 1]
+        mem.ctone = chirp_common.TONES[_mem.ctone - 1]
+        mem.tmode = TMODES[_flag.tmode]
+        mem.duplex = DUPLEX[_flag.duplex]
+        mem.mode = "FM"
+        if _flag.skip:
+            mem.skip = "S"
+
+        return mem
+
+    def set_memory(self, mem):
+        _mem = self._memobj.memory[mem.number]
+        _flag = self._memobj.flags[mem.number]
+
+        _mem.freq = int(mem.freq / 100000)
+        topfreq = int(mem.freq / 100000) * 100000
+        lastfreq = int((mem.freq - topfreq) / 10000)
+        _mem.lastfreq = lastfreq
+        midfreq = (mem.freq - topfreq - lastfreq * 10000)
+        _mem.fraction = midfreq / 500
+
+        _mem.offset = mem.offset / 10000
+        _mem.rtone = chirp_common.TONES.index(mem.rtone) + 1
+        _mem.ctone = chirp_common.TONES.index(mem.ctone) + 1
+        _flag.tmode = TMODES.index(mem.tmode)
+        _flag.duplex = DUPLEX.index(mem.duplex)
+        _flag.skip = mem.skip == "S" and 1 or 0
+        _flag.empty = mem.empty and 1 or 0
diff --git a/chirp/ict8.py b/chirp/ict8.py
new file mode 100644
index 0000000..c77d8f3
--- /dev/null
+++ b/chirp/ict8.py
@@ -0,0 +1,119 @@
+# Copyright 2012 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/>.
+
+from chirp import chirp_common, icf, util, directory
+from chirp import bitwise
+
+mem_format = """
+struct memory {
+  bbcd freq[4];
+  bbcd offset[2];
+  u8 rtone;
+  u8 ctone;
+};
+
+struct flags {
+  u8 empty:1,
+     skip:1,
+     tmode:2,
+     duplex:2,
+     unknown2:2;
+};
+
+struct memory memory[100];
+
+#seekto 0x0600;
+struct flags flags[100];
+"""
+
+DUPLEX = ["", "", "-", "+"]
+TMODES = ["", "", "Tone", "TSQL"]
+
+ at directory.register
+class ICT8ARadio(icf.IcomCloneModeRadio):
+    """Icom IC-T8A"""
+    VENDOR = "Icom"
+    MODEL = "IC-T8A"
+
+    _model = "\x19\x03\x00\x01"
+    _memsize = 0x07B0
+    _endframe = "Icom Inc\x2e"
+
+    _ranges = [(0x0000, 0x07B0, 16)]
+
+    def get_features(self):
+        rf = chirp_common.RadioFeatures()
+        rf.valid_tmodes = TMODES
+        rf.valid_duplexes = DUPLEX
+        rf.valid_bands = [(50000000, 54000000),
+                          (118000000, 174000000),
+                          (400000000, 470000000)]
+        rf.valid_skips = ["", "S"]
+        rf.valid_modes = ["FM"]
+        rf.memory_bounds = (0, 99)
+        rf.has_name = False
+        rf.has_dtcs = False
+        rf.has_dtcs_polarity = False
+        rf.has_tuning_step = False
+        rf.has_mode = False
+        rf.has_bank = False
+        return rf
+
+    def process_mmap(self):
+        self._memobj = bitwise.parse(mem_format, self._mmap)
+
+    def get_raw_memory(self, number):
+        return (str(self._memobj.memory[number]) +
+                str(self._memobj.duptone[number]))
+
+    def get_memory(self, number):
+        _mem = self._memobj.memory[number]
+        _flg = self._memobj.flags[number]
+
+        mem = chirp_common.Memory()
+        mem.number = number
+
+        if _flg.empty:
+            mem.empty = True
+            return mem
+
+        mem.freq = int(_mem.freq) * 10
+        mem.offset = int(_mem.offset) * 1000
+        mem.rtone = chirp_common.TONES[_mem.rtone - 1]
+        mem.ctone = chirp_common.TONES[_mem.ctone - 1]
+        mem.duplex = DUPLEX[_flg.duplex]
+        mem.tmode = TMODES[_flg.tmode]
+        mem.skip = _flg.skip and "S" or ""
+
+        return mem
+
+    def set_memory(self, mem):
+        _mem = self._memobj.memory[mem.number]
+        _flg = self._memobj.flags[mem.number]
+
+        if mem.empty:
+            _flg.empty = True
+            return
+
+        _mem.set_raw("\x00" * 8)
+        _flg.set_raw("\x00")
+
+        _mem.freq = mem.freq / 10
+        _mem.offset = mem.offset / 1000
+        _mem.rtone = chirp_common.TONES.index(mem.rtone) + 1
+        _mem.ctone = chirp_common.TONES.index(mem.ctone) + 1
+        _flg.duplex = DUPLEX.index(mem.duplex)
+        _flg.tmode = TMODES.index(mem.tmode)
+        _flg.skip = mem.skip == "S"
diff --git a/chirp/icw32.py b/chirp/icw32.py
new file mode 100644
index 0000000..2b028ad
--- /dev/null
+++ b/chirp/icw32.py
@@ -0,0 +1,200 @@
+# Copyright 2011 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/>.
+
+from chirp import chirp_common, icf, util, directory
+from chirp import bitwise
+
+MEM_FORMAT = """
+#seekto 0x%x;
+struct {
+  bbcd freq[3];
+  bbcd offset[3];
+  u8 ctone;
+  u8 rtone;
+  char name[8];
+} memory[111];
+
+#seekto 0x%x;
+struct {
+  u8 empty:1,
+     skip:1,
+     tmode:2,
+     duplex:2,
+     unk3:1,
+     am:1;
+} flag[111];
+
+#seekto 0x0E9C;
+struct {
+  u8 unknown1:7,
+     right_scan_direction:1;
+  u8 right_scanning:1,
+     unknown2:7;
+  u8 unknown3:7,
+     left_scan_direction:1;
+  u8 left_scanning:1,
+     unknown4:7;
+} state[1];
+
+#seekto 0x0F20;
+struct {
+  bbcd freq[3];
+  bbcd offset[3];
+  u8 ctone;
+  u8 rtone;
+} callchans[2];
+
+"""
+
+DUPLEX = ["", "", "-", "+"]
+TONE = ["", "", "Tone", "TSQL"]
+
+def _get_special():
+    special = {}
+    for i in range(0, 5):
+        special["M%iA" % (i+1)] = 100 + i*2
+        special["M%iB" % (i+1)] = 100 + i*2 + 1
+    return special            
+
+ at directory.register
+class ICW32ARadio(icf.IcomCloneModeRadio):
+    """Icom IC-W32A"""
+    VENDOR = "Icom"
+    MODEL = "IC-W32A"
+
+    _model = "\x18\x82\x00\x01"
+    _memsize = 4064
+    _endframe = "Icom Inc\x2e"
+
+    _ranges = [(0x0000, 0x0FE0, 16)]
+
+    _limits = (0, 0)
+    _mem_positions = (0, 1)
+
+    def get_features(self):
+        rf = chirp_common.RadioFeatures()
+        rf.memory_bounds = (0, 99)
+        rf.valid_bands = [self._limits]
+        if int(self._limits[0] / 100) == 1:
+            rf.valid_modes = ["FM", "AM"]
+        else:
+            rf.valid_modes = ["FM"]
+        rf.valid_tmodes = ["", "Tone", "TSQL"]
+        rf.valid_name_length = 8
+        rf.valid_special_chans = sorted(_get_special().keys())
+    
+        rf.has_sub_devices = self.VARIANT == ""
+        rf.has_ctone = True
+        rf.has_dtcs = False
+        rf.has_dtcs_polarity = False
+        rf.has_mode = "AM" in rf.valid_modes
+        rf.has_tuning_step = False
+        rf.has_bank = False
+
+        return rf
+
+    def process_mmap(self):
+        fmt = MEM_FORMAT % self._mem_positions
+        self._memobj = bitwise.parse(fmt, self._mmap)
+
+    def get_raw_memory(self, number):
+        return repr(self._memobj.memory[number])
+
+    def get_memory(self, number):
+        if isinstance(number, str):
+            number = _get_special()[number]
+
+        _mem = self._memobj.memory[number]
+        _flg = self._memobj.flag[number]
+
+        mem = chirp_common.Memory()
+        mem.number = number
+
+        if number < 100:
+            # Normal memories
+            mem.skip = _flg.skip and "S" or ""
+        else:
+            # Special memories
+            mem.extd_number = util.get_dict_rev(_get_special(), number)
+
+        if _flg.empty:
+            mem.empty = True
+            return mem
+
+        mem.freq = chirp_common.fix_rounded_step(int(_mem.freq) * 1000)
+        mem.offset = int(_mem.offset) * 100
+        if str(_mem.name)[0] != chr(0xFF):
+            mem.name = str(_mem.name).rstrip()
+        mem.rtone = chirp_common.TONES[_mem.rtone]
+        mem.ctone = chirp_common.TONES[_mem.ctone]
+
+        mem.mode = _flg.am and "AM" or "FM"
+        mem.duplex = DUPLEX[_flg.duplex]
+        mem.tmode = TONE[_flg.tmode]
+
+        if number > 100:
+            mem.immutable = ["number", "skip", "extd_number", "name"]
+
+        return mem
+
+    def set_memory(self, mem):
+        _mem = self._memobj.memory[mem.number]
+        _flg = self._memobj.flag[mem.number]
+
+        _flg.empty = mem.empty
+        if mem.empty:
+            return
+
+        _mem.freq = mem.freq / 1000
+        _mem.offset = mem.offset / 100
+        if mem.name:
+            _mem.name = mem.name.ljust(8)[:8]
+        else:
+            _mem.name = "".join(["\xFF" * 8])
+        _mem.rtone = chirp_common.TONES.index(mem.rtone)
+        _mem.ctone = chirp_common.TONES.index(mem.ctone)
+
+        _flg.duplex = DUPLEX.index(mem.duplex)
+        _flg.tmode = TONE.index(mem.tmode)
+        _flg.skip = mem.skip == "S"
+        _flg.am = mem.mode == "AM"
+
+        if self._memobj.state.left_scanning:
+            print "Canceling scan on left VFO"
+            self._memobj.state.left_scanning = 0
+        if self._memobj.state.right_scanning:
+            print "Canceling scan on right VFO"
+            self._memobj.state.right_scanning = 0
+
+    def get_sub_devices(self):
+        return [ICW32ARadioVHF(self._mmap), ICW32ARadioUHF(self._mmap)]
+
+    @classmethod
+    def match_model(cls, filedata, filename):
+        if not len(filedata) == cls._memsize:
+            return False
+        return filedata[-16:] == "IcomCloneFormat3"
+
+class ICW32ARadioVHF(ICW32ARadio):
+    """ICW32 VHF subdevice"""
+    VARIANT = "VHF"
+    _limits = (118000000, 174000000)
+    _mem_positions = (0x0000, 0x0DC0)
+
+class ICW32ARadioUHF(ICW32ARadio):
+    """ICW32 UHF subdevice"""
+    VARIANT = "UHF"
+    _limits = (400000000, 470000000)
+    _mem_positions = (0x06E0, 0x0E2E)
diff --git a/chirp/icx8x.py b/chirp/icx8x.py
new file mode 100644
index 0000000..b9ea35c
--- /dev/null
+++ b/chirp/icx8x.py
@@ -0,0 +1,202 @@
+# 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/>.
+
+from chirp import chirp_common, icf, icx8x_ll, errors, directory
+
+def _isuhf(pipe):
+    try:
+        md = icf.get_model_data(pipe)
+        val = ord(md[20])
+        uhf = val & 0x10
+    except:
+        raise errors.RadioError("Unable to probe radio band")
+
+    print "Radio is a %s82" % (uhf and "U" or "V")
+
+    return uhf
+
+ at directory.register
+class ICx8xRadio(icf.IcomCloneModeRadio, chirp_common.IcomDstarSupport):
+    """Icom IC-V/U82"""
+    VENDOR = "Icom"
+    MODEL = "IC-V82/U82"
+
+    _model = "\x28\x26\x00\x01"
+    _memsize = 6464
+    _endframe = "Icom Inc\x2eCD"
+
+    _memories = []
+
+    _ranges = [(0x0000, 0x1340, 32),
+               (0x1340, 0x1360, 16),
+               (0x1360, 0x136B,  8),
+
+               (0x1370, 0x1440, 32),
+
+               (0x1460, 0x15D0, 32),
+
+               (0x15E0, 0x1930, 32),
+
+               (0x1938, 0x1940,  8),
+               ]
+
+    MYCALL_LIMIT = (0, 6)
+    URCALL_LIMIT = (0, 6)
+    RPTCALL_LIMIT = (0, 6)
+
+    def _get_bank(self, loc):
+        return icx8x_ll.get_bank(self._mmap, loc)
+
+    def _set_bank(self, loc, bank):
+        return icx8x_ll.set_bank(self._mmap, loc, bank)
+
+    def get_features(self):
+        rf = chirp_common.RadioFeatures()
+        rf.memory_bounds = (0, 199)
+        rf.valid_modes = ["FM", "NFM", "DV"]
+        rf.valid_tmodes = ["", "Tone", "TSQL", "DTCS"]
+        rf.valid_duplexes = ["", "-", "+"]
+        rf.valid_tuning_steps = [x for x in chirp_common.TUNING_STEPS
+                                 if x != 6.25]
+        if self._isuhf:
+            rf.valid_bands = [(420000000, 470000000)]
+        else:
+            rf.valid_bands = [(118000000, 176000000)]
+        rf.valid_skips = ["", "S"]
+        rf.valid_name_length = 5
+        rf.valid_special_chans = sorted(icx8x_ll.ICx8x_SPECIAL.keys())
+
+        return rf
+
+    def _get_type(self):
+        flag = (_isuhf(self.pipe) != 0)
+
+        if self._isuhf is not None and (self._isuhf != flag):
+            raise errors.RadioError("VHF/UHF model mismatch")
+
+        self._isuhf = flag
+
+        return flag
+
+    def __init__(self, pipe):
+        icf.IcomCloneModeRadio.__init__(self, pipe)
+
+        # Until I find a better way, I'll stash a boolean to indicate
+        # UHF-ness in an unused region of memory.  If we're opening a
+        # file, look for the flag.  If we're syncing from serial, set
+        # that flag.
+        if isinstance(pipe, str):
+            self._isuhf = (ord(self._mmap[0x1930]) != 0)
+            #print "Found %s image" % (self.isUHF and "UHF" or "VHF")
+        else:
+            self._isuhf = None
+
+    def sync_in(self):
+        self._get_type()
+        icf.IcomCloneModeRadio.sync_in(self)
+        self._mmap[0x1930] = self._isuhf and 1 or 0
+
+    def sync_out(self):
+        self._get_type()
+        icf.IcomCloneModeRadio.sync_out(self)
+
+    def get_memory(self, number):
+        if not self._mmap:
+            self.sync_in()
+
+        if self._isuhf:
+            base = 400
+        else:
+            base = 0
+
+        if isinstance(number, str):
+            try:
+                number = icx8x_ll.ICx8x_SPECIAL[number]
+            except KeyError:
+                raise errors.InvalidMemoryLocation("Unknown channel %s" % \
+                                                       number)
+
+        return icx8x_ll.get_memory(self._mmap, number, base)
+
+    def set_memory(self, memory):
+        if not self._mmap:
+            self.sync_in()
+
+        if self._isuhf:
+            base = 400
+        else:
+            base = 0
+
+        if memory.empty:
+            self._mmap = icx8x_ll.erase_memory(self._mmap, memory.number)
+        else:
+            self._mmap = icx8x_ll.set_memory(self._mmap, memory, base)
+
+    def get_raw_memory(self, number):
+        return icx8x_ll.get_raw_memory(self._mmap, number)
+
+    def get_urcall_list(self):
+        calls = []
+
+        for i in range(*self.URCALL_LIMIT):
+            call = icx8x_ll.get_urcall(self._mmap, i)
+            calls.append(call)
+
+        return calls
+
+    def get_repeater_call_list(self):
+        calls = []
+
+        for i in range(*self.RPTCALL_LIMIT):
+            call = icx8x_ll.get_rptcall(self._mmap, i)
+            calls.append(call)
+
+        return calls
+
+    def get_mycall_list(self):
+        calls = []
+
+        for i in range(*self.MYCALL_LIMIT):
+            call = icx8x_ll.get_mycall(self._mmap, i)
+            calls.append(call)
+
+        return calls
+
+    def set_urcall_list(self, calls):
+        for i in range(*self.URCALL_LIMIT):
+            try:
+                call = calls[i]
+            except IndexError:
+                call = " " * 8
+
+            icx8x_ll.set_urcall(self._mmap, i, call)
+
+    def set_repeater_call_list(self, calls):
+        for i in range(*self.RPTCALL_LIMIT):
+            try:
+                call = calls[i]
+            except IndexError:
+                call = " " * 8
+
+            icx8x_ll.set_rptcall(self._mmap, i, call)
+
+    def set_mycall_list(self, calls):
+        for i in range(*self.MYCALL_LIMIT):
+            try:
+                call = calls[i]
+            except IndexError:
+                call = " " * 8
+
+            icx8x_ll.set_mycall(self._mmap, i, call)
diff --git a/chirp/icx8x_ll.py b/chirp/icx8x_ll.py
new file mode 100644
index 0000000..92f2d9c
--- /dev/null
+++ b/chirp/icx8x_ll.py
@@ -0,0 +1,493 @@
+# 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 struct
+
+from chirp import chirp_common, errors
+from chirp.memmap import MemoryMap
+from chirp.chirp_common import to_MHz
+
+POS_FREQ_START = 0
+POS_FREQ_END   = 2
+POS_OFFSET     = 2
+POS_NAME_START = 4
+POS_NAME_END   = 9
+POS_RTONE      = 9
+POS_CTONE      = 10
+POS_DTCS       = 11
+POS_TUNE_STEP  = 17
+POS_TMODE      = 21
+POS_MODE       = 21
+POS_MULT_FLAG  = 21
+POS_DTCS_POL   = 22
+POS_DUPLEX     = 22
+POS_DIG        = 23
+POS_TXI        = 23
+
+POS_FLAGS_START= 0x1370
+POS_MYCALL     = 0x15E0
+POS_URCALL     = 0x1640
+POS_RPCALL     = 0x16A0
+POS_RP2CALL    = 0x1700
+
+MEM_LOC_SIZE   = 24
+
+ICx8x_SPECIAL = { "C" : 206 }
+ICx8x_SPECIAL_REV = { 206 : "C" }
+
+for i in range(0, 3):
+    idA = "%iA" % i
+    idB = "%iB" % i
+    num = 200 + i * 2
+    ICx8x_SPECIAL[idA] = num
+    ICx8x_SPECIAL[idB] = num + 1
+    ICx8x_SPECIAL_REV[num] = idA
+    ICx8x_SPECIAL_REV[num+1] = idB
+
+def bank_name(index):
+    char = chr(ord("A") + index)
+    return "BANK-%s" % char
+
+def get_freq(mmap, base):
+    if (ord(mmap[POS_MULT_FLAG]) & 0x80) == 0x80:
+        mult = 6250
+    else:
+        mult = 5000
+
+    val = struct.unpack("<H", mmap[POS_FREQ_START:POS_FREQ_END])[0]
+
+    return (val * mult) + to_MHz(base)
+
+def set_freq(mmap, freq, base):
+    tflag = ord(mmap[POS_MULT_FLAG]) & 0x7F
+
+    if chirp_common.is_fractional_step(freq):
+        mult = 6250
+        tflag |= 0x80
+    else:
+        mult = 5000
+
+    value = (freq - to_MHz(base)) / mult
+
+    mmap[POS_MULT_FLAG] = tflag
+    mmap[POS_FREQ_START] = struct.pack("<H", value)
+
+def get_name(mmap):
+    return mmap[POS_NAME_START:POS_NAME_END].strip()
+
+def set_name(mmap, name):
+    mmap[POS_NAME_START] = name.ljust(5)[:5]
+
+def get_rtone(mmap):
+    idx, = struct.unpack("B", mmap[POS_RTONE])
+
+    return chirp_common.TONES[idx]
+
+def set_rtone(mmap, tone):
+    mmap[POS_RTONE] = chirp_common.TONES.index(tone)
+
+def get_ctone(mmap):
+    idx, = struct.unpack("B", mmap[POS_CTONE])
+
+    return chirp_common.TONES[idx]
+
+def set_ctone(mmap, tone):
+    mmap[POS_CTONE] = chirp_common.TONES.index(tone)
+
+def get_dtcs(mmap):
+    idx, = struct.unpack("B", mmap[POS_DTCS])
+
+    return chirp_common.DTCS_CODES[idx]
+
+def set_dtcs(mmap, code):
+    mmap[POS_DTCS] = chirp_common.DTCS_CODES.index(code)
+
+def get_dtcs_polarity(mmap):
+    val = struct.unpack("B", mmap[POS_DTCS_POL])[0] & 0xC0
+
+    pol_values = {
+        0x00 : "NN",
+        0x40 : "NR",
+        0x80 : "RN",
+        0xC0 : "RR" }
+
+    return pol_values[val]
+
+def set_dtcs_polarity(mmap, polarity):
+    val = struct.unpack("B", mmap[POS_DTCS_POL])[0] & 0x3F
+    pol_values = { "NN" : 0x00,
+                   "NR" : 0x40,
+                   "RN" : 0x80,
+                   "RR" : 0xC0 }
+    val |= pol_values[polarity]
+
+    mmap[POS_DTCS_POL] = val
+
+def get_dup_offset(mmap):
+    val = struct.unpack("<H", mmap[POS_OFFSET:POS_OFFSET+2])[0]
+    return val * 5000
+
+def set_dup_offset(mmap, offset):
+    val = struct.pack("<H", offset / 5000)
+    mmap[POS_OFFSET] = val
+
+def get_duplex(mmap):
+    val = struct.unpack("B", mmap[POS_DUPLEX])[0] & 0x30
+
+    if val == 0x10:
+        return "-"
+    elif val == 0x20:
+        return "+"
+    else:
+        return ""
+
+def set_duplex(mmap, duplex):
+    val = struct.unpack("B", mmap[POS_DUPLEX])[0] & 0xCF
+
+    if duplex == "-":
+        val |= 0x10
+    elif duplex == "+":
+        val |= 0x20
+
+    mmap[POS_DUPLEX] = val
+
+def get_tone_enabled(mmap):
+    val = struct.unpack("B", mmap[POS_TMODE])[0] & 0x03
+
+    if val == 0x01:
+        return "Tone"
+    elif val == 0x02:
+        return "TSQL"
+    elif val == 0x03:
+        return "DTCS"
+    else:
+        return ""
+
+def set_tone_enabled(mmap, tmode):
+    val = struct.unpack("B", mmap[POS_TMODE])[0] & 0xFC
+
+    if tmode == "Tone":
+        val |= 0x01
+    elif tmode == "TSQL":
+        val |= 0x02
+    elif tmode == "DTCS":
+        val |= 0x03
+
+    mmap[POS_TMODE] = val
+
+def get_tune_step(mmap):
+    tsidx = struct.unpack("B", mmap[POS_TUNE_STEP])[0] & 0xF0
+    tsidx >>= 4
+    icx8x_ts = list(chirp_common.TUNING_STEPS)
+    del icx8x_ts[1]
+
+    try:
+        return icx8x_ts[tsidx]
+    except IndexError:
+        raise errors.InvalidDataError("TS index %i out of range (%i)" % (tsidx,
+                                                                         len(icx8x_ts)))
+
+def set_tune_step(mmap, tstep):
+    val = struct.unpack("B", mmap[POS_TUNE_STEP])[0] & 0x0F
+    icx8x_ts = list(chirp_common.TUNING_STEPS)
+    del icx8x_ts[1]
+
+    tsidx = icx8x_ts.index(tstep)
+    val |= (tsidx << 4)
+
+    mmap[POS_TUNE_STEP] = val    
+
+def get_mode(mmap):
+    val = struct.unpack("B", mmap[POS_DIG])[0] & 0x08
+
+    if val == 0x08:
+        return "DV"
+
+    val = struct.unpack("B", mmap[POS_MODE])[0] & 0x20
+
+    if val == 0x20:
+        return "NFM"
+    else:
+        return "FM"
+
+def set_mode(mmap, mode):
+    dig = struct.unpack("B", mmap[POS_DIG])[0] & 0xF7
+
+    val = struct.unpack("B", mmap[POS_MODE])[0] & 0xDF
+
+    if mode == "FM":
+        pass
+    elif mode == "NFM":
+        val |= 0x20
+    elif mode == "DV":
+        dig |= 0x08
+    else:
+        raise errors.InvalidDataError("%s mode not supported" % mode)
+
+    mmap[POS_DIG] = dig
+    mmap[POS_MODE] = val
+
+def is_used(mmap, number):
+    if number == ICx8x_SPECIAL["C"]:
+        return True
+
+    return (ord(mmap[POS_FLAGS_START + number]) & 0x20) == 0
+
+def set_used(mmap, number, used=True):
+    if number == ICx8x_SPECIAL["C"]:
+        return
+
+    val = struct.unpack("B", mmap[POS_FLAGS_START + number])[0] & 0xDF
+
+    if not used:
+        val |= 0x20
+
+    mmap[POS_FLAGS_START + number] = val
+
+def get_skip(mmap, number):
+    val = struct.unpack("B", mmap[POS_FLAGS_START + number])[0] & 0x10
+
+    if val != 0:
+        return "S"
+    else:
+        return ""
+
+def set_skip(mmap, number, skip):
+    if skip == "P":
+        raise errors.InvalidDataError("PSKIP not supported by this model")
+
+    val = struct.unpack("B", mmap[POS_FLAGS_START + number])[0] & 0xEF
+
+    if skip == "S":
+        val |= 0x10
+
+    mmap[POS_FLAGS_START + number] = val
+
+def get_call_indices(mmap):
+    return ord(mmap[18]) & 0x0F, \
+        (ord(mmap[19]) & 0xF0) >> 4, \
+        ord(mmap[19]) & 0x0F
+
+def set_call_indices(_map, mmap, urcall, r1call, r2call):
+    ulist = []
+    for i in range(0, 6):
+        ulist.append(get_urcall(_map, i))
+
+    rlist = []
+    for i in range(0, 6):
+        rlist.append(get_rptcall(_map, i))
+
+    try:
+        if not urcall:
+            uindex = 0
+        else:
+            uindex = ulist.index(urcall)
+    except ValueError:
+        raise errors.InvalidDataError("Call `%s' not in URCALL list" % urcall)
+
+    try:
+        if not r1call:
+            r1index = 0
+        else:
+            r1index = rlist.index(r1call)
+    except ValueError:
+        raise errors.InvalidDataError("Call `%s' not in RCALL list" % r1call)
+
+    try:
+        if not r2call:
+            r2index = 0
+        else:
+            r2index = rlist.index(r2call)
+    except ValueError:
+        raise errors.InvalidDataError("Call `%s' not in RCALL list" % r2call)
+
+    mmap[18] = (ord(mmap[18]) & 0xF0) | uindex
+    mmap[19] = (r1index << 4) | r2index
+
+# --
+
+def get_mem_offset(number):
+    return number * MEM_LOC_SIZE
+
+def get_raw_memory(mmap, number):
+    offset = get_mem_offset(number)
+    return MemoryMap(mmap[offset:offset + MEM_LOC_SIZE])
+
+def get_bank(mmap, number):
+    val = ord(mmap[POS_FLAGS_START + number]) & 0x0F
+
+    if val >= 10:
+        return None
+    else:
+        return val
+
+def set_bank(mmap, number, bank):
+    if bank > 9:
+        raise errors.InvalidDataError("Invalid bank number %i" % bank)
+
+    if bank is None:
+        index = 0x0A
+    else:
+        index = bank
+
+    val = ord(mmap[POS_FLAGS_START + number]) & 0xF0
+    val |= index
+    mmap[POS_FLAGS_START + number] = val    
+
+def _get_memory(_map, mmap, base):
+    if get_mode(mmap) == "DV":
+        mem = chirp_common.DVMemory()
+        i_ucall, i_r1call, i_r2call = get_call_indices(mmap)
+        mem.dv_urcall = get_urcall(_map, i_ucall)
+        mem.dv_rpt1call = get_rptcall(_map, i_r1call)
+        mem.dv_rpt2call = get_rptcall(_map, i_r2call)
+    else:
+        mem = chirp_common.Memory()
+
+    mem.freq = get_freq(mmap, base)
+    mem.name = get_name(mmap)
+    mem.rtone = get_rtone(mmap)
+    mem.ctone = get_ctone(mmap)
+    mem.dtcs = get_dtcs(mmap)
+    mem.dtcs_polarity = get_dtcs_polarity(mmap)
+    mem.offset = get_dup_offset(mmap)
+    mem.duplex = get_duplex(mmap)
+    mem.tmode = get_tone_enabled(mmap)
+    mem.tuning_step = get_tune_step(mmap)
+    mem.mode = get_mode(mmap)
+
+    return mem
+
+def get_memory(_map, number, base):
+    if not is_used(_map, number):
+        mem = chirp_common.Memory()
+        if number < 200:
+            mem.number = number
+            mem.empty = True
+            return mem
+    else:
+        mmap = get_raw_memory(_map, number)
+        mem = _get_memory(_map, mmap, base)
+
+    mem.number = number
+
+    if number < 200:
+        mem.skip = get_skip(_map, number)
+    else:
+        mem.extd_number = ICx8x_SPECIAL_REV[number]
+        mem.immutable = ["number", "skip", "bank", "bank_index", "extd_number"]
+
+    return mem
+
+def clear_tx_inhibit(mmap):
+    txi = struct.unpack("B", mmap[POS_TXI])[0]
+    txi |= 0x40
+    mmap[POS_TXI] = txi
+
+def set_memory(_map, memory, base):
+    mmap = get_raw_memory(_map, memory.number)
+
+    set_freq(mmap, memory.freq, base)
+    set_name(mmap, memory.name)
+    set_rtone(mmap, memory.rtone)
+    set_ctone(mmap, memory.ctone)
+    set_dtcs(mmap, memory.dtcs)
+    set_dtcs_polarity(mmap, memory.dtcs_polarity)
+    set_dup_offset(mmap, memory.offset)
+    set_duplex(mmap, memory.duplex)
+    set_tone_enabled(mmap, memory.tmode)
+    set_tune_step(mmap, memory.tuning_step)
+    set_mode(mmap, memory.mode)
+    if memory.number < 200:
+        set_skip(_map, memory.number, memory.skip)
+
+    if isinstance(memory, chirp_common.DVMemory):
+        set_call_indices(_map,
+                         mmap,
+                         memory.dv_urcall,
+                         memory.dv_rpt1call,
+                         memory.dv_rpt2call)
+
+    if not is_used(_map, memory.number):
+        clear_tx_inhibit(mmap)
+
+    _map[get_mem_offset(memory.number)] = mmap.get_packed()
+    set_used(_map, memory.number)
+
+    return _map
+
+def erase_memory(_map, number):
+    set_used(_map, number, False)
+
+    return _map
+
+def call_location(base, index):
+    return base + (16 * index)
+
+def get_urcall(mmap, index):
+    if index > 5:
+        raise errors.InvalidDataError("URCALL index %i must be <= 5" % index)
+
+    start = call_location(POS_URCALL, index)
+
+    return mmap[start:start+8].rstrip()
+
+def get_rptcall(mmap, index):
+    if index > 5:
+        raise errors.InvalidDataError("RPTCALL index %i must be <= 5" % index)
+
+    start = call_location(POS_RPCALL, index)
+
+    return mmap[start:start+8].rstrip()
+
+def get_mycall(mmap, index):
+    if index > 5:
+        raise errors.InvalidDataError("MYCALL index %i must be <= 5" % index)
+
+    start = call_location(POS_MYCALL, index)
+
+    return mmap[start:start+8].rstrip()
+
+def set_urcall(mmap, index, call):
+    if index > 5:
+        raise errors.InvalidDataError("URCALL index %i must be <= 5" % index)
+
+    start = call_location(POS_URCALL, index)
+
+    mmap[start] = call.ljust(12)
+    
+    return mmap
+
+def set_rptcall(mmap, index, call):
+    if index > 5:
+        raise errors.InvalidDataError("RPTCALL index %i must be <= 5" % index)
+
+    start = call_location(POS_RPCALL, index)
+    mmap[start] = call.ljust(12)
+
+    start = call_location(POS_RP2CALL, index)
+    mmap[start] = call.ljust(12)
+    
+    return mmap
+
+def set_mycall(mmap, index, call):
+    if index > 5:
+        raise errors.InvalidDataError("MYCALL index %i must be <= 5" % index)
+
+    start = call_location(POS_MYCALL, index)
+
+    mmap[start] = call.ljust(12)
+    
+    return mmap
diff --git a/chirp/id31.py b/chirp/id31.py
new file mode 100644
index 0000000..b381d07
--- /dev/null
+++ b/chirp/id31.py
@@ -0,0 +1,334 @@
+# Copyright 2012 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/>.
+
+from chirp import directory, icf, bitwise, chirp_common
+
+MEM_FORMAT = """
+struct {
+  u24 freq;
+  u16 offset;
+  u16 rtone:6,
+      ctone:6,
+      unknown2:1,
+      is_dv:1,
+      unknown2_0:1,
+      is_narrow:1;
+  u8 dtcs;
+  u8 tune_step:4,
+     unknown5:4;
+  u8 unknown4;
+  u8 tmode:4,
+     duplex:2,
+     dtcs_polarity:2;
+  char name[16];
+  u8 unknow13;
+  u8 urcall[7];
+  u8 rpt1call[7];
+  u8 rpt2call[7];
+} memory[500];
+
+#seekto 0x69C0;
+u8 used_flags[70];
+
+#seekto 0x6A06;
+u8 skip_flags[69];
+
+#seekto 0x6A4B;
+u8 pskp_flags[69];
+
+#seekto 0x6AC0;
+struct {
+  u8 bank;
+  u8 index;
+} banks[500];
+
+#seekto 0x6F50;
+struct {
+  char name[16];
+} bank_names[26];
+
+#seekto 0x74BF;
+struct {
+  u8 unknown0;
+  u24 freq;
+  u16 offset;
+  u8 unknown1[3];
+  u8 call[7];
+  char name[16];
+  char subname[8];
+  u8 unknown3[9];
+} repeaters[700];
+
+#seekto 0xFABC;
+struct {
+  u8 call[7];
+} rptcall[700];
+
+#seekto 0x10F20;
+struct {
+  char call[8];
+} mycall[6];
+
+#seekto 0x10F68;
+struct {
+  char call[8];
+} urcall[200];
+
+"""
+
+TMODES = ["", "Tone", "TSQL", "TSQL", "DTCS", "DTCS", "TSQL-R", "DTCS-R"]
+DUPLEX = ["", "-", "+"]
+DTCS_POLARITY = ["NN", "NR", "RN", "RR"]
+TUNING_STEPS = [5.0, 6.25, 0, 0, 10.0, 12.5, 15.0, 20.0, 25.0, 30.0, 50.0,
+                100.0, 125.0, 200.0]
+
+def _decode_call(_call):
+    # Why Icom, why?
+    call = ""
+    shift = 1
+    acc = 0
+    for val in _call:
+        mask = (1 << (shift)) - 1
+        call += chr((val >> shift) | acc)
+        acc = (val & mask) << (7 - shift)
+        shift += 1
+    call += chr(acc)
+    return call
+
+def _encode_call(call):
+    _call = [0x00] * 7
+    for i in range(0, 7):
+        val = ord(call[i]) << (i + 1)
+        if i > 0:
+            _call[i-1] |= (val & 0xFF00) >> 8
+        _call[i] = val
+    _call[6] |= (ord(call[7]) & 0x7F)
+        
+    return _call
+
+def _get_freq(_mem):
+    freq = int(_mem.freq)
+    offs = int(_mem.offset)
+
+    if freq & 0x00200000:
+        mult = 6250
+    else:
+        mult = 5000
+
+    freq &= 0x0003FFFF
+
+    return (freq * mult), (offs * mult)
+
+def _set_freq(_mem, freq, offset):
+    if chirp_common.is_fractional_step(freq):
+        mult = 6250
+        flag = 0x00200000
+    else:
+        mult = 5000
+        flag = 0x00000000
+
+    _mem.freq = (freq / mult) | flag
+    _mem.offset = (offset / mult)
+
+class ID31Bank(icf.IcomBank):
+    """A ID-31 Bank"""
+    def get_name(self):
+        _banks = self._model._radio._memobj.bank_names
+        return str(_banks[self.index].name).rstrip()
+
+    def set_name(self, name):
+        _banks = self._model._radio._memobj.bank_names
+        _banks[self.index].name = str(name).ljust(16)[:16]
+
+ at directory.register
+class ID31Radio(icf.IcomCloneModeRadio, chirp_common.IcomDstarSupport):
+    """Icom ID-31"""
+    MODEL = "ID-31A"
+
+    _memsize = 0x15500
+    _model = "\x33\x22\x00\x01"
+    _endframe = "Icom Inc\x2E\x41\x38"
+    _num_banks = 26
+    _bank_class = ID31Bank
+    _can_hispeed = True
+
+    _ranges = [(0x00000, 0x15500, 32)]
+
+    def _get_bank(self, loc):
+        _bank = self._memobj.banks[loc]
+        if _bank.bank == 0xFF:
+            return None
+        else:
+            return _bank.bank
+
+    def _set_bank(self, loc, bank):
+        _bank = self._memobj.banks[loc]
+        if bank is None:
+            _bank.bank = 0xFF
+        else:
+            _bank.bank = bank
+
+    def _get_bank_index(self, loc):
+        _bank = self._memobj.banks[loc]
+        return _bank.index
+
+    def _set_bank_index(self, loc, index):
+        _bank = self._memobj.banks[loc]
+        _bank.index = index
+
+    def get_features(self):
+        rf = chirp_common.RadioFeatures()
+        rf.memory_bounds = (0, 499)
+        rf.valid_bands = [(400000000, 479000000)]
+        rf.has_settings = True
+        rf.has_ctone = True
+        rf.has_bank_index = True
+        rf.has_bank_names = True
+        rf.valid_tmodes = list(TMODES)
+        rf.valid_tuning_steps = sorted(list(TUNING_STEPS))
+        rf.valid_modes = ["FM", "NFM", "DV"]
+        rf.valid_skips = ["", "S", "P"]
+        rf.valid_characters = chirp_common.CHARSET_ASCII
+        rf.valid_name_length = 16
+        return rf
+
+    def process_mmap(self):
+        self._memobj = bitwise.parse(MEM_FORMAT, self._mmap)
+
+    def get_raw_memory(self, number):
+        return repr(self._memobj.memory[number])
+
+    def get_memory(self, number):
+        _mem = self._memobj.memory[number]
+        _usd = self._memobj.used_flags[number / 8]
+        _skp = self._memobj.skip_flags[number / 8]
+        _psk = self._memobj.pskp_flags[number / 8]
+
+        bit = (1 << (number % 8))
+
+        if _mem.is_dv:
+            mem = chirp_common.DVMemory()
+        else:
+            mem = chirp_common.Memory()
+        mem.number = number
+
+        if _usd & bit:
+            mem.empty = True
+            return mem
+
+        mem.freq, mem.offset = _get_freq(_mem)
+        mem.name = str(_mem.name).rstrip()
+        mem.rtone = chirp_common.TONES[_mem.rtone]
+        mem.ctone = chirp_common.TONES[_mem.ctone]
+        mem.tmode = TMODES[_mem.tmode]
+        mem.duplex = DUPLEX[_mem.duplex]
+        mem.dtcs = chirp_common.DTCS_CODES[_mem.dtcs]
+        mem.dtcs_polarity = DTCS_POLARITY[_mem.dtcs_polarity]
+        mem.tuning_step = TUNING_STEPS[_mem.tune_step]
+
+        if _mem.is_dv:
+            mem.mode = "DV"
+            mem.dv_urcall = _decode_call(_mem.urcall).rstrip()
+            mem.dv_rpt1call = _decode_call(_mem.rpt1call).rstrip()
+            mem.dv_rpt2call = _decode_call(_mem.rpt2call).rstrip()
+        elif _mem.is_narrow:
+            mem.mode = "NFM"
+        else:
+            mem.mode = "FM"
+
+        if _psk & bit:
+            mem.skip = "P"
+        if _skp & bit:
+            mem.skip = "S"
+            
+        return mem
+
+    def set_memory(self, memory):
+        _mem = self._memobj.memory[memory.number]
+        _usd = self._memobj.used_flags[memory.number / 8]
+        _skp = self._memobj.skip_flags[memory.number / 8]
+        _psk = self._memobj.pskp_flags[memory.number / 8]
+
+        bit = (1 << (memory.number % 8))
+
+        if memory.empty:
+            _usd |= bit
+            self._set_bank(memory.number, None)
+            return
+
+        _usd &= ~bit
+
+        _set_freq(_mem, memory.freq, memory.offset)
+        _mem.name = memory.name.ljust(16)[:16]
+        _mem.rtone = chirp_common.TONES.index(memory.rtone)
+        _mem.ctone = chirp_common.TONES.index(memory.ctone)
+        _mem.tmode = TMODES.index(memory.tmode)
+        _mem.duplex = DUPLEX.index(memory.duplex)
+        _mem.dtcs = chirp_common.DTCS_CODES.index(memory.dtcs)
+        _mem.dtcs_polarity = DTCS_POLARITY.index(memory.dtcs_polarity)
+        _mem.tune_step = TUNING_STEPS.index(memory.tuning_step)
+        
+        _mem.is_narrow = memory.mode in ["NFM", "DV"]
+        _mem.is_dv = memory.mode == "DV"
+
+        if isinstance(memory, chirp_common.DVMemory):
+            _mem.urcall = _encode_call(memory.dv_urcall.ljust(8))
+            _mem.rpt1call = _encode_call(memory.dv_rpt1call.ljust(8))
+            _mem.rpt2call = _encode_call(memory.dv_rpt2call.ljust(8))
+        elif memory.mode == "DV":
+            raise Exception("BUG")
+
+        if memory.skip == "S":
+            _skp |=  bit
+            _psk &= ~bit
+        elif memory.skip == "P":
+            _skp &= ~bit
+            _psk |=  bit
+        else:
+            _skp &= ~bit
+            _psk &= ~bit
+            
+    def get_urcall_list(self):
+        calls = []
+        for i in range(0, 200):
+            call = str(self._memobj.urcall[i].call)
+            if call == "CALLSIGN":
+                call = ""
+            calls.append(call)
+        return calls
+
+    def get_mycall_list(self):
+        calls = []
+        for i in range(0, 6):
+            calls.append(str(self._memobj.mycall[i].call))
+        return calls
+
+    def get_repeater_call_list(self):
+        calls = []
+        for rptcall in self._memobj.rptcall:
+            call = _decode_call(rptcall.call)
+            if call.rstrip() and not call == "CALLSIGN":
+                calls.append(call)
+        for repeater in self._memobj.repeaters:
+            call = _decode_call(repeater.call)
+            if call == "CALLSIGN":
+                call = ""
+            calls.append(call.rstrip())
+        return calls
+
+if __name__ == "__main__":
+    print repr(_decode_call(_encode_call("KD7REX B")))
+    print repr(_decode_call(_encode_call("       B")))
+    print repr(_decode_call(_encode_call("        ")))
diff --git a/chirp/id800.py b/chirp/id800.py
new file mode 100644
index 0000000..56350de
--- /dev/null
+++ b/chirp/id800.py
@@ -0,0 +1,380 @@
+# 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/>.
+
+from chirp import chirp_common, icf, errors, directory
+from chirp import bitwise
+
+MEM_FORMAT = """
+#seekto 0x0020;
+struct {
+  u24 freq;
+  u16 offset;  
+  u8  unknown0:2,
+      rtone:6;
+  u8  duplex:2,
+      ctone:6;
+  u8  dtcs;
+  u8  tuning_step:4,
+      unknown1:4;
+  u8  unknown2;
+  u8  mult_flag:1,
+      unknown3:5,
+      tmode:2;
+  u16 dtcs_polarity:2,
+      usealpha:1,
+      empty:1,
+      name1:6,
+      name2:6;
+  u24 name3:6,
+      name4:6,
+      name5:6,
+      name6:6;
+  u8 unknown5;
+  u8 unknown6:1,
+     digital_code:7;
+  u8 urcall;
+  u8 rpt1call;
+  u8 rpt2call;  
+  u8 unknown7:1,
+     mode:3,
+     unknown8:4;
+} memory[500];
+
+#seekto 0x2BF4;
+struct {
+  u8 unknown1:1,
+     empty:1,
+     pskip:1,
+     skip:1,
+     bank:4;
+} flags[500];
+
+#seekto 0x3220;
+struct {
+  char call[8];
+} mycalls[8];
+
+#seekto 0x3250;
+struct {
+  char call[8];
+} urcalls[99];
+
+#seekto 0x3570;
+struct {
+  char call[8];
+} rptcalls[59];
+"""
+
+MODES = ["FM", "NFM", "AM", "NAM", "DV"]
+TMODES = ["", "Tone", "TSQL", "DTCS"]
+DUPLEX = ["", "", "-", "+"]
+DTCS_POL = ["NN", "NR", "RN", "RR"]
+STEPS = [5.0, 10.0, 12.5, 15, 20.0, 25.0, 30.0, 50.0, 100.0, 200.0, 6.25]
+
+ID800_SPECIAL = {
+    "C2" : 510,
+    "C1" : 511,
+    }
+ID800_SPECIAL_REV = {
+    510 : "C2",
+    511 : "C1",
+    }
+
+for i in range(0, 5):
+    idA = "%iA" % (i + 1)
+    idB = "%iB" % (i + 1)
+    num = 500 + i * 2
+    ID800_SPECIAL[idA] = num
+    ID800_SPECIAL[idB] = num + 1
+    ID800_SPECIAL_REV[num] = idA
+    ID800_SPECIAL_REV[num+1] = idB
+
+ALPHA_CHARSET = " ABCDEFGHIJKLMNOPQRSTUVWXYZ"
+NUMERIC_CHARSET = "0123456789+-=*/()|"
+
+def get_name(_mem):
+    """Decode the name from @_mem"""
+    def _get_char(val):
+        if val == 0:
+            return " "
+        elif val & 0x20:
+            return ALPHA_CHARSET[val & 0x1F]
+        else:
+            return NUMERIC_CHARSET[val & 0x0F]
+
+    name_bytes = [_mem.name1, _mem.name2, _mem.name3,
+                  _mem.name4, _mem.name5, _mem.name6]
+    name = ""
+    for val in name_bytes:
+        name += _get_char(val)
+
+    return name.rstrip()
+
+def set_name(_mem, name):
+    """Encode @name in @_mem"""
+    def _get_index(char):
+        if char == " ":
+            return 0
+        elif char.isalpha():
+            return ALPHA_CHARSET.index(char) | 0x20
+        else:
+            return NUMERIC_CHARSET.index(char) | 0x10
+
+    name = name.ljust(6)[:6]
+
+    _mem.usealpha = bool(name.strip())
+
+    # The element override calling convention makes this harder to automate.
+    # It's just six, so do it manually
+    _mem.name1 = _get_index(name[0])
+    _mem.name2 = _get_index(name[1])
+    _mem.name3 = _get_index(name[2])
+    _mem.name4 = _get_index(name[3])
+    _mem.name5 = _get_index(name[4])
+    _mem.name6 = _get_index(name[5])
+
+ at directory.register
+class ID800v2Radio(icf.IcomCloneModeRadio, chirp_common.IcomDstarSupport):
+    """Icom ID800"""
+    VENDOR = "Icom"
+    MODEL = "ID-800H"
+    VARIANT = "v2"
+
+    _model = "\x27\x88\x02\x00"
+    _memsize = 14528
+    _endframe = "Icom Inc\x2eCB"
+    _can_hispeed = True
+
+    _memories = []
+
+    _ranges = [(0x0020, 0x2B18, 32),
+               (0x2B18, 0x2B20,  8),
+               (0x2B20, 0x2BE0, 32),
+               (0x2BE0, 0x2BF4, 20),
+               (0x2BF4, 0x2C00, 12),
+               (0x2C00, 0x2DE0, 32),
+               (0x2DE0, 0x2DF4, 20),
+               (0x2DF4, 0x2E00, 12),
+               (0x2E00, 0x2E20, 32),
+
+               (0x2F00, 0x3070, 32),
+
+               (0x30D0, 0x30E0, 16),
+               (0x30E0, 0x3160, 32),
+               (0x3160, 0x3180, 16),
+               (0x3180, 0x31A0, 32),
+               (0x31A0, 0x31B0, 16),
+
+               (0x3220, 0x3240, 32),
+               (0x3240, 0x3260, 16),
+               (0x3260, 0x3560, 32),
+               (0x3560, 0x3580, 16),
+               (0x3580, 0x3720, 32),
+               (0x3720, 0x3780,  8),
+
+               (0x3798, 0x37A0,  8),
+               (0x37A0, 0x37B0, 16),
+               (0x37B0, 0x37B1,  1),
+
+               (0x37D8, 0x37E0,  8),
+               (0x37E0, 0x3898, 32),
+               (0x3898, 0x389A,  2),
+
+               (0x38A8, 0x38C0, 16),]
+
+    MYCALL_LIMIT  = (1, 7)
+    URCALL_LIMIT  = (1, 99)
+    RPTCALL_LIMIT = (1, 59)
+
+    def _get_bank(self, loc):
+        _flg = self._memobj.flags[loc-1]
+        if _flg.bank >= 0x0A:
+            return None
+        else:
+            return _flg.bank
+
+    def _set_bank(self, loc, bank):
+        _flg = self._memobj.flags[loc-1]
+        if bank is None:
+            _flg.bank = 0x0A
+        else:
+            _flg.bank = bank
+
+    def get_features(self):
+        rf = chirp_common.RadioFeatures()
+        rf.has_implicit_calls = True
+        rf.has_settings = True
+        rf.has_bank = True
+        rf.valid_modes = list(MODES)
+        rf.valid_tmodes = ["", "Tone", "TSQL", "DTCS"]
+        rf.valid_duplexes = ["", "-", "+"]
+        rf.valid_tuning_steps = list(STEPS)
+        rf.valid_bands = [(118000000, 173995000), (230000000, 549995000),
+                          (810000000, 999990000)]
+        rf.valid_skips = ["", "S", "P"]
+        rf.valid_name_length = 6
+        rf.valid_special_chans = sorted(ID800_SPECIAL.keys())
+        rf.memory_bounds = (1, 499)
+        return rf
+
+    def process_mmap(self):
+        self._memobj = bitwise.parse(MEM_FORMAT, self._mmap)
+
+    def get_memory(self, number):
+        if isinstance(number, str):
+            try:
+                number = ID800_SPECIAL[number] + 1 # Because we subtract below
+            except KeyError:
+                raise errors.InvalidMemoryLocation("Unknown channel %s" % \
+                                                       number)
+
+        _mem = self._memobj.memory[number-1]
+        _flg = self._memobj.flags[number-1]
+
+        if MODES[_mem.mode] == "DV":
+            urcalls = self.get_urcall_list()
+            rptcalls = self.get_repeater_call_list()
+            mem = chirp_common.DVMemory()
+            mem.dv_urcall = urcalls[_mem.urcall]
+            mem.dv_rpt1call = rptcalls[_mem.rpt1call]
+            mem.dv_rpt2call = rptcalls[_mem.rpt2call]
+            mem.dv_code = _mem.digital_code
+        else:
+            mem = chirp_common.Memory()
+
+        mem.number = number
+        if _flg.empty:
+            mem.empty = True
+            return mem
+
+        mult = _mem.mult_flag and 6250 or 5000
+        mem.freq = _mem.freq * mult
+        mem.offset = _mem.offset * 5000
+        mem.duplex = DUPLEX[_mem.duplex]
+        mem.mode = MODES[_mem.mode]
+        mem.tmode = TMODES[_mem.tmode]
+        mem.rtone = chirp_common.TONES[_mem.rtone]
+        mem.ctone = chirp_common.TONES[_mem.ctone]
+        mem.dtcs = chirp_common.DTCS_CODES[_mem.dtcs]
+        mem.dtcs_polarity = DTCS_POL[_mem.dtcs_polarity]
+        mem.tuning_step = STEPS[_mem.tuning_step]
+        mem.name = get_name(_mem)
+
+        mem.skip = _flg.pskip and "P" or _flg.skip and "S" or ""
+
+        return mem
+
+    def set_memory(self, mem):
+        _mem = self._memobj.memory[mem.number-1]
+        _flg = self._memobj.flags[mem.number-1]
+
+        _flg.empty = mem.empty
+        if mem.empty:
+            self._set_bank(mem.number, None)
+            return
+
+        mult = chirp_common.is_fractional_step(mem.freq) and 6250 or 5000
+        _mem.mult_flag = mult == 6250
+        _mem.freq = mem.freq / mult
+        _mem.offset = mem.offset / 5000
+        _mem.duplex = DUPLEX.index(mem.duplex)
+        _mem.mode = MODES.index(mem.mode)
+        _mem.tmode = TMODES.index(mem.tmode)
+        _mem.rtone = chirp_common.TONES.index(mem.rtone)
+        _mem.ctone = chirp_common.TONES.index(mem.ctone)
+        _mem.dtcs = chirp_common.DTCS_CODES.index(mem.dtcs)
+        _mem.dtcs_polarity = DTCS_POL.index(mem.dtcs_polarity)
+        _mem.tuning_step = STEPS.index(mem.tuning_step)
+        set_name(_mem, mem.name)
+
+        _flg.pskip = mem.skip == "P"
+        _flg.skip = mem.skip == "S"
+
+        if mem.mode == "DV":
+            urcalls = self.get_urcall_list()
+            rptcalls = self.get_repeater_call_list()
+            if not isinstance(mem, chirp_common.DVMemory):
+                raise errors.InvalidDataError("DV mode is not a DVMemory!")
+            try:
+                err = mem.dv_urcall
+                _mem.urcall = urcalls.index(mem.dv_urcall)
+                err = mem.dv_rpt1call
+                _mem.rpt1call = rptcalls.index(mem.dv_rpt1call)
+                err = mem.dv_rpt2call
+                _mem.rpt2call = rptcalls.index(mem.dv_rpt2call)
+            except IndexError:
+                raise errors.InvalidDataError("DV Call %s not in list" % err)
+        else:
+            _mem.urcall = 0
+            _mem.rpt1call = 0
+            _mem.rpt2call = 0
+
+    def sync_in(self):
+        icf.IcomCloneModeRadio.sync_in(self)
+        self.process_mmap()
+
+    def get_raw_memory(self, number):
+        return repr(self._memobj.memory[number-1])
+
+    def get_urcall_list(self):
+        calls = ["CQCQCQ"]
+
+        for i in range(*self.URCALL_LIMIT):
+            calls.append(str(self._memobj.urcalls[i-1].call).rstrip())
+
+        return calls
+
+    def get_repeater_call_list(self):
+        calls = ["*NOTUSE*"]
+
+        for i in range(*self.RPTCALL_LIMIT):
+            calls.append(str(self._memobj.rptcalls[i-1].call).rstrip())
+
+        return calls
+
+    def get_mycall_list(self):
+        calls = []
+
+        for i in range(*self.MYCALL_LIMIT):
+            calls.append(str(self._memobj.mycalls[i-1].call).rstrip())
+
+        return calls
+    
+    def set_urcall_list(self, calls):
+        for i in range(*self.URCALL_LIMIT):
+            try:
+                call = calls[i].upper() # Skip the implicit CQCQCQ
+            except IndexError:
+                call = " " * 8
+            
+            self._memobj.urcalls[i-1].call = call.ljust(8)[:8]
+
+    def set_repeater_call_list(self, calls):
+        for i in range(*self.RPTCALL_LIMIT):
+            try:
+                call = calls[i].upper() # Skip the implicit blank
+            except IndexError:
+                call = " " * 8
+
+            self._memobj.rptcalls[i-1].call = call.ljust(8)[:8]
+        
+    def set_mycall_list(self, calls):
+        for i in range(*self.MYCALL_LIMIT):
+            try:
+                call = calls[i-1].upper()
+            except IndexError:
+                call = " " * 8
+
+            self._memobj.mycalls[i-1].call = call.ljust(8)[:8]
diff --git a/chirp/id880.py b/chirp/id880.py
new file mode 100644
index 0000000..b6c8b39
--- /dev/null
+++ b/chirp/id880.py
@@ -0,0 +1,396 @@
+# Copyright 2010 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/>.
+
+from chirp import chirp_common, icf, directory
+from chirp import bitwise
+
+MEM_FORMAT = """
+struct {
+  u24  freq;
+  u16  offset;
+  u16  rtone:6,
+       ctone:6,
+       unknown1:1,
+       mode:3;
+  u8   dtcs;
+  u8   tune_step:4,
+       unknown2:4;
+  u8   unknown3;
+  u8   unknown4:1,
+       tmode:3,
+       duplex:2,
+       dtcs_polarity:2;
+  char name[8];
+  u8   unknwon5:1,
+       digital_code:7;
+  char urcall[7];
+  char r1call[7];
+  char r2call[7];
+} memory[1000];
+
+#seekto 0xAA80;
+u8 used_flags[132];
+
+#seekto 0xAB04;
+u8 skip_flags[132];
+u8 pskip_flags[132];
+
+#seekto 0xAD00;
+struct {
+  u8 bank;
+  u8 index;
+} bank_info[1000];
+
+#seekto 0xB550;
+struct {
+  char name[6];
+} bank_names[26];
+
+#seekto 0xDE56;
+struct {
+  char call[8];
+  char extension[4];
+} mycall[6];
+
+struct {
+  char call[8];
+} urcall[60];
+
+struct {
+  char call[8];
+  char extension[4];
+} rptcall[99];
+
+#seekto 0x0FB8;
+u8 name_flags[132];
+
+"""
+
+TMODES = ["", "Tone", "?2", "TSQL", "DTCS", "TSQL-R", "DTCS-R", ""]
+DUPLEX = ["", "-", "+", "?3"]
+DTCSP  = ["NN", "NR", "RN", "RR"]
+MODES  = ["FM", "NFM", "?2", "AM", "NAM", "DV"]
+STEPS  = [5.0, 6.25, 8.33, 9.0, 10.0, 12.5, 15.0, 20.0, 25.0, 30.0, 50.0,
+          100.0, 125.0, 200.0]
+
+def decode_call(sevenbytes):
+    """Decode a callsign from a packed region @sevenbytes"""
+    if len(sevenbytes) != 7:
+        raise Exception("%i (!=7) bytes to decode_call" % len(sevenbytes))
+
+    i = 0
+    rem = 0
+    call = ""
+    for byte in [ord(x) for x in sevenbytes]:
+        i += 1
+
+        mask = (1 << i) - 1           # Mask is 0x01, 0x03, 0x07, etc
+
+        code = (byte >> i) | rem      # Code gets the upper bits of remainder
+                                      # plus all but the i lower bits of this
+                                      # byte
+        call += chr(code)
+
+        rem = (byte & mask) << 7 - i  # Remainder for next time are the masked
+                                      # bits, moved to the high places for the
+                                      # next round
+
+    # After seven trips gathering overflow bits, we chould have seven
+    # left, which is the final character
+    call += chr(rem)
+
+    return call.rstrip()
+
+def encode_call(call):
+    """Encode @call into a 7-byte region"""
+    call = call.ljust(8)
+    buf = []
+    
+    for i in range(0, 8):
+        byte = ord(call[i])
+        if i > 0:
+            last = buf[i-1]
+            himask = ~((1 << (7-i)) - 1) & 0x7F
+            last |= (byte & himask) >> (7-i)
+            buf[i-1] = last
+        else:
+            himask = 0
+
+        buf.append((byte & ~himask) << (i+1))
+
+    return "".join([chr(x) for x in buf[:7]])
+
+def _get_freq(_mem):
+    val = int(_mem.freq)
+
+    if val & 0x00200000:
+        mult = 6250
+    else:
+        mult = 5000
+
+    val &= 0x0003FFFF
+
+    return (val * mult)
+
+def _set_freq(_mem, freq):
+    if chirp_common.is_fractional_step(freq):
+        mult = 6250
+        flag = 0x00200000
+    else:
+        mult = 5000
+        flag = 0x00000000
+
+    _mem.freq = (freq / mult) | flag
+
+def _wipe_memory(mem, char):
+    mem.set_raw(char * (mem.size() / 8))
+
+class ID880Bank(icf.IcomNamedBank):
+    """ID880 Bank"""
+    def get_name(self):
+        _bank = self._model._radio._memobj.bank_names[self.index]
+        return str(_bank.name).rstrip()
+
+    def set_name(self, name):
+        _bank = self._model._radio._memobj.bank_names[self.index]
+        _bank.name = name.ljust(6)[:6]
+
+ at directory.register
+class ID880Radio(icf.IcomCloneModeRadio, chirp_common.IcomDstarSupport):
+    """Icom ID880"""
+    VENDOR = "Icom"
+    MODEL = "ID-880H"
+
+    _model = "\x31\x67\x00\x01"
+    _memsize = 62976
+    _endframe = "Icom Inc\x2eB1"
+
+    _ranges = [(0x0000, 0xF5c0, 32),
+               (0xF5c0, 0xf5e0, 16),
+               (0xf5e0, 0xf600, 32)]
+
+    _num_banks = 26
+    _bank_class = ID880Bank
+    _can_hispeed = True
+
+    MYCALL_LIMIT = (1, 7)
+    URCALL_LIMIT = (1, 60)
+    RPTCALL_LIMIT = (1, 99)
+
+    def _get_bank(self, loc):
+        _bank = self._memobj.bank_info[loc]
+        if _bank.bank == 0xFF:
+            return None
+        else:
+            return _bank.bank
+
+    def _set_bank(self, loc, bank):
+        _bank = self._memobj.bank_info[loc]
+        if bank is None:
+            _bank.bank = 0xFF
+        else:
+            _bank.bank = bank
+
+    def _get_bank_index(self, loc):
+        _bank = self._memobj.bank_info[loc]
+        return _bank.index
+        
+    def _set_bank_index(self, loc, index):
+        _bank = self._memobj.bank_info[loc]
+        _bank.index = index
+
+    def process_mmap(self):
+        self._memobj = bitwise.parse(MEM_FORMAT, self._mmap)
+
+    def get_features(self):
+        rf = chirp_common.RadioFeatures()
+        rf.requires_call_lists = False
+        rf.has_settings = True
+        rf.has_bank = True
+        rf.has_bank_index = True
+        rf.has_bank_names = True
+        rf.valid_modes = [x for x in MODES if x is not None]
+        rf.valid_tmodes = list(TMODES)
+        rf.valid_duplexes = list(DUPLEX)
+        rf.valid_tuning_steps = STEPS
+        rf.valid_bands = [(118000000, 173995000), (230000000, 549995000),
+                          (810000000, 823990000), (849000000, 868990000),
+                          (894000000, 999990000)]
+        rf.valid_skips = ["", "S", "P"]
+        rf.valid_name_length = 8
+        rf.valid_characters = chirp_common.CHARSET_UPPER_NUMERIC + \
+                              "!\"#$%&'()*+,-./:;<=>?@[\]^"
+        rf.memory_bounds = (0, 999)
+        return rf
+
+    def get_raw_memory(self, number):
+        return repr(self._memobj.memory[number])
+
+    def get_memory(self, number):
+        bytepos = number / 8
+        bitpos = 1 << (number % 8)
+
+        _mem = self._memobj.memory[number]
+        _used = self._memobj.used_flags[bytepos]
+
+        is_used = ((_used & bitpos) == 0)
+
+        if is_used and MODES[_mem.mode] == "DV":
+            mem = chirp_common.DVMemory()
+            mem.dv_urcall = decode_call(str(_mem.urcall))
+            mem.dv_rpt1call = decode_call(str(_mem.r1call))
+            mem.dv_rpt2call = decode_call(str(_mem.r2call))
+        else:
+            mem = chirp_common.Memory()
+
+        mem.number = number
+
+        if number < 1000:
+            _skip = self._memobj.skip_flags[bytepos]
+            _pskip = self._memobj.pskip_flags[bytepos]
+            if _skip & bitpos:
+                mem.skip = "S"
+            elif _pskip & bitpos:
+                mem.skip = "P"
+        else:
+            pass # FIXME: Special memories
+
+        if not is_used:
+            mem.empty = True
+            return mem
+
+        mem.freq = _get_freq(_mem)
+        mem.offset = (_mem.offset * 5) * 1000
+        mem.rtone = chirp_common.TONES[_mem.rtone]
+        mem.ctone = chirp_common.TONES[_mem.ctone]
+        mem.tmode = TMODES[_mem.tmode]
+        mem.duplex = DUPLEX[_mem.duplex]
+        mem.mode = MODES[_mem.mode]
+        mem.dtcs = chirp_common.DTCS_CODES[_mem.dtcs]
+        mem.dtcs_polarity = DTCSP[_mem.dtcs_polarity]
+        if _mem.tune_step >= len(STEPS):
+            mem.tuning_step = 5.0
+        else:
+            mem.tuning_step = STEPS[_mem.tune_step]
+        mem.name = str(_mem.name).rstrip()
+
+        return mem
+
+    def set_memory(self, mem):
+        bitpos = (1 << (mem.number % 8))
+        bytepos = mem.number / 8
+
+        _mem = self._memobj.memory[mem.number]
+        _used = self._memobj.used_flags[bytepos]
+        _namf = self._memobj.name_flags[bytepos]
+
+        was_empty = _used & bitpos
+
+        if mem.empty:
+            _used |= bitpos
+            _wipe_memory(_mem, "\xFF")
+            self._set_bank(mem.number, None)
+            return
+
+        _used &= ~bitpos
+
+        if was_empty:
+            _wipe_memory(_mem, "\x00")
+
+        _set_freq(_mem, mem.freq)
+        _mem.offset = int((mem.offset / 1000) / 5)
+        _mem.rtone = chirp_common.TONES.index(mem.rtone)
+        _mem.ctone = chirp_common.TONES.index(mem.ctone)
+        _mem.tmode = TMODES.index(mem.tmode)
+        _mem.duplex = DUPLEX.index(mem.duplex)
+        _mem.mode = MODES.index(mem.mode)
+        _mem.dtcs = chirp_common.DTCS_CODES.index(mem.dtcs)
+        _mem.dtcs_polarity = DTCSP.index(mem.dtcs_polarity)
+        _mem.tune_step = STEPS.index(mem.tuning_step)
+        _mem.name = mem.name.ljust(8)
+        if mem.name.strip():
+            _namf |= bitpos
+        else:
+            _namf &= ~bitpos
+
+        if isinstance(mem, chirp_common.DVMemory):
+            _mem.urcall = encode_call(mem.dv_urcall)
+            _mem.r1call = encode_call(mem.dv_rpt1call)
+            _mem.r2call = encode_call(mem.dv_rpt2call)
+            
+        if mem.number < 1000:
+            skip = self._memobj.skip_flags[bytepos]
+            pskip = self._memobj.pskip_flags[bytepos]
+            if mem.skip == "S":
+                skip |= bitpos
+            else:
+                skip &= ~bitpos
+            if mem.skip == "P":
+                pskip |= bitpos
+            else:
+                pskip &= ~bitpos
+
+    def get_urcall_list(self):
+        _calls = self._memobj.urcall
+        calls = ["CQCQCQ"]
+
+        for i in range(*self.URCALL_LIMIT):
+            calls.append(str(_calls[i-1].call))
+
+        return calls
+
+    def get_mycall_list(self):
+        _calls = self._memobj.mycall
+        calls = []
+
+        for i in range(*self.MYCALL_LIMIT):
+            calls.append(str(_calls[i-1].call))
+
+        return calls
+
+    def get_repeater_call_list(self):
+        _calls = self._memobj.rptcall
+        calls = ["*NOTUSE*"]
+
+        for _i in range(*self.RPTCALL_LIMIT):
+            # FIXME: Not sure where the repeater list actually is
+            calls.append("UNSUPRTD")
+            continue
+
+        return calls
+
+    @classmethod
+    def match_model(cls, filedata, filename):
+        # This is a horrid hack, given that people can change the GPS-A
+        # destination, but it should suffice in most cases until we get
+        # a rich container file format
+        return len(filedata) == cls._memsize and "API880," in filedata
+        
+# This radio isn't really supported yet and detects as a conflict with
+# the ID-880. So, don't register right now
+ at directory.register
+class ID80Radio(ID880Radio):
+    """Icom ID80"""
+    MODEL = "ID-80H"
+
+    _model = "\x31\x55\x00\x01"
+    
+    @classmethod
+    def match_model(cls, filedata, filename):
+        # This is a horrid hack, given that people can change the GPS-A
+        # destination, but it should suffice in most cases until we get
+        # a rich container file format
+        return len(filedata) == cls._memsize and "API80," in filedata
+        
diff --git a/chirp/idrp.py b/chirp/idrp.py
new file mode 100644
index 0000000..e20ba07
--- /dev/null
+++ b/chirp/idrp.py
@@ -0,0 +1,166 @@
+# 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 serial
+
+from chirp import chirp_common, errors
+from chirp import util
+
+DEBUG_IDRP = False
+
+def parse_frames(buf):
+    """Parse frames from the radio"""
+    frames = []
+
+    while "\xfe\xfe" in buf:
+        try:
+            start = buf.index("\xfe\xfe")
+            end = buf[start:].index("\xfd") + start + 1
+        except Exception:
+            print "Unable to parse frames"
+            break
+
+        frames.append(buf[start:end])
+        buf = buf[end:]
+
+    return frames
+
+def send(pipe, buf):
+    """Send data in @buf to @pipe"""
+    pipe.write("\xfe\xfe%s\xfd" % buf)
+    pipe.flush()
+
+    data = ""
+    while True:
+        buf = pipe.read(4096)
+        if not buf:
+            break
+
+        data += buf
+        if DEBUG_IDRP:
+            print "Got: \n%s" % util.hexprint(buf)
+
+    return parse_frames(data)
+
+def send_magic(pipe):
+    """Send the magic wakeup call to @pipe"""
+    send(pipe, ("\xfe" * 15) + "\x01\x7f\x19")
+
+def drain(pipe):
+    """Chew up any data waiting on @pipe"""
+    while True:
+        buf = pipe.read(4096)
+        if not buf:
+            break
+
+def set_freq(pipe, freq):
+    """Set the frequency of the radio on @pipe to @freq"""
+    freqbcd = util.bcd_encode(freq, bigendian=False, width=9)
+    buf = "\x01\x7f\x05" + freqbcd
+
+    drain(pipe)
+    send_magic(pipe)
+    resp = send(pipe, buf)
+    for frame in resp:
+        if len(frame) == 6:
+            if frame[4] == "\xfb":
+                return True
+
+    raise errors.InvalidDataError("Repeater reported error")
+    
+def get_freq(pipe):
+    """Get the frequency of the radio attached to @pipe"""
+    buf = "\x01\x7f\x1a\x09"
+
+    drain(pipe)
+    send_magic(pipe)
+    resp = send(pipe, buf)
+
+    for frame in resp:
+        if frame[4] == "\x03":
+            els = frame[5:10]
+
+            freq = int("%02x%02x%02x%02x%02x" % (ord(els[4]),
+                                                 ord(els[3]),
+                                                 ord(els[2]),
+                                                 ord(els[1]),
+                                                 ord(els[0])))
+            if DEBUG_IDRP:
+                print "Freq: %f" % freq
+            return freq
+
+    raise errors.InvalidDataError("No frequency frame received")
+
+RP_IMMUTABLE = ["number", "skip", "bank", "extd_number", "name", "rtone",
+                "ctone", "dtcs", "tmode", "dtcs_polarity", "skip", "duplex",
+                "offset", "mode", "tuning_step", "bank_index"]
+
+class IDRPx000V(chirp_common.LiveRadio):
+    """Icom IDRP-*"""
+    BAUD_RATE = 19200
+    VENDOR = "Icom"
+    MODEL = "ID-2000V/4000V/2D/2V"
+
+    _model = "0000" # Unknown
+    mem_upper_limit = 0
+        
+    def get_features(self):
+        rf = chirp_common.RadioFeatures()
+        rf.valid_modes = ["DV"]
+        rf.valid_tmodes = []
+        rf.valid_characters = ""
+        rf.valid_duplexes = [""]
+        rf.valid_name_length = 0
+        rf.valid_skips = []
+        rf.valid_tuning_steps = []
+        rf.has_bank = False
+        rf.has_ctone = False
+        rf.has_dtcs = False
+        rf.has_dtcs_polarity = False
+        rf.has_mode = False
+        rf.has_name = False
+        rf.has_offset = False
+        rf.has_tuning_step = False        
+        rf.memory_bounds = (0, 0)
+        return rf
+
+    def get_memory(self, number):
+        if number != 0:
+            raise errors.InvalidMemoryLocation("Repeaters have only one slot")
+
+        mem = chirp_common.Memory()
+        mem.number = 0
+        mem.freq = get_freq(self.pipe)
+        mem.name = "TX/RX"
+        mem.mode = "DV"
+        mem.offset = 0.0
+        mem.immutable = RP_IMMUTABLE
+
+        return mem
+
+    def set_memory(self, mem):
+        if mem.number != 0:
+            raise errors.InvalidMemoryLocation("Repeaters have only one slot")
+
+        set_freq(self.pipe, mem.freq)
+
+def do_test():
+    """Get the frequency of /dev/icom"""
+    ser = serial.Serial(port="/dev/icom", baudrate=19200, timeout=0.5)
+    #set_freq(pipe, 439.920)
+    get_freq(ser)
+
+if __name__ == "__main__":
+    do_test()
diff --git a/chirp/import_logic.py b/chirp/import_logic.py
new file mode 100644
index 0000000..a445e9b
--- /dev/null
+++ b/chirp/import_logic.py
@@ -0,0 +1,243 @@
+# Copyright 2011 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/>.
+
+from chirp import chirp_common, errors
+
+class ImportError(Exception):
+    """An import error"""
+    pass
+
+class DestNotCompatible(ImportError):
+    """Memory is not compatible with the destination radio"""
+    pass
+
+def ensure_has_calls(radio, memory):
+    """Make sure @radio has the necessary D-STAR callsigns for @memory"""
+    ulist_changed = rlist_changed = False
+
+    ulist = radio.get_urcall_list()
+    rlist = radio.get_repeater_call_list()
+
+    if memory.dv_urcall and memory.dv_urcall not in ulist:
+        for i in range(0, len(ulist)):
+            if not ulist[i].strip():
+                ulist[i] = memory.dv_urcall
+                ulist_changed = True
+                break
+        if not ulist_changed:
+            raise ImportError("No room to add callsign %s" % memory.dv_urcall)
+
+    rlist_add = []
+    if memory.dv_rpt1call and memory.dv_rpt1call not in rlist:
+        rlist_add.append(memory.dv_rpt1call)
+    if memory.dv_rpt2call and memory.dv_rpt2call not in rlist:
+        rlist_add.append(memory.dv_rpt2call)
+
+    while rlist_add:
+        call = rlist_add.pop()
+        for i in range(0, len(rlist)):
+            if not rlist[i].strip():
+                rlist[i] = call
+                call = None
+                rlist_changed = True
+                break
+        if call:
+            raise errors.RadioError("No room to add callsign %s" % call)
+
+    if ulist_changed:
+        radio.set_urcall_list(ulist)
+    if rlist_changed:
+        radio.set_repeater_cal_list(rlist)
+
+# Filter the name according to the destination's rules
+def _import_name(dst_radio, _srcrf, mem):
+    mem.name = dst_radio.filter_name(mem.name)
+
+def _import_power(dst_radio, _srcrf, mem):
+    levels = dst_radio.get_features().valid_power_levels
+    if not levels:
+        mem.power = None
+        return
+    elif mem.power is None:
+        # Source radio did not support power levels, so choose the
+        # first (highest) level from the destination radio.
+        mem.power = levels[0]
+        return 
+
+    # If both radios support power levels, we need to decide how to
+    # convert the source power level to a valid one for the destination
+    # radio.  To do that, find the absolute level of the source value
+    # and calculate the different between it and all the levels of the
+    # destination, choosing the one that matches most closely.
+
+    deltas = [abs(mem.power - power) for power in levels]
+    mem.power = levels[deltas.index(min(deltas))]
+
+def _import_tone(dst_radio, srcrf, mem):
+    dstrf = dst_radio.get_features()
+
+    # Some radios keep separate tones for Tone and TSQL modes (rtone and
+    # ctone). If we're importing to or from radios with differing models,
+    # do the conversion here.
+
+    if srcrf.has_ctone and not dstrf.has_ctone:
+        # If copying from a radio with separate rtone/ctone to a radio
+        # without, and the tmode is TSQL, then use the ctone value
+        if mem.tmode == "TSQL":
+            mem.rtone = mem.ctone
+    elif not srcrf.has_ctone and dstrf.has_ctone:
+        # If copying from a radio without separate rtone/ctone to a radio
+        # with it, set the dest ctone to the src rtone
+        if mem.tmode == "TSQL":
+            mem.ctone = mem.rtone
+
+def _import_dtcs(dst_radio, srcrf, mem):
+    dstrf = dst_radio.get_features()
+
+    # Some radios keep separate DTCS codes for tx and rx
+    # If we're importing to or from radios with differing models,
+    # do the conversion here.
+
+    if srcrf.has_rx_dtcs and not dstrf.has_rx_dtcs:
+        # If copying from a radio with separate codes to a radio
+        # without, and the tmode is DTCS, then use the rx_dtcs value
+        if mem.tmode == "DTCS":
+            mem.dtcs = mem.rx_dtcs
+    elif not srcrf.has_rx_dtcs and dstrf.has_rx_dtcs:
+        # If copying from a radio without separate codes to a radio
+        # with it, set the dest rx_dtcs to the src dtcs
+        if mem.tmode == "DTCS":
+            mem.rx_dtcs = mem.dtcs
+
+def _guess_mode_by_frequency(freq):
+    ranges = [
+        (0, 135, "AM"),
+        (136, 9999, "FM"),
+        ]
+
+    for lo, hi, mode in ranges:
+        if freq > lo and freq <= hi:
+            return mode
+
+    # If we don't know, assume FM
+    return "FM"
+
+def _import_mode(dst_radio, srcrf, mem):
+    dstrf = dst_radio.get_features()
+
+    # Some radios support an "Auto" mode. If we're importing from one
+    # that does to one that does not, guess at the proper mode based on the
+    # frequency
+
+    if mem.mode == "Auto" and mem.mode not in dstrf.valid_modes:
+        mem.mode = _guess_mode_by_frequency(mem.freq)
+
+def _make_offset_with_split(rxfreq, txfreq):
+    offset = txfreq - rxfreq
+    
+    if offset == 0:
+        return "", offset
+    elif offset > 0:
+        return "+", offset
+    elif offset < 0:
+        return "-", offset * -1
+
+def _import_duplex(dst_radio, srcrf, mem):
+    dstrf = dst_radio.get_features()
+
+    # If a radio does not support odd split, we can use an equivalent offset
+    if mem.duplex == "split" and mem.duplex not in dstrf.valid_duplexes:
+        mem.duplex, mem.offset = _make_offset_with_split(mem.freq, mem.offset)
+        
+        # Enforce maximum offset
+        ranges = [
+            (        0,  500000000,  7000000),
+            (500000000, 3000000000, 50000000),
+        ]
+        for lo, hi, limit in ranges:
+            if lo < mem.freq <= hi:
+                if abs(mem.offset) > limit:
+                    raise DestNotCompatible("Unable to create import memory: "
+                                            "offset is abnormally large.")
+
+def import_mem(dst_radio, src_features, src_mem, overrides={}):
+    """Perform import logic to create a destination memory from
+    src_mem that will be compatible with @dst_radio"""
+    dst_rf = dst_radio.get_features()
+
+    if isinstance(src_mem, chirp_common.DVMemory):
+        if not isinstance(dst_radio, chirp_common.IcomDstarSupport):
+            raise DestNotCompatible("Destination radio does not support D-STAR")
+        if dst_rf.requires_call_lists:
+            ensure_has_calls(dst_radio, src_mem)
+
+    dst_mem = src_mem.dupe()
+
+    for k, v in overrides.items():
+        dst_mem.__dict__[k] = v
+
+    helpers = [_import_name,
+               _import_power,
+               _import_tone,
+               _import_dtcs,
+               _import_mode,
+               _import_duplex,
+               ]
+
+    for helper in helpers:
+        helper(dst_radio, src_features, dst_mem)
+
+    msgs = dst_radio.validate_memory(dst_mem)
+    errs = [x for x in msgs if isinstance(x, chirp_common.ValidationError)]
+    if errs:
+        raise DestNotCompatible("Unable to create import memory: %s" %\
+                                    ", ".join(errs))
+
+    return dst_mem
+        
+def import_bank(dst_radio, src_radio, dst_mem, src_mem):
+    """Attempt to set the same banks for @mem(by index) in @dst_radio that
+    it has in @src_radio"""
+
+    dst_bm = dst_radio.get_bank_model()
+    if not dst_bm:
+        return
+
+    dst_banks = dst_bm.get_banks()
+
+    src_bm = src_radio.get_bank_model()
+    if not src_bm:
+        return
+
+    src_banks = src_bm.get_banks()
+    src_mem_banks = src_bm.get_memory_banks(src_mem)
+    src_indexes = [src_banks.index(b) for b in src_mem_banks]
+
+    for bank in dst_bm.get_memory_banks(dst_mem):
+        dst_bm.remove_memory_from_bank(dst_mem, bank)
+
+    for index in src_indexes:
+        try:
+            bank = dst_banks[index]
+            print "Adding memory to bank %s" % bank
+            dst_bm.add_memory_to_bank(dst_mem, bank)
+            if isinstance(dst_bm, chirp_common.BankIndexInterface):
+                dst_bm.set_memory_index(dst_mem, bank,
+                                        dst_bm.get_next_bank_index(bank))
+
+        except IndexError:
+            pass
+
+    
diff --git a/chirp/kenwood_hmk.py b/chirp/kenwood_hmk.py
new file mode 100644
index 0000000..de52f68
--- /dev/null
+++ b/chirp/kenwood_hmk.py
@@ -0,0 +1,122 @@
+# 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/>.
+
+import csv
+
+from chirp import chirp_common, errors, directory, generic_csv
+
+class OmittedHeaderError(Exception):
+    """An internal exception to indicate that a header was omitted"""
+    pass
+
+ at directory.register
+class HMKRadio(generic_csv.CSVRadio):
+    """Kenwood HMK format"""
+    VENDOR = "Kenwood"
+    MODEL = "HMK"
+    FILE_EXTENSION = "hmk"
+
+    DUPLEX_MAP = {
+        " ":    "",
+        "S":    "split",
+        "+":    "+",
+        "-":    "-",
+    }
+
+    SKIP_MAP = {
+        "Off":  "",
+        "On":   "S",
+        }
+
+    TMODE_MAP = {
+        "Off":  "",
+        "T":    "Tone",
+        "CT":   "TSQL",
+        "DCS":  "DTCS",
+        "":     "Cross",
+        }
+
+    ATTR_MAP = {
+        "!!Ch"         : (int,   "number"),
+        "M.Name"       : (str,   "name"),
+        "Rx Freq."     : (chirp_common.parse_freq, "freq"),
+        "Shift/Split"  : (lambda v: HMKRadio.DUPLEX_MAP[v], "duplex"),
+        "Offset"       : (chirp_common.parse_freq, "offset"),
+        "T/CT/DCS"     : (lambda v: HMKRadio.TMODE_MAP[v], "tmode"),
+        "TO Freq."     : (float, "rtone"),
+        "CT Freq."     : (float, "ctone"),
+        "DCS Code"     : (int,   "dtcs"),
+        "Mode"         : (str,   "mode"),
+        "Rx Step"      : (float, "tuning_step"),
+        "L.Out"        : (lambda v: HMKRadio.SKIP_MAP[v], "skip"),
+        }
+
+    def load(self, filename=None):
+        if filename is None and self._filename is None:
+            raise errors.RadioError("Need a location to load from")
+
+        if filename:
+            self._filename = filename
+
+        self._blank()
+
+        f = file(self._filename, "r")
+        for line in f:
+            if line.strip() == "// Memory Channels":
+                break
+
+        reader = csv.reader(f, delimiter=chirp_common.SEPCHAR, quotechar='"')
+
+        good = 0
+        lineno = 0
+        for line in reader:
+            lineno += 1
+            if lineno == 1:
+                header = line
+                continue
+
+            if len(header) > len(line):
+                print "Line %i has %i columns, expected %i" % (lineno,
+                                                               len(line),
+                                                               len(header))
+                self.errors.append("Column number mismatch on line %i" % lineno)
+                continue
+
+            # hmk stores Tx Freq. in its own field, but Chirp expects the Tx
+            # Freq. for odd-split channels to be in the Offset field.
+            # If channel is odd-split, copy Tx Freq. field to Offset field.
+            if line[header.index('Shift/Split')] == "S":
+                line[header.index('Offset')] = line[header.index('Tx Freq.')]
+
+            # fix EU decimal
+            line = [i.replace(',','.') for i in line]
+
+            try:
+                mem = self._parse_csv_data_line(header, line)
+                if mem.number is None:
+                    raise Exception("Invalid Location field" % lineno)
+            except Exception, e:
+                print "Line %i: %s" % (lineno, e)
+                self.errors.append("Line %i: %s" % (lineno, e))
+                continue
+
+            self._grow(mem.number)
+            self.memories[mem.number] = mem
+            good += 1
+
+        if not good:
+            print self.errors
+            raise errors.InvalidDataError("No channels found")
diff --git a/chirp/kenwood_live.py b/chirp/kenwood_live.py
new file mode 100644
index 0000000..d66a73f
--- /dev/null
+++ b/chirp/kenwood_live.py
@@ -0,0 +1,1118 @@
+# Copyright 2010 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 threading
+import os
+import sys
+import time
+
+NOCACHE = os.environ.has_key("CHIRP_NOCACHE")
+
+if __name__ == "__main__":
+    import sys
+    sys.path.insert(0, "..")
+
+from chirp import chirp_common, errors, directory, util
+from chirp.settings import RadioSetting, RadioSettingGroup, \
+    RadioSettingValueInteger, RadioSettingValueBoolean, \
+    RadioSettingValueString, RadioSettingValueList
+
+DEBUG = True
+
+DUPLEX = { 0 : "", 1 : "+", 2 : "-" }
+MODES = { 0 : "FM", 1 : "AM" }
+STEPS = list(chirp_common.TUNING_STEPS)
+STEPS.append(100.0)
+
+THF6_MODES = ["FM", "WFM", "AM", "LSB", "USB", "CW"]
+
+LOCK = threading.Lock()
+
+def command(ser, cmd, *args):
+    """Send @cmd to radio via @ser"""
+    global LOCK
+
+    start = time.time()
+
+    LOCK.acquire()
+    if args:
+        cmd += " " + " ".join(args)
+    if DEBUG:
+        print "PC->RADIO: %s" % cmd
+    ser.write(cmd + "\r")
+
+    result = ""
+    while not result.endswith("\r"):
+        result += ser.read(8)
+        if (time.time() - start) > 0.5:
+            print "Timeout waiting for data"
+            break
+
+    if DEBUG:
+        print "D7->PC: %s" % result.strip()
+
+    LOCK.release()
+
+    return result.strip()
+
+LAST_BAUD = 9600
+def get_id(ser):
+    """Get the ID of the radio attached to @ser"""
+    global LAST_BAUD
+    bauds = [9600, 19200, 38400, 57600]
+    bauds.remove(LAST_BAUD)
+    bauds.insert(0, LAST_BAUD)
+
+    for i in bauds:
+        print "Trying ID at baud %i" % i
+        ser.setBaudrate(i)
+        ser.write("\r")
+        ser.read(25)
+        resp = command(ser, "ID")
+        if " " in resp:
+            LAST_BAUD = i
+            return resp.split(" ")[1]
+
+    raise errors.RadioError("No response from radio")
+
+def get_tmode(tone, ctcss, dcs):
+    """Get the tone mode based on the values of the tone, ctcss, dcs"""
+    if dcs and int(dcs) == 1:
+        return "DTCS"
+    elif int(ctcss):
+        return "TSQL"
+    elif int(tone):
+        return "Tone"
+    else:
+        return ""
+
+def iserr(result):
+    """Returns True if the @result from a radio is an error"""
+    return result in ["N", "?"]
+
+class KenwoodLiveRadio(chirp_common.LiveRadio):
+    """Base class for all live-mode kenwood radios"""
+    BAUD_RATE = 9600
+    VENDOR = "Kenwood"
+    MODEL = ""
+
+    _vfo = 0
+    _upper = 200
+    _kenwood_split = False
+    _kenwood_valid_tones = list(chirp_common.TONES)
+
+    def __init__(self, *args, **kwargs):
+        chirp_common.LiveRadio.__init__(self, *args, **kwargs)
+
+        self.__memcache = {}
+
+        if self.pipe:
+            self.pipe.setTimeout(0.1)
+            radio_id = get_id(self.pipe)
+            if radio_id != self.MODEL.split(" ")[0]:
+                raise Exception("Radio reports %s (not %s)" % (radio_id,
+                                                               self.MODEL))
+
+            command(self.pipe, "AI", "0")
+
+    def _cmd_get_memory(self, number):
+        return "MR", "%i,0,%03i" % (self._vfo, number)
+
+    def _cmd_get_memory_name(self, number):
+        return "MNA", "%i,%03i" % (self._vfo, number)
+
+    def _cmd_get_split(self, number):
+        return "MR", "%i,1,%03i" % (self._vfo, number)
+
+    def _cmd_set_memory(self, number, spec):
+        if spec:
+            spec = "," + spec
+        return "MW", "%i,0,%03i%s" % (self._vfo, number, spec)
+
+    def _cmd_set_memory_name(self, number, name):
+        return "MNA", "%i,%03i,%s" % (self._vfo, number, name)
+
+    def _cmd_set_split(self, number, spec):
+        return "MW", "%i,1,%03i,%s" % (self._vfo, number, spec)
+
+    def get_raw_memory(self, number):
+        return command(self.pipe, *self._cmd_get_memory(number))
+
+    def get_memory(self, number):
+        if number < 0 or number > self._upper:
+            raise errors.InvalidMemoryLocation( \
+                "Number must be between 0 and %i" % self._upper)
+        if self.__memcache.has_key(number) and not NOCACHE:
+            return self.__memcache[number]
+
+        result = command(self.pipe, *self._cmd_get_memory(number))
+        if result == "N":
+            mem = chirp_common.Memory()
+            mem.number = number
+            mem.empty = True
+            self.__memcache[mem.number] = mem
+            return mem
+        elif " " not in result:
+            print "Not sure what to do with this: `%s'" % result
+            raise errors.RadioError("Unexpected result returned from radio")
+
+        value = result.split(" ")[1]
+        spec = value.split(",")
+
+        mem = self._parse_mem_spec(spec)
+        self.__memcache[mem.number] = mem
+
+        result = command(self.pipe, *self._cmd_get_memory_name(number))
+        if " " in result:
+            value = result.split(" ", 1)[1]
+            if value.count(",") == 2:
+                _zero, _loc, mem.name = value.split(",")
+            else:
+                _loc, mem.name = value.split(",")
+ 
+        if mem.duplex == "" and self._kenwood_split:
+            result = command(self.pipe, *self._cmd_get_split(number))
+            if " " in result:
+                value = result.split(" ", 1)[1]
+                self._parse_split_spec(mem, value.split(","))
+
+        return mem
+
+    def _make_mem_spec(self, mem):
+        pass
+
+    def _parse_mem_spec(self, spec):
+        pass
+
+    def _parse_split_spec(self, mem, spec):
+        mem.duplex = "split"
+        mem.offset = int(spec[2])
+
+    def _make_split_spec(self, mem):
+        return ("%011i" % mem.offset, "0")
+
+    def set_memory(self, memory):
+        if memory.number < 0 or memory.number > self._upper:
+            raise errors.InvalidMemoryLocation( \
+                "Number must be between 0 and %i" % self._upper)
+
+        spec = self._make_mem_spec(memory)
+        spec = ",".join(spec)
+        r1 = command(self.pipe, *self._cmd_set_memory(memory.number, spec))
+        if not iserr(r1):
+            time.sleep(0.5)
+            r2 = command(self.pipe, *self._cmd_set_memory_name(memory.number,
+                                                               memory.name))
+            if not iserr(r2):
+                memory.name = memory.name.rstrip()
+                self.__memcache[memory.number] = memory
+            else:
+                raise errors.InvalidDataError("Radio refused name %i: %s" %\
+                                                  (memory.number,
+                                                   repr(memory.name)))
+        else:
+            raise errors.InvalidDataError("Radio refused %i" % memory.number)
+
+        if memory.duplex == "split" and self._kenwood_split: 
+            spec = ",".join(self._make_split_spec(memory))
+            result = command(self.pipe, *self._cmd_set_split(memory.number,
+                                                             spec))
+            if iserr(result):
+                raise errors.InvalidDataError("Radio refused %i" % \
+                                                  memory.number)
+
+    def erase_memory(self, number):
+        if not self.__memcache.has_key(number):
+            return
+
+        resp = command(self.pipe, *self._cmd_set_memory(number, ""))
+        if iserr(resp):
+            raise errors.RadioError("Radio refused delete of %i" % number)
+        del self.__memcache[number]
+
+TH_D7_SETTINGS = {
+    "BAL"  : ["4:0", "3:1", "2:2", "1:3", "0:4"],
+    "BEP"  : ["Off", "Key", "Key+Data", "All"],
+    "BEPT" : ["Off", "Mine", "All New"], # D700 has fourth "All"
+    "DS"   : ["Data Band", "Both Bands"],
+    "DTB"  : ["A", "B"],
+    "DTBA" : ["A", "B", "A:TX/B:RX"], # D700 has fourth A:RX/B:TX
+    "DTX"  : ["Manual", "PTT", "Auto"],
+    "ICO"  : ["Kenwood", "Runner", "House", "Tent", "Boat", "SSTV",
+              "Plane", "Speedboat", "Car", "Bicycle"],
+    "MNF"  : ["Name", "Frequency"],
+    "PKSA" : ["1200", "9600"],
+    "POSC" : ["Off Duty", "Enroute", "In Service", "Returning",
+              "Committed", "Special", "Priority", "Emergency"],
+    "PT"   : ["100ms", "200ms", "500ms", "750ms", "1000ms", "1500ms", "2000ms"],
+    "SCR"  : ["Time", "Carrier", "Seek"],
+    "SV"   : ["Off", "0.2s", "0.4s", "0.6s", "0.8s", "1.0s",
+              "2s", "3s", "4s", "5s"],
+    "TEMP" : ["F", "C"],
+    "TXI"  : ["30sec", "1min", "2min", "3min", "4min", "5min",
+              "10min", "20min", "30min"],
+    "UNIT" : ["English", "Metric"],
+    "WAY"  : ["Off", "6 digit NMEA", "7 digit NMEA", "8 digit NMEA",
+              "9 digit NMEA", "6 digit Magellan", "DGPS"],
+}
+
+ at directory.register
+class THD7Radio(KenwoodLiveRadio):
+    """Kenwood TH-D7"""
+    MODEL = "TH-D7"
+
+    _kenwood_split = True
+
+    def get_features(self):
+        rf = chirp_common.RadioFeatures()
+        rf.has_settings = True
+        rf.has_dtcs = False
+        rf.has_dtcs_polarity = False
+        rf.has_bank = False
+        rf.has_mode = True
+        rf.has_tuning_step = False
+        rf.can_odd_split = True
+        rf.valid_duplexes = ["", "-", "+", "split"]
+        rf.valid_modes = MODES.values()
+        rf.valid_tmodes = ["", "Tone", "TSQL"]
+        rf.valid_characters = chirp_common.CHARSET_ALPHANUMERIC
+        rf.valid_name_length = 7
+        rf.memory_bounds = (1, self._upper)
+        return rf
+
+    def _make_mem_spec(self, mem):
+        if mem.duplex in " -+":
+            duplex = util.get_dict_rev(DUPLEX, mem.duplex)
+            offset = mem.offset
+        else:
+            duplex = 0
+            offset = 0
+        
+        spec = ( \
+            "%011i" % mem.freq,
+            "%X" % STEPS.index(mem.tuning_step),
+            "%i" % duplex,
+            "0",
+            "%i" % (mem.tmode == "Tone"),
+            "%i" % (mem.tmode == "TSQL"),
+            "", # DCS Flag
+            "%02i" % (self._kenwood_valid_tones.index(mem.rtone) + 1),
+            "", # DCS Code
+            "%02i" % (self._kenwood_valid_tones.index(mem.ctone) + 1),
+            "%09i" % offset,
+            "%i" % util.get_dict_rev(MODES, mem.mode),
+            "%i" % ((mem.skip == "S") and 1 or 0))
+
+        return spec
+
+    def _parse_mem_spec(self, spec):
+        mem = chirp_common.Memory()
+
+        mem.number = int(spec[2])
+        mem.freq = int(spec[3], 10)
+        mem.tuning_step = STEPS[int(spec[4], 16)]
+        mem.duplex = DUPLEX[int(spec[5])]
+        mem.tmode = get_tmode(spec[7], spec[8], spec[9])
+        mem.rtone = self._kenwood_valid_tones[int(spec[10]) - 1]
+        mem.ctone = self._kenwood_valid_tones[int(spec[12]) - 1]
+        if spec[11] and spec[11].isdigit():
+            mem.dtcs = chirp_common.DTCS_CODES[int(spec[11][:-1]) - 1]
+        else:
+            print "Unknown or invalid DCS: %s" % spec[11]
+        if spec[13]:
+            mem.offset = int(spec[13])
+        else:
+            mem.offset = 0
+        mem.mode = MODES[int(spec[14])]
+        mem.skip = int(spec[15]) and "S" or ""
+
+        return mem
+
+    def _kenwood_get(self, cmd):
+        resp = command(self.pipe, cmd)
+        if " " in resp:
+            return resp.split(" ", 1)
+        else:
+            raise errors.RadioError("Radio refused to return %s" % cmd)
+
+    def _kenwood_set(self, cmd, value):
+        resp = command(self.pipe, cmd, value)
+        if " " in resp:
+            return
+        raise errors.RadioError("Radio refused to set %s" % cmd)
+
+    def _kenwood_get_bool(self, cmd):
+        _cmd, result = self._kenwood_get(cmd)
+        return result == "1"
+
+    def _kenwood_set_bool(self, cmd, value):
+        return self._kenwood_set(cmd, str(int(value)))
+
+    def _kenwood_get_int(self, cmd):
+        _cmd, result = self._kenwood_get(cmd)
+        return int(result)
+
+    def _kenwood_set_int(self, cmd, value, digits=1):
+        return self._kenwood_set(cmd, ("%%0%ii" % digits) % value)
+    
+    def get_settings(self):
+        aux = RadioSettingGroup("aux", "Aux")
+        tnc = RadioSettingGroup("tnc", "TNC")
+        save = RadioSettingGroup("save", "Save")
+        display = RadioSettingGroup("display", "Display")
+        dtmf = RadioSettingGroup("dtmf", "DTMF")
+        radio = RadioSettingGroup("radio", "Radio",
+                                  aux, tnc, save, display, dtmf)
+        sky = RadioSettingGroup("sky", "SkyCommand")
+        aprs = RadioSettingGroup("aprs", "APRS")
+        top = RadioSettingGroup("top", "All Settings", radio, aprs, sky)
+
+        bools = [("AMR", aprs, "APRS Message Auto-Reply"),
+                 ("AIP", aux, "Advanced Intercept Point"),
+                 ("ARO", aux, "Automatic Repeater Offset"),
+                 ("BCN", aprs, "Beacon"),
+                 ("CH", radio, "Channel Mode Display"),
+                 #("DIG", aprs, "APRS Digipeater"),
+                 ("DL", all, "Dual"),
+                 ("LK", all, "Lock"),
+                 ("LMP", all, "Lamp"),
+                 ("TSP", dtmf, "DTMF Fast Transmission"),
+                 ("TXH", dtmf, "TX Hold"),
+                 ]
+
+        for setting, group, name in bools:
+            value = self._kenwood_get_bool(setting)
+            rs = RadioSetting(setting, name,
+                              RadioSettingValueBoolean(value))
+            group.append(rs)
+
+        lists = [("BAL", all, "Balance"),
+                 ("BEP", aux, "Beep"),
+                 ("BEPT", aprs, "APRS Beep"),
+                 ("DS", tnc, "Data Sense"),
+                 ("DTB", tnc, "Data Band"),
+                 ("DTBA", aprs, "APRS Data Band"),
+                 ("DTX", aprs, "APRS Data TX"),
+                 #("ICO", aprs, "APRS Icon"),
+                 ("MNF", all, "Memory Display Mode"),
+                 ("PKSA", aprs, "APRS Packet Speed"),
+                 ("POSC", aprs, "APRS Position Comment"),
+                 ("PT", dtmf, "DTMF Speed"),
+                 ("SV", save, "Battery Save"),
+                 ("TEMP", aprs, "APRS Temperature Units"),
+                 ("TXI", aprs, "APRS Transmit Interval"),
+                 #("UNIT", aprs, "APRS Display Units"),
+                 ("WAY", aprs, "Waypoint Mode"),
+                 ]
+
+        for setting, group, name in lists:
+            value = self._kenwood_get_int(setting)
+            options = TH_D7_SETTINGS[setting]
+            rs = RadioSetting(setting, name,
+                              RadioSettingValueList(options,
+                                                    options[value]))
+            group.append(rs)
+
+        ints = [("CNT", display, "Contrast", 1, 16),
+                ]
+        for setting, group, name, minv, maxv in ints:
+            value = self._kenwood_get_int(setting)
+            rs = RadioSetting(setting, name,
+                              RadioSettingValueInteger(minv, maxv, value))
+            group.append(rs)
+
+        strings = [("MES", display, "Power-on Message", 8),
+                   ("MYC", aprs, "APRS Callsign", 8),
+                   ("PP", aprs, "APRS Path", 32),
+                   ("SCC", sky, "SkyCommand Callsign", 8),
+                   ("SCT", sky, "SkyCommand To Callsign", 8),
+                   #("STAT", aprs, "APRS Status Text", 32),
+                   ]
+        for setting, group, name, length in strings:
+            _cmd, value = self._kenwood_get(setting)
+            rs = RadioSetting(setting, name,
+                              RadioSettingValueString(0, length, value))
+            group.append(rs)
+
+        return top
+
+    def set_settings(self, settings):
+        for element in settings:
+            if not element.changed():
+                continue
+            if isinstance(element.value, RadioSettingValueBoolean):
+                self._kenwood_set_bool(element.get_name(), element.value)
+            elif isinstance(element.value, RadioSettingValueList):
+                options = TH_D7_SETTINGS[element.get_name()]
+                self._kenwood_set_int(element.get_name(),
+                                      options.index(str(element.value)))
+            elif isinstance(element.value, RadioSettingValueInteger):
+                if element.value.get_max() > 9:
+                    digits = 2
+                else:
+                    digits = 1
+                self._kenwood_set_int(element.get_name(), element.value, digits)
+            elif isinstance(element.value, RadioSettingValueString):
+                self._kenwood_set(element.get_name(), str(element.value))
+            else:
+                print "Unknown type %s" % element.value
+
+ at directory.register
+class THD7GRadio(THD7Radio):
+    """Kenwood TH-D7G"""
+    MODEL = "TH-D7G"
+
+ at directory.register
+class TMD700Radio(KenwoodLiveRadio):
+    """Kenwood TH-D700"""
+    MODEL = "TM-D700"
+
+    _kenwood_split = True
+
+    def get_features(self):
+        rf = chirp_common.RadioFeatures()
+        rf.has_dtcs = True
+        rf.has_dtcs_polarity = False
+        rf.has_bank = False
+        rf.has_mode = False
+        rf.has_tuning_step = False
+        rf.can_odd_split = True
+        rf.valid_duplexes = ["", "-", "+", "split"]
+        rf.valid_modes = ["FM"]
+        rf.valid_tmodes = ["", "Tone", "TSQL", "DTCS"]
+        rf.valid_characters = chirp_common.CHARSET_ALPHANUMERIC
+        rf.valid_name_length = 8
+        rf.memory_bounds = (1, self._upper)
+        return rf
+
+    def _make_mem_spec(self, mem):
+        if mem.duplex in " -+":
+            duplex = util.get_dict_rev(DUPLEX, mem.duplex)
+        else:
+            duplex = 0
+        spec = ( \
+            "%011i" % mem.freq,
+            "%X" % STEPS.index(mem.tuning_step),
+            "%i" % duplex,
+            "0",
+            "%i" % (mem.tmode == "Tone"),
+            "%i" % (mem.tmode == "TSQL"),
+            "%i" % (mem.tmode == "DTCS"),
+            "%02i" % (self._kenwood_valid_tones.index(mem.rtone) + 1),
+            "%03i0" % (chirp_common.DTCS_CODES.index(mem.dtcs) + 1),
+            "%02i" % (self._kenwood_valid_tones.index(mem.ctone) + 1),
+            "%09i" % mem.offset,
+            "%i" % util.get_dict_rev(MODES, mem.mode),
+            "%i" % ((mem.skip == "S") and 1 or 0))
+
+        return spec
+
+    def _parse_mem_spec(self, spec):
+        mem = chirp_common.Memory()
+
+        mem.number = int(spec[2])
+        mem.freq = int(spec[3])
+        mem.tuning_step = STEPS[int(spec[4], 16)]
+        mem.duplex = DUPLEX[int(spec[5])]
+        mem.tmode = get_tmode(spec[7], spec[8], spec[9])
+        mem.rtone = self._kenwood_valid_tones[int(spec[10]) - 1]
+        mem.ctone = self._kenwood_valid_tones[int(spec[12]) - 1]
+        if spec[11] and spec[11].isdigit():
+            mem.dtcs = chirp_common.DTCS_CODES[int(spec[11][:-1]) - 1]
+        else:
+            print "Unknown or invalid DCS: %s" % spec[11]
+        if spec[13]:
+            mem.offset = int(spec[13])
+        else:
+            mem.offset = 0
+        mem.mode = MODES[int(spec[14])]
+        mem.skip = int(spec[15]) and "S" or ""
+
+        return mem
+
+OLD_TONES = list(chirp_common.TONES)
+OLD_TONES.remove(159.8)
+OLD_TONES.remove(165.5)
+OLD_TONES.remove(171.3)
+OLD_TONES.remove(177.3)
+OLD_TONES.remove(183.5)
+OLD_TONES.remove(189.9)
+OLD_TONES.remove(196.6)
+OLD_TONES.remove(199.5)
+OLD_TONES.remove(206.5)
+OLD_TONES.remove(229.1)
+OLD_TONES.remove(254.1)
+
+ at directory.register
+class TMV7Radio(KenwoodLiveRadio):
+    """Kenwood TM-V7"""
+    MODEL = "TM-V7"
+
+    mem_upper_limit = 200 # Will be updated
+
+    _kenwood_valid_tones = list(OLD_TONES)
+
+    def set_memory(self, memory):
+        supported_tones = list(OLD_TONES)
+        supported_tones.remove(69.3)
+        if memory.rtone not in supported_tones:
+            raise errors.UnsupportedToneError("This radio does not support " +
+                                              "tone %.1fHz" % memory.rtone)
+        if memory.ctone not in supported_tones:
+            raise errors.UnsupportedToneError("This radio does not support " +
+                                              "tone %.1fHz" % memory.ctone)
+
+        return KenwoodLiveRadio.set_memory(self, memory)
+
+    def get_features(self):
+        rf = chirp_common.RadioFeatures()
+        rf.has_dtcs = False
+        rf.has_dtcs_polarity = False
+        rf.has_bank = False
+        rf.has_mode = False
+        rf.has_tuning_step = False
+        rf.valid_modes = ["FM"]
+        rf.valid_tmodes = ["", "Tone", "TSQL"]
+        rf.valid_characters = chirp_common.CHARSET_ALPHANUMERIC
+        rf.valid_name_length = 7
+        rf.has_sub_devices = True
+        rf.memory_bounds = (1, self._upper)
+        return rf
+
+    def _make_mem_spec(self, mem):
+        spec = ( \
+            "%011i" % mem.freq,
+            "%X" % STEPS.index(mem.tuning_step),
+            "%i" % util.get_dict_rev(DUPLEX, mem.duplex),
+            "0",
+            "%i" % (mem.tmode == "Tone"),
+            "%i" % (mem.tmode == "TSQL"),
+            "0",
+            "%02i" % (self._kenwood_valid_tones.index(mem.rtone) + 1),
+            "000",
+            "%02i" % (self._kenwood_valid_tones.index(mem.ctone) + 1),
+            "",
+            "0")
+
+        return spec
+
+    def _parse_mem_spec(self, spec):
+        mem = chirp_common.Memory()
+        mem.number = int(spec[2])
+        mem.freq = int(spec[3])
+        mem.tuning_step = STEPS[int(spec[4], 16)]
+        mem.duplex = DUPLEX[int(spec[5])]
+        if int(spec[7]):
+            mem.tmode = "Tone"
+        elif int(spec[8]):
+            mem.tmode = "TSQL"
+        mem.rtone = self._kenwood_valid_tones[int(spec[10]) - 1]
+        mem.ctone = self._kenwood_valid_tones[int(spec[12]) - 1]
+
+        return mem
+
+    def get_sub_devices(self):
+        return [TMV7RadioVHF(self.pipe), TMV7RadioUHF(self.pipe)]
+
+    def __test_location(self, loc):
+        mem = self.get_memory(loc)
+        if not mem.empty:
+            # Memory was not empty, must be valid
+            return True
+
+        # Mem was empty (or invalid), try to set it
+        if self._vfo == 0:
+            mem.freq = 144000000
+        else:
+            mem.freq = 440000000
+        mem.empty = False
+        try:
+            self.set_memory(mem)
+        except Exception:
+            # Failed, so we're past the limit
+            return False
+
+        # Erase what we did
+        try:
+            self.erase_memory(loc)
+        except Exception:
+            pass # V7A Can't delete just yet
+
+        return True
+
+    def _detect_split(self):
+        return 50
+
+class TMV7RadioSub(TMV7Radio):
+    """Base class for the TM-V7 sub devices"""
+    def __init__(self, pipe):
+        TMV7Radio.__init__(self, pipe)
+        self._detect_split()
+
+class TMV7RadioVHF(TMV7RadioSub):
+    """TM-V7 VHF subdevice"""
+    VARIANT = "VHF"
+    _vfo = 0
+
+class TMV7RadioUHF(TMV7RadioSub):
+    """TM-V7 UHF subdevice"""
+    VARIANT = "UHF"
+    _vfo = 1
+
+ at directory.register
+class TMG707Radio(TMV7Radio):
+    """Kenwood TM-G707"""
+    MODEL = "TM-G707"
+    
+    def get_features(self):
+        rf = TMV7Radio.get_features(self)
+        rf.has_sub_devices = False
+        rf.memory_bounds = (1, 180)
+        rf.valid_bands = [(144000000, 148000000),
+                          (430000000, 450000000)]
+        return rf
+
+THF6A_STEPS = [5.0, 6.25, 8.33, 9.0, 10.0, 12.5, 15.0, 20.0, 25.0, 30.0, 50.0,
+               100.0]
+
+THF6A_DUPLEX = dict(DUPLEX)
+THF6A_DUPLEX[3] = "split"
+
+ at directory.register
+class THF6ARadio(KenwoodLiveRadio):
+    """Kenwood TH-F6"""
+    MODEL = "TH-F6"
+
+    _upper = 399
+    _kenwood_split = True
+
+    def get_features(self):
+        rf = chirp_common.RadioFeatures()
+        rf.has_dtcs_polarity = False
+        rf.has_bank = False
+        rf.can_odd_split = True
+        rf.valid_modes = list(THF6_MODES)
+        rf.valid_tmodes = ["", "Tone", "TSQL", "DTCS"]
+        rf.valid_tuning_steps = list(THF6A_STEPS)
+        rf.valid_bands = [(1000, 1300000000)]
+        rf.valid_skips = ["", "S"]
+        rf.valid_duplexes = THF6A_DUPLEX.values()
+        rf.valid_characters = chirp_common.CHARSET_ASCII
+        rf.valid_name_length = 8
+        rf.memory_bounds = (0, self._upper)
+        return rf
+
+    def _cmd_set_memory(self, number, spec):
+        if spec:
+            spec = "," + spec
+        return "MW", "0,%03i%s" % (number, spec)
+
+    def _cmd_get_memory(self, number):
+        return "MR", "0,%03i" % number
+
+    def _cmd_get_memory_name(self, number):
+        return "MNA", "%03i" % number
+
+    def _cmd_set_memory_name(self, number, name):
+        return "MNA", "%03i,%s" % (number, name)
+
+    def _cmd_get_split(self, number):
+        return "MR", "1,%03i" % number
+
+    def _cmd_set_split(self, number, spec):
+        return "MW", "1,%03i,%s" % (number, spec)
+
+    def _parse_mem_spec(self, spec):
+        mem = chirp_common.Memory()
+
+        mem.number = int(spec[1])
+        mem.freq = int(spec[2])
+        mem.tuning_step = THF6A_STEPS[int(spec[3], 16)]
+        mem.duplex = THF6A_DUPLEX[int(spec[4])]
+        mem.tmode = get_tmode(spec[6], spec[7], spec[8])
+        mem.rtone = self._kenwood_valid_tones[int(spec[9])]
+        mem.ctone = self._kenwood_valid_tones[int(spec[10])]
+        if spec[11] and spec[11].isdigit():
+            mem.dtcs = chirp_common.DTCS_CODES[int(spec[11])]
+        else:
+            print "Unknown or invalid DCS: %s" % spec[11]
+        if spec[12]:
+            mem.offset = int(spec[12])
+        else:
+            mem.offset = 0
+        mem.mode = THF6_MODES[int(spec[13])]
+        if spec[14] == "1":
+            mem.skip = "S"
+
+        return mem
+
+    def _make_mem_spec(self, mem):
+        if mem.duplex in " +-":
+            duplex = util.get_dict_rev(THF6A_DUPLEX, mem.duplex)
+            offset = mem.offset
+        elif mem.duplex == "split":
+            duplex = 0
+            offset = 0
+        else:
+            print "Bug: unsupported duplex `%s'" % mem.duplex
+        spec = ( \
+            "%011i" % mem.freq,
+            "%X" % THF6A_STEPS.index(mem.tuning_step),
+            "%i" % duplex,
+            "0",
+            "%i" % (mem.tmode == "Tone"),
+            "%i" % (mem.tmode == "TSQL"),
+            "%i" % (mem.tmode == "DTCS"),
+            "%02i" % (self._kenwood_valid_tones.index(mem.rtone)),
+            "%02i" % (self._kenwood_valid_tones.index(mem.ctone)),
+            "%03i" % (chirp_common.DTCS_CODES.index(mem.dtcs)),
+            "%09i" % offset,
+            "%i" % (THF6_MODES.index(mem.mode)),
+            "%i" % (mem.skip == "S"))
+
+        return spec
+
+ at directory.register
+class THF7ERadio(THF6ARadio):
+    """Kenwood TH-F7"""
+    MODEL = "TH-F7"
+
+D710_DUPLEX = ["", "+", "-", "split"]
+D710_MODES = ["FM", "NFM", "AM"]
+D710_SKIP = ["", "S"]
+D710_STEPS = [5.0, 6.25, 8.33, 10.0, 12.5, 15.0, 20.0, 25.0, 30.0, 50.0, 100.0]
+D710_TONES = list(chirp_common.TONES)
+D710_TONES.remove(159.8)
+D710_TONES.remove(165.5)
+D710_TONES.remove(171.3)
+D710_TONES.remove(177.3)
+D710_TONES.remove(183.5)
+D710_TONES.remove(189.9)
+D710_TONES.remove(196.6)
+D710_TONES.remove(199.5)
+
+ at directory.register
+class TMD710Radio(KenwoodLiveRadio):
+    """Kenwood TM-D710"""
+    MODEL = "TM-D710"
+    
+    _upper = 999
+    _kenwood_valid_tones = list(D710_TONES)
+
+    def get_features(self):
+        rf = chirp_common.RadioFeatures()
+        rf.can_odd_split = True
+        rf.has_dtcs_polarity = False
+        rf.has_bank = False
+        rf.valid_tmodes = ["", "Tone", "TSQL", "DTCS"]
+        rf.valid_modes = D710_MODES
+        rf.valid_duplexes = D710_DUPLEX
+        rf.valid_tuning_steps = D710_STEPS
+        rf.valid_characters = chirp_common.CHARSET_ASCII.replace(',','')
+        rf.valid_name_length = 8
+        rf.valid_skips = D710_SKIP
+        rf.memory_bounds = (0, 999)
+        return rf
+
+    def _cmd_get_memory(self, number):
+        return "ME", "%03i" % number
+
+    def _cmd_get_memory_name(self, number):
+        return "MN", "%03i" % number
+
+    def _cmd_set_memory(self, number, spec):
+        return "ME", "%03i,%s" % (number, spec)
+
+    def _cmd_set_memory_name(self, number, name):
+        return "MN", "%03i,%s" % (number, name)
+
+    def _parse_mem_spec(self, spec):
+        mem = chirp_common.Memory()
+
+        mem.number = int(spec[0])
+        mem.freq = int(spec[1])
+        mem.tuning_step = D710_STEPS[int(spec[2], 16)]
+        mem.duplex = D710_DUPLEX[int(spec[3])]
+        # Reverse
+        if int(spec[5]):
+            mem.tmode = "Tone"
+        elif int(spec[6]):
+            mem.tmode = "TSQL"
+        elif int(spec[7]):
+            mem.tmode = "DTCS"
+        mem.rtone = self._kenwood_valid_tones[int(spec[8])]
+        mem.ctone = self._kenwood_valid_tones[int(spec[9])]
+        mem.dtcs = chirp_common.DTCS_CODES[int(spec[10])]
+        mem.offset = int(spec[11])
+        mem.mode = D710_MODES[int(spec[12])]
+        # TX Frequency
+        if int(spec[13]):
+            mem.duplex = "split"
+            mem.offset = int(spec[13])
+        # Unknown
+        mem.skip = D710_SKIP[int(spec[15])] # Memory Lockout
+
+        return mem
+
+    def _make_mem_spec(self, mem):
+        spec = ( \
+            "%010i" % mem.freq,
+            "%X" % D710_STEPS.index(mem.tuning_step),
+            "%i" % (0 if mem.duplex == "split" else \
+                        D710_DUPLEX.index(mem.duplex)),
+            "0", # Reverse
+            "%i" % (mem.tmode == "Tone" and 1 or 0),
+            "%i" % (mem.tmode == "TSQL" and 1 or 0),
+            "%i" % (mem.tmode == "DTCS" and 1 or 0),
+            "%02i" % (self._kenwood_valid_tones.index(mem.rtone)),
+            "%02i" % (self._kenwood_valid_tones.index(mem.ctone)),
+            "%03i" % (chirp_common.DTCS_CODES.index(mem.dtcs)),
+            "%08i" % (0 if mem.duplex == "split" else mem.offset), # Offset
+            "%i" % D710_MODES.index(mem.mode),
+            "%010i" % (mem.offset if mem.duplex == "split" else 0), # TX Freq
+            "0", # Unknown
+            "%i" % D710_SKIP.index(mem.skip), # Memory Lockout
+            )
+
+        return spec
+
+ at directory.register
+class THD72Radio(TMD710Radio):
+    """Kenwood TH-D72"""
+    MODEL = "TH-D72 (live mode)"
+    HARDWARE_FLOW = sys.platform == "darwin" # only OS X driver needs hw flow
+
+    def _parse_mem_spec(self, spec):
+        mem = chirp_common.Memory()
+
+        mem.number = int(spec[0])
+        mem.freq = int(spec[1])
+        mem.tuning_step = D710_STEPS[int(spec[2], 16)]
+        mem.duplex = D710_DUPLEX[int(spec[3])]
+        # Reverse
+        if int(spec[5]):
+            mem.tmode = "Tone"
+        elif int(spec[6]):
+            mem.tmode = "TSQL"
+        elif int(spec[7]):
+            mem.tmode = "DTCS"
+        mem.rtone = self._kenwood_valid_tones[int(spec[9])]
+        mem.ctone = self._kenwood_valid_tones[int(spec[10])]
+        mem.dtcs = chirp_common.DTCS_CODES[int(spec[11])]
+        mem.offset = int(spec[13])
+        mem.mode = D710_MODES[int(spec[14])]
+        # TX Frequency
+        if int(spec[15]):
+            mem.duplex = "split"
+            mem.offset = int(spec[15])
+        # Lockout
+        mem.skip = D710_SKIP[int(spec[17])] # Memory Lockout
+
+        return mem
+
+    def _make_mem_spec(self, mem):
+        spec = ( \
+            "%010i" % mem.freq,
+            "%X" % D710_STEPS.index(mem.tuning_step),
+            "%i" % (0 if mem.duplex == "split" else \
+                        D710_DUPLEX.index(mem.duplex)),
+            "0", # Reverse
+            "%i" % (mem.tmode == "Tone" and 1 or 0),
+            "%i" % (mem.tmode == "TSQL" and 1 or 0),
+            "%i" % (mem.tmode == "DTCS" and 1 or 0),
+            "0",
+            "%02i" % (self._kenwood_valid_tones.index(mem.rtone)),
+            "%02i" % (self._kenwood_valid_tones.index(mem.ctone)),
+            "%03i" % (chirp_common.DTCS_CODES.index(mem.dtcs)),
+            "0",
+            "%08i" % (0 if mem.duplex == "split" else mem.offset), # Offset
+            "%i" % D710_MODES.index(mem.mode),
+            "%010i" % (mem.offset if mem.duplex == "split" else 0), # TX Freq
+            "0", # Unknown
+            "%i" % D710_SKIP.index(mem.skip), # Memory Lockout
+            )
+
+        return spec
+
+ at directory.register
+class TMV71Radio(TMD710Radio):
+    """Kenwood TM-V71"""
+    MODEL = "TM-V71"
+
+THK2_DUPLEX = ["", "+", "-"]
+THK2_MODES = ["FM", "NFM"]
+THK2_TONES = list(chirp_common.TONES)
+THK2_TONES.remove(159.8) # ??
+THK2_TONES.remove(165.5) # ??
+THK2_TONES.remove(171.3) # ??
+THK2_TONES.remove(177.3) # ??
+THK2_TONES.remove(183.5) # ??
+THK2_TONES.remove(189.9) # ??
+THK2_TONES.remove(196.6) # ??
+THK2_TONES.remove(199.5) # ??
+
+THK2_CHARS = chirp_common.CHARSET_UPPER_NUMERIC + "-/"
+
+ at directory.register
+class THK2Radio(KenwoodLiveRadio):
+    """Kenwood TH-K2"""
+    MODEL = "TH-K2"
+
+    _kenwood_valid_tones = list(THK2_TONES)
+
+    def get_features(self):
+        rf = chirp_common.RadioFeatures()
+        rf.can_odd_split = False
+        rf.has_dtcs_polarity = False
+        rf.has_bank = False
+        rf.has_tuning_step = False
+        rf.valid_tmodes = ["", "Tone", "TSQL", "DTCS"]
+        rf.valid_modes = THK2_MODES
+        rf.valid_duplexes = THK2_DUPLEX
+        rf.valid_characters = THK2_CHARS
+        rf.valid_name_length = 6
+        rf.valid_bands = [(136000000, 173990000)]
+        rf.valid_skips = ["", "S"]
+        rf.valid_tuning_steps = [5.0]
+        rf.memory_bounds = (1, 50)
+        return rf
+
+    def _cmd_get_memory(self, number):
+        return "ME", "%02i" % number
+
+    def _cmd_get_memory_name(self, number):
+        return "MN", "%02i" % number
+
+    def _cmd_set_memory(self, number, spec):
+        return "ME", "%02i,%s" % (number, spec)
+
+    def _cmd_set_memory_name(self, number, name):
+        return "MN", "%02i,%s" % (number, name)
+
+    def _parse_mem_spec(self, spec):
+        mem = chirp_common.Memory()
+
+        mem.number = int(spec[0])
+        mem.freq = int(spec[1])
+        #mem.tuning_step = 
+        mem.duplex = THK2_DUPLEX[int(spec[3])]
+        if int(spec[5]):
+            mem.tmode = "Tone"
+        elif int(spec[6]):
+            mem.tmode = "TSQL"
+        elif int(spec[7]):
+            mem.tmode = "DTCS"
+        mem.rtone = self._kenwood_valid_tones[int(spec[8])]
+        mem.ctone = self._kenwood_valid_tones[int(spec[9])]
+        mem.dtcs = chirp_common.DTCS_CODES[int(spec[10])]
+        mem.offset = int(spec[11])
+        mem.mode = THK2_MODES[int(spec[12])]
+        mem.skip = int(spec[16]) and "S" or ""
+        return mem
+
+    def _make_mem_spec(self, mem):
+        try:
+            rti = self._kenwood_valid_tones.index(mem.rtone)
+            cti = self._kenwood_valid_tones.index(mem.ctone)
+        except ValueError:
+            raise errors.UnsupportedToneError()
+
+        spec = ( \
+            "%010i" % mem.freq,
+            "0",
+            "%i"    % THK2_DUPLEX.index(mem.duplex),
+            "0",
+            "%i"    % int(mem.tmode == "Tone"),
+            "%i"    % int(mem.tmode == "TSQL"),
+            "%i"    % int(mem.tmode == "DTCS"),
+            "%02i"  % rti,
+            "%02i"  % cti,
+            "%03i"  % chirp_common.DTCS_CODES.index(mem.dtcs),
+            "%08i"  % mem.offset,
+            "%i"    % THK2_MODES.index(mem.mode),
+            "0",
+            "%010i" % 0,
+            "0",
+            "%i"    % int(mem.skip == "S")
+            )
+        return spec
+            
+
+ at directory.register
+class TM271Radio(THK2Radio):
+    """Kenwood TM-271"""
+    MODEL = "TM-271"
+    
+    def get_features(self):
+        rf = chirp_common.RadioFeatures()
+        rf.can_odd_split = False
+        rf.has_dtcs_polarity = False
+        rf.has_bank = False
+        rf.has_tuning_step = False
+        rf.valid_tmodes = ["", "Tone", "TSQL", "DTCS"]
+        rf.valid_modes = THK2_MODES
+        rf.valid_duplexes = THK2_DUPLEX
+        rf.valid_characters = THK2_CHARS
+        rf.valid_name_length = 6
+        rf.valid_bands = [(137000000, 173990000)]
+        rf.valid_skips = ["", "S"]
+        rf.valid_tuning_steps = [5.0]
+        rf.memory_bounds = (0, 99)
+        return rf
+
+    def _cmd_get_memory(self, number):
+        return "ME", "%03i" % number
+
+    def _cmd_get_memory_name(self, number):
+        return "MN", "%03i" % number
+
+    def _cmd_set_memory(self, number, spec):
+        return "ME", "%03i,%s" % (number, spec)
+
+    def _cmd_set_memory_name(self, number, name):
+        return "MN", "%03i,%s" % (number, name)
+
+def do_test():
+    """Dev test"""
+    mem = chirp_common.Memory()
+    mem.number = 1
+    mem.freq = 144000000
+    mem.duplex = "split"
+    mem.offset = 146000000
+
+    tc = THF6ARadio
+    class FakeSerial:
+        """Faked serial line"""
+        buf = ""
+        def write(self, buf):
+            """Write"""
+            self.buf = buf
+        def read(self, count):
+            """Read"""
+            if self.buf[:2] == "ID":
+                return "ID %s\r" % tc.MODEL
+            return self.buf
+        def setTimeout(self, foo):
+            """Set Timeout"""
+            pass
+        def setBaudrate(self, foo):
+            """Set Baudrate"""
+            pass
+
+    radio = tc(FakeSerial())
+    radio.set_memory(mem)
+
+if __name__ == "__main__":
+    do_test()
diff --git a/chirp/memmap.py b/chirp/memmap.py
new file mode 100644
index 0000000..2230265
--- /dev/null
+++ b/chirp/memmap.py
@@ -0,0 +1,88 @@
+# 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/>.
+
+from chirp import util
+
+class MemoryMap:
+    """
+    A pythonic memory map interface
+    """
+
+    def __init__(self, data):
+        self._data = list(data)
+
+    def printable(self, start=None, end=None, printit=True):
+        """Return a printable representation of the memory map"""
+        if not start:
+            start = 0
+
+        if not end:
+            end = len(self._data)
+
+        string = util.hexprint(self._data[start:end])
+
+        if printit:
+            print string
+
+        return string
+
+    def get(self, start, length=1):
+        """Return a chunk of memory of @length bytes from @start"""
+        if start == -1:
+            return "".join(self._data[start:])
+        else:
+            return "".join(self._data[start:start+length])
+
+    def set(self, pos, value):
+        """Set a chunk of memory at @pos to @value"""
+        if isinstance(value, int):
+            self._data[pos] = chr(value)
+        elif isinstance(value, str):
+            for byte in value:
+                self._data[pos] = byte
+                pos += 1
+        else:
+            raise ValueError("Unsupported type %s for value" % \
+                                 type(value).__name__)
+
+    def get_packed(self):
+        """Return the entire memory map as raw data"""
+        return "".join(self._data)
+
+    def __len__(self):
+        return len(self._data)
+
+    def __getslice__(self, start, end):
+        return self.get(start, end-start)
+
+    def __getitem__(self, pos):
+        return self.get(pos)
+
+    def __setitem__(self, pos, value):
+        """
+        NB: Setting a value of more than one character overwrites
+        len(value) bytes of the map, unlike a typical array!
+        """
+        self.set(pos, value)
+
+    def __str__(self):
+        return self.get_packed()
+
+    def __repr__(self):
+        return self.printable(printit=False)
+
+    def truncate(self, size):
+        """Truncate the memory map to @size"""
+        self._data = self._data[:size]
diff --git a/chirp/platform.py b/chirp/platform.py
new file mode 100644
index 0000000..882d898
--- /dev/null
+++ b/chirp/platform.py
@@ -0,0 +1,434 @@
+# 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 os
+import sys
+import glob
+from subprocess import Popen
+
+try:
+    from serial.tools.list_ports import comports
+except:
+    def comports():
+        import win32file
+        import win32con
+
+        ports = []
+        for i in range(1, 257):
+            portname = "COM%i" % i
+            print "Trying %s" % portname
+            try:
+                mode = win32con.GENERIC_READ | win32con.GENERIC_WRITE
+                port = \
+                    win32file.CreateFile(portname,
+                                         mode,
+                                         win32con.FILE_SHARE_READ,
+                                         None,
+                                         win32con.OPEN_EXISTING,
+                                         0,
+                                         None)
+                ports.append((portname,"Unknown","Serial"))
+                win32file.CloseHandle(port)
+                port = None
+            except Exception, e:
+                print "Failed: %s" % e
+                pass
+
+        return ports
+    
+def _find_me():
+    return sys.modules["chirp.platform"].__file__
+
+class Platform:
+    """Base class for platform-specific functions"""
+
+    def __init__(self, basepath):
+        self._base = basepath
+        self._last_dir = self.default_dir()
+
+    def get_last_dir(self):
+        """Return the last directory used"""
+        return self._last_dir
+
+    def set_last_dir(self, last_dir):
+        """Set the last directory used"""
+        self._last_dir = last_dir
+
+    def config_dir(self):
+        """Return the preferred configuration file directory"""
+        return self._base
+
+    def log_dir(self):
+        """Return the preferred log file directory"""
+        logdir = os.path.join(self.config_dir(), "logs")
+        if not os.path.isdir(logdir):
+            os.mkdir(logdir)
+
+        return logdir
+
+    def filter_filename(self, filename):
+        """Filter @filename for platform-forbidden characters"""
+        return filename
+
+    def log_file(self, filename):
+        """Return the full path to a log file with @filename"""
+        filename = self.filter_filename(filename + ".txt").replace(" ", "_")
+        return os.path.join(self.log_dir(), filename)
+
+    def config_file(self, filename):
+        """Return the full path to a config file with @filename"""
+        return os.path.join(self.config_dir(),
+                            self.filter_filename(filename))
+
+    def open_text_file(self, path):
+        """Spawn the necessary program to open a text file at @path"""
+        raise NotImplementedError("The base class can't do that")
+
+    def open_html_file(self, path):
+        """Spawn the necessary program to open an HTML file at @path"""
+        raise NotImplementedError("The base class can't do that")
+
+    def list_serial_ports(self):
+        """Return a list of valid serial ports"""
+        return []
+
+    def default_dir(self):
+        """Return the default directory for this platform"""
+        return "."
+
+    def gui_open_file(self, start_dir=None, types=[]):
+        """Prompt the user to pick a file to open"""
+        import gtk
+
+        if not start_dir:
+            start_dir = self._last_dir
+
+        dlg = gtk.FileChooserDialog("Select a file to open",
+                                    None,
+                                    gtk.FILE_CHOOSER_ACTION_OPEN,
+                                    (gtk.STOCK_CANCEL, gtk.RESPONSE_CANCEL,
+                                     gtk.STOCK_OPEN, gtk.RESPONSE_OK))
+        if start_dir and os.path.isdir(start_dir):
+            dlg.set_current_folder(start_dir)
+
+        for desc, spec in types:
+            ff = gtk.FileFilter()
+            ff.set_name(desc)
+            ff.add_pattern(spec)
+            dlg.add_filter(ff)
+
+        res = dlg.run()
+        fname = dlg.get_filename()
+        dlg.destroy()
+
+        if res == gtk.RESPONSE_OK:
+            self._last_dir = os.path.dirname(fname)
+            return fname
+        else:
+            return None
+
+    def gui_save_file(self, start_dir=None, default_name=None, types=[]):
+        """Prompt the user to pick a filename to save"""
+        import gtk
+
+        if not start_dir:
+            start_dir = self._last_dir
+
+        dlg = gtk.FileChooserDialog("Save file as",
+                                    None,
+                                    gtk.FILE_CHOOSER_ACTION_SAVE,
+                                    (gtk.STOCK_CANCEL, gtk.RESPONSE_CANCEL,
+                                     gtk.STOCK_SAVE, gtk.RESPONSE_OK))
+        if start_dir and os.path.isdir(start_dir):
+            dlg.set_current_folder(start_dir)
+
+        if default_name:
+            dlg.set_current_name(default_name)
+
+        extensions = {}
+        for desc, ext in types:
+            ff = gtk.FileFilter()
+            ff.set_name(desc)
+            ff.add_pattern("*.%s" % ext)
+            extensions[desc] = ext
+            dlg.add_filter(ff)
+
+        res = dlg.run()
+
+        fname = dlg.get_filename()
+        ext = extensions[dlg.get_filter().get_name()]
+        if fname and not fname.endswith(".%s" % ext):
+            fname = "%s.%s" % (fname, ext)
+
+        dlg.destroy()
+
+        if res == gtk.RESPONSE_OK:
+            self._last_dir = os.path.dirname(fname)
+            return fname
+        else:
+            return None
+
+    def gui_select_dir(self, start_dir=None):
+        """Prompt the user to pick a directory"""
+        import gtk
+
+        if not start_dir:
+            start_dir = self._last_dir
+
+        dlg = gtk.FileChooserDialog("Choose folder",
+                                    None,
+                                    gtk.FILE_CHOOSER_ACTION_SELECT_FOLDER,
+                                    (gtk.STOCK_CANCEL, gtk.RESPONSE_CANCEL,
+                                     gtk.STOCK_SAVE, gtk.RESPONSE_OK))
+        if start_dir and os.path.isdir(start_dir):
+            dlg.set_current_folder(start_dir)
+
+        res = dlg.run()
+        fname = dlg.get_filename()
+        dlg.destroy()
+
+        if res == gtk.RESPONSE_OK and os.path.isdir(fname):
+            self._last_dir = fname
+            return fname
+        else:
+            return None
+
+    def os_version_string(self):
+        """Return a string that describes the OS/platform version"""
+        return "Unknown Operating System"
+
+    def executable_path(self):
+        """Return a full path to the program executable"""
+        def we_are_frozen():
+            return hasattr(sys, "frozen")
+
+        if we_are_frozen():
+            # Win32, find the directory of the executable
+            return os.path.dirname(unicode(sys.executable,
+                                           sys.getfilesystemencoding()))
+        else:
+            # UNIX: Find the parent directory of this module
+            return os.path.dirname(os.path.abspath(os.path.join(_find_me(),
+                                                                "..")))
+
+def _unix_editor():
+    macos_textedit = "/Applications/TextEdit.app/Contents/MacOS/TextEdit"
+
+    if os.path.exists(macos_textedit):
+        return macos_textedit
+    else:
+        return "gedit"
+
+class UnixPlatform(Platform):
+    """A platform module suitable for UNIX systems"""
+    def __init__(self, basepath):
+        if not basepath:
+            basepath = os.path.abspath(os.path.join(self.default_dir(),
+                                                    ".chirp"))
+        
+        if not os.path.isdir(basepath):
+            os.mkdir(basepath)
+
+        Platform.__init__(self, basepath)
+
+	# This is a hack that needs to be properly fixed by importing the
+	# latest changes to this module from d-rats.  In the interest of
+	# time, however, I'll throw it here
+        if sys.platform == "darwin":
+            if not os.environ.has_key("DISPLAY"):
+                print "Forcing DISPLAY for MacOS"
+                os.environ["DISPLAY"] = ":0"
+
+            os.environ["PANGO_RC_FILE"] = "../Resources/etc/pango/pangorc"
+
+    def default_dir(self):
+        return os.path.abspath(os.getenv("HOME"))
+
+    def filter_filename(self, filename):
+        return filename.replace("/", "")
+
+    def open_text_file(self, path):
+        pid1 = os.fork()
+        if pid1 == 0:
+            pid2 = os.fork()
+            if pid2 == 0:
+                editor = _unix_editor()
+                print "calling `%s %s'" % (editor, path)
+                os.execlp(editor, editor, path)
+            else:
+                sys.exit(0)
+        else:
+            os.waitpid(pid1, 0)
+            print "Exec child exited"
+
+    def open_html_file(self, path):
+        os.system("firefox '%s'" % path)
+
+    def list_serial_ports(self):
+        return sorted(glob.glob("/dev/ttyS*") +
+                      glob.glob("/dev/ttyUSB*") +
+                      glob.glob("/dev/cu.*") +
+                      glob.glob("/dev/term/*") +
+                      glob.glob("/dev/tty.KeySerial*"))
+
+    def os_version_string(self):
+        try:
+            issue = file("/etc/issue.net", "r")
+            ver = issue.read().strip().replace("\r", "").replace("\n", "")[:64]
+            issue.close()
+            ver = "%s - %s" % (os.uname()[0], ver)
+        except Exception:
+            ver = " ".join(os.uname())
+
+        return ver
+
+class Win32Platform(Platform):
+    """A platform module suitable for Windows systems"""
+    def __init__(self, basepath=None):
+        if not basepath:
+            appdata = os.getenv("APPDATA")
+            if not appdata:
+                appdata = "C:\\"
+            basepath = os.path.abspath(os.path.join(appdata, "CHIRP"))
+
+        if not os.path.isdir(basepath):
+            os.mkdir(basepath)
+
+        Platform.__init__(self, basepath)
+
+    def default_dir(self):
+        return os.path.abspath(os.path.join(os.getenv("USERPROFILE"),
+                                            "Desktop"))
+
+    def filter_filename(self, filename):
+        for char in "/\\:*?\"<>|":
+            filename = filename.replace(char, "")
+
+        return filename
+
+    def open_text_file(self, path):
+        Popen(["notepad", path])
+        return
+
+    def open_html_file(self, path):
+        os.system("explorer %s" % path)
+    
+    def list_serial_ports(self):
+        return [port for port, name, url in comports()]
+
+    def gui_open_file(self, start_dir=None, types=[]):
+        import win32gui
+
+        typestrs = ""
+        for desc, spec in types:
+            typestrs += "%s\0%s\0" % (desc, spec)
+        if not typestrs:
+            typestrs = None
+
+        try:
+            fname, _, _ = win32gui.GetOpenFileNameW(Filter=typestrs)
+        except Exception, e:
+            print "Failed to get filename: %s" % e
+            return None
+
+        return str(fname)
+
+    def gui_save_file(self, start_dir=None, default_name=None, types=[]):
+        import win32gui
+        import win32api
+        
+        (pform, _, _, _, _) = win32api.GetVersionEx()
+
+        typestrs = ""
+        custom = "%s\0*.%s\0" % (types[0][0], types[0][1])
+        for desc, ext in types[1:]:
+            typestrs += "%s\0%s\0" % (desc, "*.%s" % ext)
+
+        if pform > 5:
+            typestrs = "%s\0%s\0" % (types[0][0], "*.%s" % types[0][1]) + \
+                typestrs
+
+        if not typestrs:
+            typestrs = custom
+            custom = None
+
+        def_ext = "*.%s" % types[0][1]
+        try:
+            fname, _, _ = win32gui.GetSaveFileNameW(File=default_name,
+                                                    CustomFilter=custom,
+                                                    DefExt=def_ext,
+                                                    Filter=typestrs)
+        except Exception, e:
+            print "Failed to get filename: %s" % e
+            return None
+
+        return str(fname)
+
+    def gui_select_dir(self, start_dir=None):
+        from win32com.shell import shell
+
+        try:
+            pidl, _, _ = shell.SHBrowseForFolder()
+            fname = shell.SHGetPathFromIDList(pidl)
+        except Exception, e:
+            print "Failed to get directory: %s" % e
+            return None
+
+        return str(fname)
+
+    def os_version_string(self):
+        import win32api
+
+        vers = { 4: "Win2k",
+                 5: "WinXP",
+                 6: "WinVista/7",
+                 }
+
+        (pform, sub, build, _, _) = win32api.GetVersionEx()
+
+        return vers.get(pform, "Win32 (Unknown %i.%i:%i)" % (pform, sub, build))
+
+def _get_platform(basepath):
+    if os.name == "nt":
+        return Win32Platform(basepath)
+    else:
+        return UnixPlatform(basepath)
+
+PLATFORM = None
+def get_platform(basepath=None):
+    """Return the platform singleton"""
+    global PLATFORM
+
+    if not PLATFORM:
+        PLATFORM = _get_platform(basepath)
+
+    return PLATFORM
+
+def _do_test():
+    __pform = get_platform()
+
+    print "Config dir: %s" % __pform.config_dir()
+    print "Default dir: %s" % __pform.default_dir()
+    print "Log file (foo): %s" % __pform.log_file("foo")
+    print "Serial ports: %s" % __pform.list_serial_ports()
+    print "OS Version: %s" % __pform.os_version_string()
+    #__pform.open_text_file("d-rats.py")
+
+    #print "Open file: %s" % __pform.gui_open_file()
+    #print "Save file: %s" % __pform.gui_save_file(default_name="Foo.txt")
+    print "Open folder: %s" % __pform.gui_select_dir("/tmp")
+
+if __name__ == "__main__":
+    _do_test()
diff --git a/chirp/puxing.py b/chirp/puxing.py
new file mode 100644
index 0000000..05602ef
--- /dev/null
+++ b/chirp/puxing.py
@@ -0,0 +1,506 @@
+# Copyright 2011 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/>.
+
+"""Puxing radios management module"""
+
+import time
+import os
+from chirp import util, chirp_common, bitwise, errors, directory
+from chirp.wouxun_common import wipe_memory, do_download, do_upload
+
+if os.getenv("CHIRP_DEBUG"):
+    DEBUG = True
+else:
+    DEBUG = False
+
+def _puxing_prep(radio):
+    radio.pipe.write("\x02PROGRA")
+    ack = radio.pipe.read(1)
+    if ack != "\x06":
+        raise Exception("Radio did not ACK first command")
+
+    radio.pipe.write("M\x02")
+    ident = radio.pipe.read(8)
+    if len(ident) != 8:
+        print util.hexprint(ident)
+        raise Exception("Radio did not send identification")
+
+    radio.pipe.write("\x06")
+    if radio.pipe.read(1) != "\x06":
+        raise Exception("Radio did not ACK ident")
+
+def puxing_prep(radio):
+    """Do the Puxing PX-777 identification dance"""
+    for _i in range(0, 10):
+        try:
+            return _puxing_prep(radio)
+        except Exception, e:
+            time.sleep(1)
+
+    raise e
+
+def puxing_download(radio):
+    """Talk to a Puxing PX-777 and do a download"""
+    try:
+        puxing_prep(radio)
+        return do_download(radio, 0x0000, 0x0C60, 0x0008)
+    except errors.RadioError:
+        raise
+    except Exception, e:
+        raise errors.RadioError("Failed to communicate with radio: %s" % e)
+
+def puxing_upload(radio):
+    """Talk to a Puxing PX-777 and do an upload"""
+    try:
+        puxing_prep(radio)
+        return do_upload(radio, 0x0000, 0x0C40, 0x0008)
+    except errors.RadioError:
+        raise
+    except Exception, e:
+        raise errors.RadioError("Failed to communicate with radio: %s" % e)
+
+POWER_LEVELS = [chirp_common.PowerLevel("High", watts=5.00),
+                chirp_common.PowerLevel("Low", watts=1.00)]
+                
+PUXING_CHARSET = list("0123456789") + \
+    [chr(x + ord("A")) for x in range(0, 26)] + \
+    list("-                       ")
+
+PUXING_MEM_FORMAT = """
+#seekto 0x0000;
+struct {
+  lbcd rx_freq[4];
+  lbcd tx_freq[4];
+  lbcd rx_tone[2];
+  lbcd tx_tone[2];
+  u8 _3_unknown_1;
+  u8 _2_unknown_1:2,
+     power_high:1,
+     iswide:1,
+     skip:1,
+     bclo:2,
+     _2_unknown_2:1;
+  u8 _4_unknown1:7,
+     pttid:1;
+  u8 unknown;
+} memory[128];
+
+#seekto 0x080A;
+struct {
+  u8 limits;
+  u8 model;
+} model[1];
+
+#seekto 0x0850;
+struct {
+  u8 name[6];
+  u8 pad[2];
+} names[128];
+"""
+
+# Limits
+#   67- 72: 0xEE
+#  136-174: 0xEF
+#  240-260: 0xF0
+#  350-390: 0xF1
+#  400-430: 0xF2
+#  430-450: 0xF3
+#  450-470: 0xF4
+#  470-490: 0xF5
+#  400-470: 0xF6
+#  460-520: 0xF7
+
+PUXING_MODELS = {
+    328 : 0x38,
+    338 : 0x39,
+    777 : 0x3A,
+}
+
+PUXING_777_BANDS = [
+    ( 67000000,  72000000),
+    (136000000, 174000000),
+    (240000000, 260000000),
+    (350000000, 390000000),
+    (400000000, 430000000),
+    (430000000, 450000000),
+    (450000000, 470000000),
+    (470000000, 490000000),
+    (400000000, 470000000),
+    (460000000, 520000000),
+]
+
+ at directory.register
+class Puxing777Radio(chirp_common.CloneModeRadio):
+    """Puxing PX-777"""
+    VENDOR = "Puxing"
+    MODEL = "PX-777"
+
+    def sync_in(self):
+        self._mmap = puxing_download(self)
+        self.process_mmap()
+
+    def sync_out(self):
+        puxing_upload(self)
+
+    def get_features(self):
+        rf = chirp_common.RadioFeatures()
+        rf.valid_tmodes = ["", "Tone", "TSQL", "DTCS"]
+        rf.valid_modes = ["FM", "NFM"]
+        rf.valid_power_levels = POWER_LEVELS
+        rf.valid_characters = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ"
+        rf.valid_name_length = 6
+        rf.has_ctone = False
+        rf.has_tuning_step = False
+        rf.has_bank = False
+        rf.memory_bounds = (1, 128)
+
+        if not hasattr(self, "_memobj") or self._memobj is None:
+            limit_idx = 1
+        else:
+            limit_idx = self._memobj.model.limits - 0xEE
+        try:
+            rf.valid_bands = [PUXING_777_BANDS[limit_idx]]
+        except IndexError:
+            print "Invalid band index %i (0x%02x)" % \
+                (limit_idx, self._memobj.model.limits)
+            rf.valid_bands = [PUXING_777_BANDS[1]]
+
+        return rf
+
+    def process_mmap(self):
+        self._memobj = bitwise.parse(PUXING_MEM_FORMAT, self._mmap)
+
+    def get_raw_memory(self, number):
+        return repr(self._memobj.memory[number - 1]) + "\r\n" + \
+            repr(self._memobj.names[number - 1])
+
+    @classmethod
+    def match_model(cls, filedata, filename):
+        if len(filedata) > 0x080B and \
+                ord(filedata[0x080B]) != PUXING_MODELS[777]:
+            return False
+        return len(filedata) == 3168
+
+    def get_memory(self, number):
+        _mem = self._memobj.memory[number - 1]
+        _nam = self._memobj.names[number - 1]
+
+        def _is_empty():
+            for i in range(0, 4):
+                if _mem.rx_freq[i].get_raw() != "\xFF":
+                    return False
+            return True
+
+        def _is_no_tone(field):
+            return field.get_raw() in ["\x00\x00", "\xFF\xFF"]
+
+        def _get_dtcs(value):
+            # Upper nibble 0x80 -> DCS, 0xC0 -> Inv. DCS
+            if value > 12000:
+                return "R", value - 12000
+            elif value > 8000:
+                return "N", value - 8000
+            else:
+                raise Exception("Unable to convert DCS value")
+
+        def _do_dtcs(mem, txfield, rxfield):
+            if int(txfield) < 8000 or int(rxfield) < 8000:
+                raise Exception("Split tone not supported")
+
+            if txfield[0].get_raw() == "\xFF":
+                tp, tx = "N", None
+            else:
+                tp, tx = _get_dtcs(int(txfield))
+            
+            if rxfield[0].get_raw() == "\xFF":
+                rp, rx = "N", None
+            else:
+                rp, rx = _get_dtcs(int(rxfield))
+
+            if not rx:
+                rx = tx
+            if not tx:
+                tx = rx
+
+            if tx != rx:
+                raise Exception("Different RX and TX DCS codes not supported")
+
+            mem.dtcs = tx
+            mem.dtcs_polarity = "%s%s" % (tp, rp)
+
+        mem = chirp_common.Memory()
+        mem.number = number
+
+        if _is_empty():
+            mem.empty = True
+            return mem
+
+        mem.freq = int(_mem.rx_freq) * 10
+        mem.offset = (int(_mem.tx_freq) * 10) - mem.freq
+        if mem.offset < 0:
+            mem.duplex = "-"
+        elif mem.offset:
+            mem.duplex = "+"
+        mem.offset = abs(mem.offset)
+        if not _mem.skip:
+            mem.skip = "S"
+        if not _mem.iswide:
+            mem.mode = "NFM"
+
+        if _is_no_tone(_mem.tx_tone):
+            pass # No tone
+        elif int(_mem.tx_tone) > 8000 or \
+                (not _is_no_tone(_mem.rx_tone) and int(_mem.rx_tone) > 8000):
+            mem.tmode = "DTCS"
+            _do_dtcs(mem, _mem.tx_tone, _mem.rx_tone)
+        else:
+            mem.rtone = int(_mem.tx_tone) / 10.0
+            mem.tmode = _is_no_tone(_mem.rx_tone) and "Tone" or "TSQL"
+
+        mem.power = POWER_LEVELS[not _mem.power_high]
+
+        for i in _nam.name:
+            if i == 0xFF:
+                break
+            mem.name += PUXING_CHARSET[i]
+
+        return mem
+
+    def set_memory(self, mem):
+        _mem = self._memobj.memory[mem.number - 1]
+        _nam = self._memobj.names[mem.number - 1]
+
+        if mem.empty:
+            wipe_memory(_mem, "\xFF")
+            return
+
+        _mem.rx_freq = mem.freq / 10
+        if mem.duplex == "+":
+            _mem.tx_freq = (mem.freq / 10) + (mem.offset / 10)
+        elif mem.duplex == "-":
+            _mem.tx_freq = (mem.freq / 10) - (mem.offset / 10)
+        else:
+            _mem.tx_freq = (mem.freq / 10)
+        _mem.skip = mem.skip != "S"
+        _mem.iswide = mem.mode != "NFM"
+
+
+        _mem.rx_tone[0].set_raw("\xFF")
+        _mem.rx_tone[1].set_raw("\xFF")
+        _mem.tx_tone[0].set_raw("\xFF")
+        _mem.tx_tone[1].set_raw("\xFF")
+
+
+        if mem.tmode == "DTCS":
+            _mem.tx_tone = int("%x" % int("%i" % (mem.dtcs), 16))
+            _mem.rx_tone = int("%x" % int("%i" % (mem.dtcs), 16))
+
+            # Argh.  Set the high order two bits to signal DCS or Inv. DCS
+            txm = mem.dtcs_polarity[0] == "N" and 0x80 or 0xC0
+            rxm = mem.dtcs_polarity[1] == "N" and 0x80 or 0xC0
+            _mem.tx_tone[1].set_raw(chr(ord(_mem.tx_tone[1].get_raw()) | txm))
+            _mem.rx_tone[1].set_raw(chr(ord(_mem.rx_tone[1].get_raw()) | rxm))
+
+        elif mem.tmode:
+            _mem.tx_tone = int(mem.rtone * 10)
+            if mem.tmode == "TSQL":
+                _mem.rx_tone = int(_mem.tx_tone)
+
+        if mem.power:
+            _mem.power_high = not POWER_LEVELS.index(mem.power)
+        else:
+            _mem.power_high = True
+
+        # Default to disabling the busy channel lockout
+        # 00 == Close
+        # 01 == Carrier
+        # 10 == QT/DQT
+        _mem.bclo = 0
+
+        _nam.name = [0xFF] * 6
+        for i in range(0, len(mem.name)):
+            try:
+                _nam.name[i] = PUXING_CHARSET.index(mem.name[i])
+            except IndexError:
+                raise Exception("Character `%s' not supported")
+
+def puxing_2r_prep(radio):
+    """Do the Puxing 2R identification dance"""
+    radio.pipe.setTimeout(0.2)
+    radio.pipe.write("PROGRAM\x02")
+    ack = radio.pipe.read(1)
+    if ack != "\x06":
+        raise Exception("Radio is not responding")
+
+    radio.pipe.write(ack)
+    ident = radio.pipe.read(16)
+    print "Radio ident: %s (%i)" % (repr(ident), len(ident))
+
+def puxing_2r_download(radio):
+    """Talk to a Puxing 2R and do a download"""
+    try:
+        puxing_2r_prep(radio)
+        return do_download(radio, 0x0000, 0x0FE0, 0x0010)
+    except errors.RadioError:
+        raise
+    except Exception, e:
+        raise errors.RadioError("Failed to communicate with radio: %s" % e)
+
+def puxing_2r_upload(radio):
+    """Talk to a Puxing 2R and do an upload"""
+    try:
+        puxing_2r_prep(radio)
+        return do_upload(radio, 0x0000, 0x0FE0, 0x0010)
+    except errors.RadioError:
+        raise
+    except Exception, e:
+        raise errors.RadioError("Failed to communicate with radio: %s" % e)
+
+PUXING_2R_MEM_FORMAT = """
+#seekto 0x0010;
+struct {
+  lbcd freq[4];
+  lbcd offset[4];
+  u8 rx_tone;
+  u8 tx_tone;
+  u8 duplex:2,
+     txdtcsinv:1,
+     rxdtcsinv:1,
+     simplex:1,
+     unknown2:1,
+     iswide:1,
+     ishigh:1;
+  u8 name[5];
+} memory[128];
+"""
+
+PX2R_DUPLEX = ["", "+", "-", ""]
+PX2R_POWER_LEVELS = [chirp_common.PowerLevel("Low", watts=1.0),
+                     chirp_common.PowerLevel("High", watts=2.0)]
+PX2R_CHARSET = "0123456789- ABCDEFGHIJKLMNOPQRSTUVWXYZ +"
+
+ at directory.register
+class Puxing2RRadio(chirp_common.CloneModeRadio):
+    """Puxing PX-2R"""
+    VENDOR = "Puxing"
+    MODEL = "PX-2R"
+    _memsize = 0x0FE0
+
+    def get_features(self):
+        rf = chirp_common.RadioFeatures()
+        rf.valid_tmodes = ["", "Tone", "TSQL", "DTCS"]
+        rf.valid_modes = ["FM", "NFM"]
+        rf.valid_power_levels = PX2R_POWER_LEVELS
+        rf.valid_bands = [(400000000, 500000000)]
+        rf.valid_characters = PX2R_CHARSET
+        rf.valid_name_length = 5
+        rf.valid_duplexes = ["", "+", "-"]
+        rf.valid_skips = []
+        rf.has_ctone = False
+        rf.has_tuning_step = False
+        rf.has_bank = False
+        rf.memory_bounds = (1, 128)
+        rf.can_odd_split = False
+        return rf
+
+    @classmethod
+    def match_model(cls, filedata, filename):
+        return (len(filedata) == cls._memsize) and \
+            filedata[-16:] != "IcomCloneFormat3"
+
+    def sync_in(self):
+        self._mmap = puxing_2r_download(self)
+        self.process_mmap()
+
+    def sync_out(self):
+        puxing_2r_upload(self)
+
+    def process_mmap(self):
+        self._memobj = bitwise.parse(PUXING_2R_MEM_FORMAT, self._mmap)
+
+    def get_memory(self, number):
+        _mem = self._memobj.memory[number-1]
+
+        mem = chirp_common.Memory()
+        mem.number = number
+        if _mem.get_raw()[0:4] == "\xff\xff\xff\xff":
+            mem.empty = True
+            return mem
+
+        mem.freq = int(_mem.freq) * 10
+        mem.offset = int(_mem.offset) * 10
+        mem.mode = _mem.iswide and "FM" or "NFM"
+        mem.duplex = PX2R_DUPLEX[_mem.duplex]
+        mem.power = PX2R_POWER_LEVELS[_mem.ishigh]
+
+        if _mem.tx_tone >= 0x33:
+            mem.dtcs = chirp_common.DTCS_CODES[_mem.tx_tone - 0x33]
+            mem.tmode = "DTCS"
+            mem.dtcs_polarity = \
+                (_mem.txdtcsinv and "R" or "N") + \
+                (_mem.rxdtcsinv and "R" or "N")
+        elif _mem.tx_tone:
+            mem.rtone = chirp_common.TONES[_mem.tx_tone - 1]
+            mem.tmode = _mem.rx_tone and "TSQL" or "Tone"
+
+        count = 0
+        for i in _mem.name:
+            if i == 0xFF:
+                break
+            try:
+                mem.name += PX2R_CHARSET[i]
+            except Exception:
+                print "Unknown name char %i: 0x%02x (mem %i)" % (count,
+                                                                 i, number)
+                mem.name += " "
+            count += 1
+        mem.name = mem.name.rstrip()
+
+        return mem
+
+    def set_memory(self, mem):
+        _mem = self._memobj.memory[mem.number-1]
+
+        if mem.empty:
+            _mem.set_raw("\xff" * 16)
+            return
+
+        _mem.freq = mem.freq / 10
+        _mem.offset = mem.offset / 10
+        _mem.iswide = mem.mode == "FM"
+        _mem.duplex = PX2R_DUPLEX.index(mem.duplex)
+        _mem.ishigh = mem.power == PX2R_POWER_LEVELS[1]
+
+        if mem.tmode == "DTCS":
+            _mem.tx_tone = chirp_common.DTCS_CODES.index(mem.dtcs) + 0x33
+            _mem.rx_tone = chirp_common.DTCS_CODES.index(mem.dtcs) + 0x33
+            _mem.txdtcsinv = mem.dtcs_polarity[0] == "R" 
+            _mem.rxdtcsinv = mem.dtcs_polarity[1] == "R"
+        elif mem.tmode in ["Tone", "TSQL"]:
+            _mem.tx_tone = chirp_common.TONES.index(mem.rtone) + 1
+            _mem.rx_tone = mem.tmode == "TSQL" and int(_mem.tx_tone) or 0
+        else:
+            _mem.tx_tone = 0
+            _mem.rx_tone = 0
+
+        for i in range(0, 5):
+            try:
+                _mem.name[i] = PX2R_CHARSET.index(mem.name[i])
+            except IndexError:
+                _mem.name[i] = 0xFF
+
+    def get_raw_memory(self, number):
+        return repr(self._memobj.memory[number-1])
+
diff --git a/chirp/pyPEG.py b/chirp/pyPEG.py
new file mode 100644
index 0000000..381a403
--- /dev/null
+++ b/chirp/pyPEG.py
@@ -0,0 +1,312 @@
+# YPL parser 0.45
+
+# written by VB.
+
+import re
+
+class keyword(str): pass
+class code(str): pass
+class ignore(object):
+    def __init__(self, regex_text):
+        self.regex = re.compile(regex_text)
+
+class _and(object):
+    def __init__(self, something):
+        self.obj = something
+
+class _not(_and): pass
+
+class Name(str):
+    def __init__(self, *args):
+        self.line = 0
+        self.file = ""
+
+word_regex = re.compile(r"\w+")
+rest_regex = re.compile(r".*")
+ignoring = ignore("")
+
+def skip(skipper, text, pattern, skipWS, skipComments):
+    if skipWS:
+        t = text.strip()
+    else:
+        t = text
+    if skipComments:
+        try:
+            while True:
+                skip, t = skipper.parseLine(t, skipComments, [], skipWS, None)
+                if skipWS:
+                    t = t.strip()
+        except: pass
+    return t
+
+class parser(object):
+    def __init__(self, another = False):
+        self.restlen = -1 
+        if not(another):
+            self.skipper = parser(True)
+        else:
+            self.skipper = self
+        self.lines = None
+        self.textlen = 0
+        self.memory = {}
+        self.packrat = False
+
+    # parseLine():
+    #   textline:       text to parse
+    #   pattern:        pyPEG language description
+    #   resultSoFar:    parsing result so far (default: blank list [])
+    #   skipWS:         Flag if whitespace should be skipped (default: True)
+    #   skipComments:   Python functions returning pyPEG for matching comments
+    #   
+    #   returns:        pyAST, textrest
+    #
+    #   raises:         SyntaxError(reason) if textline is detected not being in language
+    #                   described by pattern
+    #
+    #                   SyntaxError(reason) if pattern is an illegal language description
+
+    def parseLine(self, textline, pattern, resultSoFar = [], skipWS = True, skipComments = None):
+        name = None
+        _textline = textline
+        _pattern = pattern
+        _packrat = self.packrat
+        _memory = self.memory
+
+        def R(result, text):
+            if self.restlen == -1:
+                self.restlen = len(text)
+            else:
+                self.restlen = min(self.restlen, len(text))
+            res = resultSoFar
+            if name and result:
+                res.append((name, result))
+            elif name:
+                res.append((name, []))
+            elif result:
+                if type(result) is type([]):
+                    res.extend(result)
+                else:
+                    res.extend([result])
+            if _packrat:
+                if name:
+                    _memory[(len(_textline), id(_pattern))] = (res, text)
+            return res, text
+
+        def syntaxError():
+            if _packrat:
+                if name:
+                    _memory[(len(_textline), id(_pattern))] = False
+            raise SyntaxError()
+
+        if type(pattern) is type(lambda x: 0):
+            if _packrat:
+                try:
+                    result = _memory[(len(_textline), id(_pattern))]
+                    if result:
+                        return result
+                    else:
+                        raise SyntaxError()
+                except: pass
+
+            if pattern.__name__[0] != "_":
+                name = Name(pattern.__name__)
+                name.line = self.lineNo()
+
+            pattern = pattern()
+            if type(pattern) is type(lambda x: 0):
+                pattern = (pattern,)
+
+        text = skip(self.skipper, textline, pattern, skipWS, skipComments)
+
+        pattern_type = type(pattern)
+
+        if pattern_type is type(""):
+            if text[:len(pattern)] == pattern:
+                text = skip(self.skipper, text[len(pattern):], pattern, skipWS, skipComments)
+                return R(None, text)
+            else:
+                syntaxError()
+
+        elif pattern_type is type(keyword("")):
+            m = word_regex.match(text)
+            if m:
+                if m.group(0) == pattern:
+                    text = skip(self.skipper, text[len(pattern):], pattern, skipWS, skipComments)
+                    return R(None, text)
+                else:
+                    syntaxError()
+            else:
+                syntaxError()
+
+        elif pattern_type is type(_not("")):
+            try:
+                r, t = self.parseLine(text, pattern.obj, [], skipWS, skipComments)
+            except:
+                return resultSoFar, textline
+            syntaxError()
+
+        elif pattern_type is type(_and("")):
+            r, t = self.parseLine(text, pattern.obj, [], skipWS, skipComments)
+            return resultSoFar, textline
+
+        elif pattern_type is type(word_regex) or pattern_type is type(ignoring):
+            if pattern_type is type(ignoring):
+                pattern = pattern.regex
+            m = pattern.match(text)
+            if m:
+                text = skip(self.skipper, text[len(m.group(0)):], pattern, skipWS, skipComments)
+                if pattern_type is type(ignoring):
+                    return R(None, text)
+                else:
+                    return R(m.group(0), text)
+            else:
+                syntaxError()
+
+        elif pattern_type is type((None,)):
+            result = []
+            n = 1
+            for p in pattern:
+                if type(p) is type(0):
+                    n = p
+                else:
+                    if n>0:
+                        for i in range(n):
+                            result, text = self.parseLine(text, p, result, skipWS, skipComments)
+                    elif n==0:
+                        if text == "":
+                            pass
+                        else:
+                            try:
+                                newResult, newText = self.parseLine(text, p, result, skipWS, skipComments)
+                                result, text = newResult, newText
+                            except SyntaxError:
+                                pass
+                    elif n<0:
+                        found = False
+                        while True:
+                            try:
+                                newResult, newText = self.parseLine(text, p, result, skipWS, skipComments)
+                                result, text, found = newResult, newText, True
+                            except SyntaxError:
+                                break
+                        if n == -2 and not(found):
+                            syntaxError()
+                    n = 1
+            return R(result, text)
+
+        elif pattern_type is type([]):
+            result = []
+            found = False
+            for p in pattern:
+                try:
+                    result, text = self.parseLine(text, p, result, skipWS, skipComments)
+                    found = True
+                except SyntaxError:
+                    pass
+                if found:
+                    break
+            if found:
+                return R(result, text)
+            else:
+                syntaxError()
+
+        else:
+            raise SyntaxError("illegal type in grammar: " + str(pattern_type))
+
+    def lineNo(self):
+        if not(self.lines): return ""
+        if self.restlen == -1: return ""
+        parsed = self.textlen - self.restlen
+
+        left, right = 0, len(self.lines)
+
+        while True:
+            mid = (right + left) / 2
+            if self.lines[mid][0] <= parsed:
+                try:
+                    if self.lines[mid + 1][0] >= parsed:
+                        try:
+                            return self.lines[mid + 1][1] + ":" + str(self.lines[mid + 1][2])
+                        except:
+                            return ""
+                    else:
+                        left = mid + 1
+                except:
+                    try:
+                        return self.lines[mid + 1][1] + ":" + str(self.lines[mid + 1][2])
+                    except:
+                        return ""
+            else:
+                right = mid - 1
+            if left > right:
+                return ""
+
+# plain module API
+
+def parseLine(textline, pattern, resultSoFar = [], skipWS = True, skipComments = None, packrat = False):
+    p = parser()
+    p.packrat = packrat
+    text = skip(p.skipper, textline, pattern, skipWS, skipComments)
+    ast, text = p.parseLine(text, pattern, resultSoFar, skipWS, skipComments)
+    return ast, text
+
+# parse():
+#   language:       pyPEG language description
+#   lineSource:     a fileinput.FileInput object
+#   skipWS:         Flag if whitespace should be skipped (default: True)
+#   skipComments:   Python function which returns pyPEG for matching comments
+#   packrat:        use memoization
+#   lineCount:      add line number information to AST
+#   
+#   returns:        pyAST
+#
+#   raises:         SyntaxError(reason), if a parsed line is not in language
+#                   SyntaxError(reason), if the language description is illegal
+
+def parse(language, lineSource, skipWS = True, skipComments = None, packrat = False, lineCount = True):
+    lines, lineNo = [], 0
+
+    while type(language) is type(lambda x: 0):
+        language = language()
+
+    orig, ld = "", 0
+    for line in lineSource:
+        if lineSource.isfirstline():
+            ld = 1
+        else:
+            ld += 1
+        lines.append((len(orig), lineSource.filename(), lineSource.lineno() - 1))
+        orig += line
+    textlen = len(orig)
+
+    try:
+        p = parser()
+        p.packrat = packrat
+        p.textlen = len(orig)
+        if lineCount:
+            p.lines = lines
+        else:
+            p.line = None
+        text = skip(p.skipper, orig, language, skipWS, skipComments)
+        result, text = p.parseLine(text, language, [], skipWS, skipComments)
+        if text:
+            raise SyntaxError()
+
+    except SyntaxError, msg:
+        parsed = textlen - p.restlen
+        textlen = 0
+        nn, lineNo, file = 0, 0, ""
+        for n, ld, l in lines:
+            if n >= parsed:
+                break
+            else:
+                lineNo = l
+                nn += 1
+                file = ld
+
+        lineNo += 1
+        nn -= 1
+        lineCont = orig.splitlines()[nn]
+        raise SyntaxError("syntax error in " + file + ":" + str(l) + ": " + lineCont)
+
+    return result
diff --git a/chirp/radioreference.py b/chirp/radioreference.py
new file mode 100644
index 0000000..6db0ceb
--- /dev/null
+++ b/chirp/radioreference.py
@@ -0,0 +1,180 @@
+# 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 chirp import chirp_common, errors
+try:
+    from suds.client import Client
+    HAVE_SUDS = True
+except ImportError:
+    HAVE_SUDS = False
+
+MODES = {
+    "FM"    : "FM",
+    "AM"    : "AM",
+    "FMN"   : "NFM",
+    "D-STAR": "DV",
+    "USB"   : "USB",
+    "LSB"   : "LSB",
+    "P25"   : "P25",
+}
+
+class RadioReferenceRadio(chirp_common.NetworkSourceRadio):
+    """RadioReference.com data source"""
+    VENDOR = "Radio Reference LLC"
+    MODEL = "RadioReference.com"
+
+    URL = "http://api.radioreference.com/soap2/?wsdl"
+    APPKEY = "46785108"
+
+    def __init__(self, *args, **kwargs):
+        chirp_common.NetworkSourceRadio.__init__(self, *args, **kwargs)
+
+        if not HAVE_SUDS:
+            raise errors.RadioError(
+                "Suds library required for RadioReference.com import.\n" + \
+                "Try installing your distribution's python-suds package.")
+
+        self._auth = {"appKey": self.APPKEY, "username": "", "password": ""}
+        self._client = Client(self.URL)
+        self._freqs = None
+        self._modes = None
+        self._zip = None
+
+    def set_params(self, zipcode, username, password):
+        """Set the parameters to be used for a query"""
+        self._zip = zipcode
+        self._auth["username"] = username
+        self._auth["password"] = password
+
+    def do_fetch(self):
+        """Fetches frequencies for all subcategories in a county."""
+        self._freqs = []
+
+        zipcode = self._client.service.getZipcodeInfo(self._zip, self._auth)
+        county = self._client.service.getCountyInfo(zipcode.ctid, self._auth)
+
+        status = chirp_common.Status()
+        status.max = 0
+        for cat in county.cats:
+            status.max += len(cat.subcats)
+        status.max += len(county.agencyList)
+
+        for cat in county.cats:
+            print "Fetching category:", cat.cName
+            for subcat in cat.subcats:
+                print "\t", subcat.scName
+                result = self._client.service.getSubcatFreqs(subcat.scid,
+                                                             self._auth)
+                self._freqs += result
+                status.cur += 1
+                self.status_fn(status)
+        status.max -= len(county.agencyList)
+        for agency in county.agencyList:
+            agency = self._client.service.getAgencyInfo(agency.aid, self._auth)
+            for cat in agency.cats:
+                status.max += len(cat.subcats)
+            for cat in agency.cats:
+                print "Fetching category:", cat.cName
+                for subcat in cat.subcats:
+                    print "\t", subcat.scName
+                    result = self._client.service.getSubcatFreqs(subcat.scid,
+                                                                 self._auth)
+                    self._freqs += result
+                    status.cur += 1
+                    self.status_fn(status)
+
+    def get_features(self):
+        if not self._freqs:
+            self.do_fetch()
+
+        rf = chirp_common.RadioFeatures()
+        rf.memory_bounds = (0, len(self._freqs)-1)
+        rf.has_bank = False
+        rf.has_ctone = False
+        rf.valid_tmodes = ["", "TSQL", "DTCS"]
+        return rf
+
+    def get_raw_memory(self, number):
+        return repr(self._freqs[number])
+
+    def get_memory(self, number):
+        if not self._freqs:
+            self.do_fetch()
+
+        freq = self._freqs[number]
+
+        mem = chirp_common.Memory()
+        mem.number = number
+
+        mem.name = freq.alpha or freq.descr or ""
+        mem.freq = chirp_common.parse_freq(str(freq.out))
+        if freq["in"] == 0.0:
+            mem.duplex = ""
+        else:
+            mem.duplex = "split"
+            mem.offset = chirp_common.parse_freq(str(freq["in"]))
+        if freq.tone is not None:
+            if str(freq.tone) == "CSQ": # Carrier Squelch
+                mem.tmode = ""
+            else:
+                try:
+                    tone, tmode = freq.tone.split(" ")
+                except Exception:
+                    tone, tmode = None, None
+                if tmode == "PL":
+                    mem.tmode = "TSQL"
+                    mem.rtone = mem.ctone = float(tone)
+                elif tmode == "DPL":
+                    mem.tmode = "DTCS"
+                    mem.dtcs = int(tone)
+                else:
+                    print "Error: unsupported tone"
+                    print freq
+        try:
+            mem.mode = self._get_mode(freq.mode)
+        except KeyError:
+            # skip memory if mode is unsupported
+            mem.empty = True
+            return mem
+        mem.comment = freq.descr.strip()
+
+        return mem
+
+    def _get_mode(self, modeid):
+        if not self._modes:
+            self._modes = {}
+            for mode in self._client.service.getMode("0", self._auth):
+                # sax.text.Text cannot be coerced directly to int
+                self._modes[int(str(mode.mode))] = str(mode.modeName)
+        return MODES[self._modes[int(str(modeid))]]
+
+
+def main():
+    """
+    Usage:
+    cd ~/src/chirp.hg
+    python ./chirp/radioreference.py [ZIPCODE] [USERNAME] [PASSWORD]
+    """
+    import sys
+    rrr = RadioReferenceRadio(None)
+    rrr.set_params(zipcode=sys.argv[1],
+                   username=sys.argv[2],
+                   password=sys.argv[3])
+    rrr.do_fetch()
+    print rrr.get_raw_memory(0)
+    print rrr.get_memory(0)
+
+if __name__ == "__main__":
+    main()
diff --git a/chirp/rfinder.py b/chirp/rfinder.py
new file mode 100644
index 0000000..0879ce5
--- /dev/null
+++ b/chirp/rfinder.py
@@ -0,0 +1,322 @@
+# Copyright 2011 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 urllib
+import hashlib
+import re
+
+from math import pi, cos, acos, sin, atan2
+
+from chirp import chirp_common, CHIRP_VERSION
+
+EARTH_RADIUS = 3963.1
+
+SCHEMA = [
+    "ID",
+    "TRUSTEE",
+    "OUTFREQUENCY",
+    "CITY",
+    "STATE",
+    "COUNTRY",
+    "LATITUDE",
+    "LONGITUDE",
+    "CLUB",
+    "DESCRIPTION",
+    "NOTES",
+    "RANGE",
+    "OFFSETSIGN",
+    "OFFSETFREQ",
+    "PL",
+    "DCS",
+    "REPEATERTYPE",
+    "BAND",
+    "IRLP",
+    "ECHOLINK",
+    "DOC_ID",
+    ]
+
+def deg2rad(deg):
+    """Convert degrees to radians"""
+    return deg * (pi / 180)
+
+def rad2deg(rad):
+    """Convert radians to degrees"""
+    return rad / (pi / 180)
+
+def dm2deg(degrees, minutes):
+    """Convert degrees and minutes to decimal degrees"""
+    return degrees + (minutes / 60.0)
+
+def deg2dm(decdeg):
+    """Convert decimal degrees to degrees and minutes"""
+    degrees = int(decdeg)
+    minutes = (decdeg - degrees) * 60.0
+
+    return degrees, minutes
+
+def nmea2deg(nmea, direction="N"):
+    """Convert NMEA-encoded value to float"""
+    deg = int(nmea) / 100
+    try:
+        minutes = nmea % (deg * 100)
+    except ZeroDivisionError:
+        minutes = int(nmea)
+
+    if direction == "S" or direction == "W":
+        sign = -1
+    else:
+        sign = 1
+
+    return dm2deg(deg, minutes) * sign
+
+def deg2nmea(deg):
+    """Convert degrees to a NMEA-encoded value"""
+    degrees, minutes = deg2dm(deg)
+
+    return (degrees * 100) + minutes
+
+def meters2feet(meters):
+    """Convert meters to feet"""
+    return meters * 3.2808399
+
+def feet2meters(feet):
+    """Convert feet to meters"""
+    return feet * 0.3048
+
+def distance(lat_a, lon_a, lat_b, lon_b):
+    """Calculate the distance between two points"""
+    lat_a = deg2rad(lat_a)
+    lon_a = deg2rad(lon_a)
+    
+    lat_b = deg2rad(lat_b)
+    lon_b = deg2rad(lon_b)
+    
+    earth_radius = EARTH_RADIUS
+    
+    tmp = (cos(lat_a) * cos(lon_a) * \
+               cos(lat_b) * cos(lon_b)) + \
+               (cos(lat_a) * sin(lon_a) * \
+                    cos(lat_b) * sin(lon_b)) + \
+                    (sin(lat_a) * sin(lat_b))
+
+    # Correct round-off error (which is just *silly*)
+    if tmp > 1:
+        tmp = 1
+    elif tmp < -1:
+        tmp = -1
+
+    dist = acos(tmp)
+
+    return dist * earth_radius
+
+def bearing(lat_a, lon_a, lat_b, lon_b):
+    """Calculate the bearing between two points"""
+    lat_me = deg2rad(lat_a)
+    lat_u = deg2rad(lat_b)
+    lon_d = deg2rad(lon_b - lon_a)
+
+    posy = sin(lon_d) * cos(lat_u)
+    posx = cos(lat_me) * sin(lat_u) - \
+        sin(lat_me) * cos(lat_u) * cos(lon_d)
+
+    bear = rad2deg(atan2(posy, posx))
+
+    return (bear + 360) % 360
+
+def fuzzy_to(lat_a, lon_a, lat_b, lon_b):
+    """Calculate a fuzzy distance to a point"""
+    bear = bearing(lat_a, lon_a, lat_b, lon_b)
+
+    dirs = ["N", "NNE", "NE", "ENE", "E",
+            "ESE", "SE", "SSE", "S",
+            "SSW", "SW", "WSW", "W",
+            "WNW", "NW", "NNW"]
+
+    delta = 22.5
+    angle = 0
+
+    direction = "?"
+    for i in dirs:
+        if bear > angle and bear < (angle + delta):
+            direction = i
+        angle += delta
+
+    return direction
+
+class RFinderParser:
+    """Parser for RFinder's data format"""
+    def __init__(self, lat, lon):
+        self.__memories = []
+        self.__cheat = {}
+        self.__lat = lat
+        self.__lon = lon
+
+    def fetch_data(self, user, pw, coords, radius):
+        """Fetches the data for a set of parameters"""
+        print user
+        print pw
+        args = {
+            "email"  : urllib.quote_plus(user),
+            "pass"  : hashlib.new("md5", pw).hexdigest(),
+            "lat"   : "%7.5f" % coords[0],
+            "lon"   : "%8.5f" % coords[1],
+            "radius": "%i" % radius,
+            "vers"  : "CH%s" % CHIRP_VERSION,
+            }
+
+        _url = "https://www.rfinder.net/query.php?%s" % (\
+            "&".join(["%s=%s" % (k,v) for k,v in args.items()]))
+
+        print "Query URL: %s" % _url    
+
+        f = urllib.urlopen(_url)
+        data = f.read()
+        f.close()
+
+        match = re.match("^/#SERVERMSG#/(.*)/#ENDMSG#/", data)
+        if match:
+            raise Exception(match.groups()[0])
+
+        return data
+
+    def _parse_line(self, line):
+        mem = chirp_common.Memory()
+
+        _vals = line.split("|")
+
+        vals = {}
+        for i in range(0, len(SCHEMA)):
+            try:
+                vals[SCHEMA[i]] = _vals[i]
+            except IndexError:
+                print "No such vals %s" % SCHEMA[i]
+        self.__cheat = vals
+
+        mem.name = vals["TRUSTEE"]
+        mem.freq = chirp_common.parse_freq(vals["OUTFREQUENCY"])
+        if vals["OFFSETSIGN"] != "X":
+            mem.duplex = vals["OFFSETSIGN"]
+        if vals["OFFSETFREQ"]:
+            mem.offset = chirp_common.parse_freq(vals["OFFSETFREQ"])
+
+        if vals["PL"] and float(vals["PL"]) != 0:
+            mem.rtone = float(vals["PL"])
+            mem.tmode = "Tone"
+        elif vals["DCS"] and vals["DCS"] != "0":
+            mem.dtcs = int(vals["DCS"])
+            mem.tmode = "DTCS"
+
+        if vals["NOTES"]:
+            mem.comment = vals["NOTES"].strip()
+
+        if vals["LATITUDE"] and vals["LONGITUDE"]:
+            try:
+                lat = float(vals["LATITUDE"])
+                lon = float(vals["LONGITUDE"])
+                dist = distance(self.__lat, self.__lon, lat, lon)
+                bear = fuzzy_to(self.__lat, self.__lon, lat, lon)
+                mem.comment = "(%imi %s) %s" % (dist, bear, mem.comment)
+            except Exception, e:
+                print "Failed to calculate distance: %s" % e
+
+        return mem
+
+    def parse_data(self, data):
+        """Parse the fetched data"""
+        number = 1
+        for line in data.split("\n"):
+            if line.startswith("<"):
+                continue
+            elif not line.strip():
+                continue
+            try:
+                mem = self._parse_line(line)
+                mem.number = number
+                number += 1
+                self.__memories.append(mem)
+            except Exception, e:
+                import traceback, sys
+                traceback.print_exc(file=sys.stdout)
+                print "Error in received data, cannot continue"
+                print e
+                print self.__cheat
+                print line
+                print "\n\n"
+
+    def get_memories(self):
+        """Return the Memory objects associated with the fetched data"""
+        return self.__memories
+
+class RFinderRadio(chirp_common.NetworkSourceRadio):
+    """A network source radio that supports the RFinder repeater directory"""
+    VENDOR = "ITWeRKS"
+    MODEL = "RFinder"
+
+    def __init__(self, *args, **kwargs):
+        chirp_common.NetworkSourceRadio.__init__(self, *args, **kwargs)
+       
+        self._lat = 0
+        self._lon = 0
+        self._user = ""
+        self._pass = ""
+        self._miles = 25
+ 
+        self._rfp = None
+
+    def set_params(self, (lat, lon), miles, email, password):
+        """Sets the parameters to use for the query"""
+        self._lat = lat
+        self._lon = lon
+        self._miles = miles
+        self._user = email
+        self._pass = password
+
+    def do_fetch(self):
+        self._rfp = RFinderParser(self._lat, self._lon)
+
+        self._rfp.parse_data(self._rfp.fetch_data(self._user,
+                                                  self._pass,
+                                                  (self._lat, self._lon),
+                                                  self._miles))
+        
+    def get_features(self):
+        if not self._rfp:
+            self.do_fetch()
+
+        rf = chirp_common.RadioFeatures()
+        rf.memory_bounds = (1, len(self._rfp.get_memories()))
+        rf.has_bank = False
+        rf.has_ctone = False
+        rf.valid_tmodes = ["", "Tone", "TSQL", "DTCS"]
+        rf.valid_modes = ["", "FM", "NFM", "AM", "NAM", "DV"]
+        return rf
+
+    def get_memory(self, number):
+        if not self._rfp:
+            self.do_fetch()
+
+        return self._rfp.get_memories()[number-1]
+
+def _test():
+    rfp = RFinderParser()
+    data = rfp.fetch_data("KK7DS", "dsmith at danplanet.com",
+                          (45.5, -122.91), 25)
+    rfp.parse_data(data)
+
+    for mem in rfp.get_memories():
+        print mem
+
+if __name__ == "__main__":
+    _test()
diff --git a/chirp/settings.py b/chirp/settings.py
new file mode 100644
index 0000000..111c424
--- /dev/null
+++ b/chirp/settings.py
@@ -0,0 +1,290 @@
+# Copyright 2012 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 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/>.
+
+from chirp import chirp_common
+
+class InvalidValueError(Exception):
+    """An invalid value was specified for a given setting"""
+    pass
+
+class InternalError(Exception):
+    """A driver provided an invalid settings object structure"""
+    pass
+
+class RadioSettingValue:
+    """Base class for a single radio setting"""
+    def __init__(self):
+        self._current = None
+        self._has_changed = False
+
+    def changed(self):
+        """Returns True if the setting has been changed since init"""
+        return self._has_changed
+
+    def set_value(self, value):
+        """Sets the current value, triggers changed"""
+        if self._current != None and value != self._current:
+            self._has_changed = True
+        self._current = value
+
+    def get_value(self):
+        """Gets the current value"""
+        return self._current
+
+    def __trunc__(self):
+        return int(self.get_value())
+
+    def __str__(self):
+        return str(self.get_value())
+
+class RadioSettingValueInteger(RadioSettingValue):
+    """An integer setting"""
+    def __init__(self, minval, maxval, current, step=1):
+        RadioSettingValue.__init__(self)
+        self._min = minval
+        self._max = maxval
+        self._step = step
+        self.set_value(current)
+
+    def set_value(self, value):
+        try:
+            value = int(value)
+        except:
+            raise InvalidValueError("An integer is required")
+        if value > self._max or value < self._min:
+            raise InvalidValueError("Value %i not in range %i-%i" % (value,
+                                                                     self._min,
+                                                                     self._max))
+        RadioSettingValue.set_value(self, value)
+
+    def get_min(self):
+        """Returns the minimum allowed value"""
+        return self._min
+
+    def get_max(self):
+        """Returns the maximum allowed value"""
+        return self._max
+
+    def get_step(self):
+        """Returns the step increment"""
+        return self._step
+
+class RadioSettingValueBoolean(RadioSettingValue):
+    """A boolean setting"""
+    def __init__(self, current):
+        RadioSettingValue.__init__(self)
+        self.set_value(current)
+
+    def set_value(self, value):
+        RadioSettingValue.set_value(self, bool(value))
+
+    def __str__(self):
+        return str(bool(self.get_value()))
+
+class RadioSettingValueList(RadioSettingValue):
+    """A list-of-strings setting"""
+    def __init__(self, options, current):
+        RadioSettingValue.__init__(self)
+        self._options = options
+        self.set_value(current)
+
+    def set_value(self, value):
+        if not value in self._options:
+            raise InvalidValueError("%s is not valid for this setting" % value)
+        RadioSettingValue.set_value(self, value)
+
+    def get_options(self):
+        """Returns the list of valid option values"""
+        return self._options
+
+    def __trunc__(self):
+        return self._options.index(self._current)
+
+class RadioSettingValueString(RadioSettingValue):
+    """A string setting"""
+    def __init__(self, minlength, maxlength, current,
+                 autopad=True):
+        RadioSettingValue.__init__(self)
+        self._minlength = minlength
+        self._maxlength = maxlength
+        self._charset = chirp_common.CHARSET_ASCII
+        self._autopad = autopad
+        self.set_value(current)
+
+    def set_charset(self, charset):
+        """Sets the set of allowed characters"""
+        self._charset = charset
+
+    def set_value(self, value):
+        if len(value) < self._minlength or len(value) > self._maxlength:
+            raise InvalidValueError("Value must be between %i and %i chars" % (\
+                    self._minlength, self._maxlength))
+        if self._autopad:
+            value = value.ljust(self._maxlength)
+        for char in value:
+            if char not in self._charset:
+                raise InvalidValueError("Value contains invalid " +
+                                        "character `%s'" % char)
+        RadioSettingValue.set_value(self, value)
+
+    def __str__(self):
+        return self._current
+
+class RadioSettingGroup(object):
+    """A group of settings"""
+    def _validate(self, element):
+        # RadioSettingGroup can only contain RadioSettingGroup objects
+        if not isinstance(element, RadioSettingGroup):
+            raise InternalError("Incorrect type")
+
+    def __init__(self, name, shortname, *elements):
+        self._name = name           # Setting identifier
+        self._shortname = shortname # Short human-readable name/description
+        self.__doc__ = name         # Longer explanation/documentation
+        self._elements = {}
+        self._element_order = []
+        
+        for element in elements:
+            self._validate(element)
+            print "Appending element to %s" % self._name
+            self.append(element)
+
+    def get_name(self):
+        """Returns the group name"""
+        return self._name
+
+    def get_shortname(self):
+        """Returns the short group identifier"""
+        return self._shortname
+
+    def set_doc(self, doc):
+        """Sets the docstring for the group"""
+        self.__doc__ = doc
+
+    def __str__(self):
+        string = "{Settings Group %s:\n" % self._name
+        for element in self._elements.values():
+            string += str(element) + "\n"
+        string += "}"
+        return string
+
+    # Kinda list interface
+
+    def append(self, element):
+        """Adds an element to the group"""
+        self[element.get_name()] = element
+
+    def __iter__(self):
+        class RSGIterator:
+            """Iterator for a RadioSettingsGroup"""
+            def __init__(self, rsg):
+                self.__rsg = rsg
+                self.__i = 0
+            def __iter__(self):
+                return self
+            def next(self):
+                """Next Iterator Interface"""
+                if self.__i >= len(self.__rsg.keys()):
+                    raise StopIteration()
+                e =  self.__rsg[self.__rsg.keys()[self.__i]]
+                self.__i += 1
+                return e
+        return RSGIterator(self)
+
+    # Dictionary interface
+
+    def __len__(self):
+        return len(self._elements)
+
+    def __getitem__(self, name):
+        return self._elements[name]
+
+    def __setitem__(self, name, value):
+        if name in self._element_order:
+            raise KeyError("Duplicate item %s" % name)
+        self._elements[name] = value
+        self._element_order.append(name)
+
+    def items(self):
+        """Returns a key=>value set of elements, like a dict"""
+        return [(name, self._elements[name]) for name in self._element_order]
+
+    def keys(self):
+        """Returns a list of string element names"""
+        return self._element_order
+
+    def values(self):
+        """Returns the list of elements"""
+        return [self._elements[name] for name in self._element_order]
+
+class RadioSetting(RadioSettingGroup):
+    """A single setting, which could be an array of items like a group"""
+    def _validate(self, value):
+        # RadioSetting can only contain RadioSettingValue objects
+        if not isinstance(value, RadioSettingValue):
+            raise InternalError("Incorrect type")
+
+    def changed(self):
+        """Returns True if any of the elements in the group have been changed"""
+        for element in self._elements.values():
+            if element.changed():
+                return True
+        return False
+
+    def __str__(self):
+        return "%s:%s" % (self._name, self.value)
+
+    def __repr__(self):
+        return "[RadioSetting %s:%s]" % (self._name, self._value)
+
+    # Magic foo.value attribute
+    def __getattr__(self, name):
+        if name == "value":
+            if len(self) == 1:
+                return self._elements[self._element_order[0]]
+            else:
+                return self._elements.values()
+        else:
+            return self.__dict__[name]
+
+    def __setattr__(self, name, value):
+        if name == "value":
+            if len(self) == 1:
+                self._elements[self._element_order[0]].set_value(value)
+            else:
+                raise InternalError("Setting %s is not a scalar" % self._name)
+        else:
+            self.__dict__[name] = value
+            
+    # List interface
+
+    def append(self, value):
+        index = len(self._element_order)
+        self._elements[index] = value
+        self._element_order.append(index)
+
+    def __getitem__(self, name):
+        if not isinstance(name, int):
+            raise IndexError("Index `%s' is not an integer" % name)
+        return self._elements[name]
+
+    def __setitem__(self, name, value):
+        if not isinstance(name, int):
+            raise IndexError("Index `%s' is not an integer" % name)
+        if self._elements.has_key(name):
+            self._elements[name].set_value(value)
+        else:
+            self._elements[name] = value
+
diff --git a/chirp/template.py b/chirp/template.py
new file mode 100644
index 0000000..d01dbea
--- /dev/null
+++ b/chirp/template.py
@@ -0,0 +1,128 @@
+# Copyright 2012 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 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/>.
+
+from chirp import chirp_common, directory, memmap
+from chirp import bitwise
+
+# Here is where we define the memory map for the radio. Since
+# We often just know small bits of it, we can use #seekto to skip
+# around as needed.
+#
+# Our fake radio includes just a single array of ten memory objects,
+# With some very basic settings, a 32-bit unsigned integer for the
+# frequency (in Hertz) and an eight-character alpha tag
+#
+MEM_FORMAT = """
+#seekto 0x0000;
+struct {
+  u32 freq;
+  char name[8];
+} memory[10];
+"""
+
+def do_download(radio):
+    """This is your download function"""
+    # NOTE: Remove this in your real implementation!
+    return memmap.MemoryMap("\x00" * 1000)
+
+    # Get the serial port connection
+    serial = radio.pipe
+
+    # Our fake radio is just a simple download of 1000 bytes
+    # from the serial port. Do that one byte at a time and
+    # store them in the memory map
+    data = ""
+    for _i in range(0, 1000):
+        data = serial.read(1)
+
+    return memmap.MemoryMap(data)
+
+def do_upload(radio):
+    """This is your upload function"""
+    # NOTE: Remove this in your real implementation!
+    raise Exception("This template driver does not really work!")
+
+    # Get the serial port connection
+    serial = radio.pipe
+
+    # Our fake radio is just a simple upload of 1000 bytes
+    # to the serial port. Do that one byte at a time, reading
+    # from our memory map
+    for i in range(0, 1000):
+        serial.write(radio.get_mmap()[i])
+
+# Uncomment this to actually register this radio in CHIRP
+# @directory.register
+class TemplateRadio(chirp_common.CloneModeRadio):
+    """Acme Template"""
+    VENDOR = "Acme"    # Replace this with your vendor
+    MODEL = "Template" # Replace this with your model
+    BAUD_RATE = 9600   # Replace this with your baud rate
+
+    # Return information about this radio's features, including
+    # how many memories it has, what bands it supports, etc
+    def get_features(self):
+        rf = chirp_common.RadioFeatures()
+        rf.has_bank = False
+        rf.memory_bounds = (0, 9) # This radio supports memories 0-9
+        rf.valid_bands = [(144000000, 148000000), # Supports 2-meters
+                          (440000000, 450000000), # Supports 70-centimeters
+                          ]
+        return rf
+
+    # Do a download of the radio from the serial port
+    def sync_in(self):
+        self._mmap = do_download(self)
+        self._memobj = bitwise.parse(MEM_FORMAT, self._mmap)
+
+    # Do an upload of the radio to the serial port
+    def sync_out(self):
+        do_upload(self)
+
+    # Return a raw representation of the memory object, which 
+    # is very helpful for development
+    def get_raw_memory(self, number):
+        return repr(self._memobj.memory[number])
+
+    # Extract a high-level memory object from the low-level memory map
+    # This is called to populate a memory in the UI
+    def get_memory(self, number):
+        # Get a low-level memory object mapped to the image
+        _mem = self._memobj.memory[number]
+
+        # Create a high-level memory object to return to the UI
+        mem = chirp_common.Memory()
+
+        mem.number = number                # Set the memory number
+        mem.freq = int(_mem.freq)          # Convert your low-level frequency
+                                           # to Hertz
+        mem.name = str(_mem.name).rstrip() # Set the alpha tag
+
+        # We'll consider any blank (i.e. 0MHz frequency) to be empty
+        if mem.freq == 0:
+            mem.empty = True
+
+        return mem
+
+    # Store details about a high-level memory to the memory map
+    # This is called when a user edits a memory in the UI
+    def set_memory(self, mem):
+        # Get a low-level memory object mapped to the image
+        _mem = self._memobj.memory[mem.number]
+
+        _mem.freq = mem.freq               # Convert to low-level frequency
+                                           # representation
+        _mem.name = mem.name.ljust(8)[:8]  # Store the alpha tag
+
diff --git a/chirp/th_uv3r.py b/chirp/th_uv3r.py
new file mode 100644
index 0000000..f06ba45
--- /dev/null
+++ b/chirp/th_uv3r.py
@@ -0,0 +1,267 @@
+# Copyright 2011 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/>.
+
+"""TYT uv3r radio management module"""
+
+import os
+from chirp import chirp_common, bitwise, errors, directory
+from chirp.wouxun_common import do_download, do_upload
+
+if os.getenv("CHIRP_DEBUG"):
+    DEBUG = True
+else:
+    DEBUG = False
+
+def tyt_uv3r_prep(radio):
+    try:
+        radio.pipe.write("PROGRAMa")
+        ack = radio.pipe.read(1)
+        if ack != "\x06":
+            raise errors.RadioError("Radio did not ACK first command")
+    except:
+        raise errors.RadioError("Unable to communicate with the radio")
+
+def tyt_uv3r_download(radio):
+    tyt_uv3r_prep(radio)
+    return do_download(radio, 0x0000, 0x0910, 0x0010)
+
+def tyt_uv3r_upload(radio):
+    tyt_uv3r_prep(radio)
+    return do_upload(radio, 0x0000, 0x0910, 0x0010)
+
+mem_format = """
+struct memory {
+  ul24 duplex:2,
+       bit:1,
+       iswide:1,
+       bits:2,
+       is625:1,
+       freq:17;
+  ul16 offset;
+  ul16 rx_tone;
+  ul16 tx_tone;
+  u8 unknown;
+  u8 name[6];
+};
+
+#seekto 0x0010;
+struct memory memory[128];
+
+#seekto 0x0870;
+u8 emptyflags[16];
+u8 skipflags[16];
+"""
+
+THUV3R_DUPLEX = ["", "+", "-"]
+THUV3R_CHARSET = "".join([chr(ord("0") + x) for x in range(0, 10)] +
+                         [" -*+"] +
+                         [chr(ord("A") + x) for x in range(0, 26)] +
+                         ["_/"])
+ at directory.register
+class TYTUV3RRadio(chirp_common.CloneModeRadio):
+    VENDOR = "TYT"
+    MODEL = "TH-UV3R"
+    BAUD_RATE = 2400
+    _memsize = 2320
+
+    def get_features(self):
+        rf = chirp_common.RadioFeatures()
+        rf.has_bank = False
+        rf.has_tuning_step = False
+        rf.has_cross = True
+        rf.memory_bounds = (1, 128)
+        rf.valid_tmodes = ["", "Tone", "TSQL", "DTCS", "Cross"]
+        rf.valid_cross_modes = ["Tone->Tone",
+                                "Tone->DTCS", "DTCS->Tone",
+                                "->Tone", "->DTCS"]
+        rf.valid_skips = []
+        rf.valid_modes = ["FM", "NFM"]
+        rf.valid_name_length = 6
+        rf.valid_characters = THUV3R_CHARSET
+        rf.valid_bands = [(136000000, 470000000)]
+        rf.valid_tuning_steps = [5.0, 6.25, 10.0, 12.5, 25.0, 37.50,
+                                 50.0, 100.0]
+        rf.valid_skips = ["", "S"]
+        return rf
+
+    def sync_in(self):
+        self.pipe.setTimeout(2)
+        self._mmap = tyt_uv3r_download(self)
+        self.process_mmap()
+
+    def sync_out(self):
+        tyt_uv3r_upload(self)
+
+    def process_mmap(self):
+        self._memobj = bitwise.parse(mem_format, self._mmap)
+
+    def get_raw_memory(self, number):
+        return repr(self._memobj.memory[number - 1])
+
+    def _decode_tone_value(self, value):
+        if value == 0xFFFF:
+            return "", "N", 0
+        elif value & 0x8000:
+            # FIXME: rev pol
+            pol = value & 0x4000 and "R" or "N"
+            return "DTCS", pol, int("%x" % (value & 0x0FFF))
+        else:
+            return "Tone", "N", int("%x" % value) / 10.0
+
+    def _decode_tone(self, mem, _mem):
+        tx_mode, tpol, tx_tone = self._decode_tone_value(_mem.tx_tone)
+        rx_mode, rpol, rx_tone = self._decode_tone_value(_mem.rx_tone)
+
+        if rx_mode == tx_mode == "":
+            return
+
+        mem.dtcs_polarity = "%s%s" % (tpol, rpol)
+
+        if rx_mode == tx_mode == "DTCS":
+            # Break this for now until we can support this in chirp
+            tx_tone = rx_tone
+
+        if rx_mode in ["", tx_mode] and rx_tone in [0, tx_tone]:
+            mem.tmode = rx_mode == "Tone" and "TSQL" or tx_mode
+            if mem.tmode == "DTCS":
+                mem.dtcs = tx_tone
+            elif mem.tmode == "TSQL":
+                mem.ctone = tx_tone
+            else:
+                mem.rtone = tx_tone
+            return
+
+        mem.cross_mode = "%s->%s" % (tx_mode, rx_mode)
+        mem.tmode = "Cross"
+        if tx_mode == "Tone":
+            mem.rtone = tx_tone
+        elif tx_mode == "DTCS":
+            mem.dtcs = tx_tone
+        if rx_mode == "Tone":
+            mem.ctone = rx_tone
+        elif rx_mode == "DTCS":
+            mem.dtcs = rx_tone # No support for different codes yet
+
+    def _encode_tone(self, mem, _mem):
+        if mem.tmode == "":
+            _mem.tx_tone = _mem.rx_tone = 0xFFFF
+            return
+
+        def _tone(val):
+            return int("%i" % (val * 10), 16)
+        def _dcs(val, pol):
+            polmask = pol == "R" and 0xC000 or 0x8000
+            return int("%i" % (val), 16) | polmask
+
+        rx_tone = tx_tone = 0xFFFF
+
+        if mem.tmode == "Tone":
+            rx_mode = ""
+            tx_mode = "Tone"
+            tx_tone = _tone(mem.rtone)
+        elif mem.tmode == "TSQL":
+            rx_mode = tx_mode = "Tone"
+            rx_tone = tx_tone = _tone(mem.ctone)
+        elif mem.tmode == "DTCS":
+            rx_tone = tx_tone = "DTCS"
+            tx_tone = _dcs(mem.dtcs, mem.dtcs_polarity[0])
+            rx_tone = _dcs(mem.dtcs, mem.dtcs_polarity[1])
+        elif mem.tmode == "Cross":
+            tx_mode, rx_mode = mem.cross_mode.split("->", 1)
+            if tx_mode == "DTCS":
+                tx_tone = _dcs(mem.dtcs, mem.dtcs_polarity[0])
+            elif tx_mode == "Tone":
+                tx_tone = _tone(mem.rtone)
+            if rx_mode == "DTCS":
+                rx_tone = _dcs(mem.dtcs, mem.dtcs_polarity[1])
+            elif rx_mode == "Tone":
+                rx_tone = _tone(mem.ctone)
+
+        _mem.rx_tone = rx_tone
+        _mem.tx_tone = tx_tone            
+
+    def get_memory(self, number):
+        _mem = self._memobj.memory[number - 1]
+        mem = chirp_common.Memory()
+        mem.number = number
+
+        bit = 1 << ((number - 1) % 8)
+        byte = (number - 1) / 8
+
+        if self._memobj.emptyflags[byte] & bit:
+            mem.empty = True
+            return mem
+
+        mult = _mem.is625 and 6250 or 5000
+        mem.freq = _mem.freq * mult
+        mem.offset = _mem.offset * 5000
+        mem.duplex = THUV3R_DUPLEX[_mem.duplex]
+        mem.mode = _mem.iswide and "FM" or "NFM"
+        self._decode_tone(mem, _mem)
+        mem.skip = (self._memobj.skipflags[byte] & bit) and "S" or ""
+
+        for char in _mem.name:
+            try:
+                c = THUV3R_CHARSET[char]
+            except:
+                c = ""
+            mem.name += c
+        mem.name = mem.name.rstrip()
+
+        return mem
+
+    def set_memory(self, mem):
+        _mem = self._memobj.memory[mem.number - 1]
+        bit = 1 << ((mem.number - 1) % 8)
+        byte = (mem.number - 1) / 8
+
+        if mem.empty:
+            self._memobj.emptyflags[byte] |= bit
+            _mem.set_raw("\xFF" * 16)
+            return
+
+        self._memobj.emptyflags[byte] &= ~bit
+
+        if chirp_common.is_fractional_step(mem.freq):
+            mult = 6250
+            _mem.is625 = True
+        else:
+            mult = 5000
+            _mem.is625 = False
+        _mem.freq = mem.freq / mult
+        _mem.offset = mem.offset / 5000
+        _mem.duplex = THUV3R_DUPLEX.index(mem.duplex)
+        _mem.iswide = mem.mode == "FM"
+        self._encode_tone(mem, _mem)
+
+        if mem.skip:
+            self._memobj.skipflags[byte] |= bit
+        else:
+            self._memobj.skipflags[byte] &= ~bit
+
+        name = []
+        for char in mem.name.ljust(6):
+            try:
+                c = THUV3R_CHARSET.index(char)
+            except:
+                c = THUV3R_CHARSET.index(" ")
+            name.append(c)
+        _mem.name = name
+        print repr(_mem)
+
+    @classmethod
+    def match_model(cls, filedata, filename):
+        return len(filedata) == 2320
+
diff --git a/chirp/thd72.py b/chirp/thd72.py
new file mode 100644
index 0000000..2f63e59
--- /dev/null
+++ b/chirp/thd72.py
@@ -0,0 +1,542 @@
+# Copyright 2010 Vernon Mauery <vernon at mauery.org>
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program.  If not, see <http://www.gnu.org/licenses/>.
+
+from chirp import chirp_common, errors, util, directory
+from chirp import bitwise, memmap
+import time, struct, sys
+
+DEBUG = True
+
+# TH-D72 memory map
+# 0x0000..0x0200: startup password and other stuff
+# 0x0200..0x0400: current channel and other settings
+#   0x244,0x246: last menu numbers
+#   0x249: last f menu number
+# 0x0400..0x0c00: APRS settings and likely other settings
+# 0x0c00..0x1500: memory channel flags
+# 0x1500..0x5380: 0-999 channels
+# 0x5380..0x54c0: 0-9 scan channels
+# 0x54c0..0x5560: 0-9 wx channels
+# 0x5560..0x5e00: ?
+# 0x5e00..0x7d40: 0-999 channel names
+# 0x7d40..0x7de0: ?
+# 0x7de0..0x7e30: wx channel names
+# 0x7e30..0x7ed0: ?
+# 0x7ed0..0x7f20: group names
+# 0x7f20..0x8b00: ?
+# 0x8b00..0x9c00: last 20 APRS entries
+# 0x9c00..0xe500: ?
+# 0xe500..0xe7d0: startup bitmap
+# 0xe7d0..0xe800: startup bitmap filename
+# 0xe800..0xead0: gps-logger bitmap
+# 0xe8d0..0xeb00: gps-logger bipmap filename 
+# 0xeb00..0xff00: ?
+# 0xff00..0xffff: stuff?
+
+# memory channel
+# 0 1 2 3  4 5     6            7     8     9    a          b c d e   f
+# [freq ]  ? mode  tmode/duplex rtone ctone dtcs cross_mode [offset]  ?
+
+mem_format = """
+#seekto 0x0000;
+struct {
+  ul16 version;
+  u8   shouldbe32;
+  u8   efs[11];
+  u8   unknown0[3];
+  u8   radio_custom_image;
+  u8   gps_custom_image;
+  u8   unknown1[7];
+  u8   passwd[6];
+} frontmatter;
+
+#seekto 0x0c00;
+struct {
+  u8 disabled:7,
+     unknown0:1;
+  u8 unknown1:7,
+     skip:1;
+} flag[1032];
+
+#seekto 0x1500;
+struct {
+  ul32 freq;
+  u8 unknown1;
+  u8 mode;
+  u8 tone_mode:4,
+     duplex:4;
+  u8 rtone;
+  u8 ctone;
+  u8 dtcs;
+  u8 cross_mode;
+  ul32 offset;
+  u8 unknown2;
+} memory[1032];
+
+#seekto 0x5e00;
+struct {
+    char name[8];
+} channel_name[1000];
+
+#seekto 0x7de0;
+struct {
+    char name[8];
+} wx_name[10];
+
+#seekto 0x7ed0;
+struct {
+    char name[8];
+} group_name[10];
+"""
+
+THD72_SPECIAL = {}
+
+for i in range(0, 10):
+    THD72_SPECIAL["L%i" % i] = 1000 + (i * 2)
+    THD72_SPECIAL["U%i" % i] = 1000 + (i * 2) + 1
+for i in range(0, 10):
+    THD72_SPECIAL["WX%i" % (i + 1)] = 1020 + i
+THD72_SPECIAL["C VHF"] = 1030
+THD72_SPECIAL["C UHF"] = 1031
+
+THD72_SPECIAL_REV = {}
+for k,v in THD72_SPECIAL.items():
+    THD72_SPECIAL_REV[v] = k
+
+TMODES = {
+    0x08 : "Tone",
+    0x04 : "TSQL",
+    0x02 : "DTCS",
+    0x01 : "Cross",
+    0x00 : "",
+}
+TMODES_REV = {
+    ""      : 0x00,
+    "Cross" : 0x01,
+    "DTCS"  : 0x02,
+    "TSQL"  : 0x04,
+    "Tone"  : 0x08,
+}
+
+MODES = {
+    0x00 : "FM",
+    0x01 : "NFM",
+    0x02 : "AM",
+}
+
+MODES_REV = {
+    "FM" : 0x00,
+    "NFM": 0x01,
+    "AM" : 0x2,
+}
+
+DUPLEX = {
+    0x00 : "",
+    0x01 : "+",
+    0x02 : "-",
+    0x04 : "split",
+}
+DUPLEX_REV = {
+    ""  : 0x00,
+    "+" : 0x01,
+    "-" : 0x02,
+    "split" : 0x04,
+}
+
+
+EXCH_R = "R\x00\x00\x00\x00"
+EXCH_W = "W\x00\x00\x00\x00"
+
+
+# Uploads result in "MCP Error" and garbage data in memory
+# Clone driver disabled in favor of error-checking live driver.
+ at directory.register
+class THD72Radio(chirp_common.CloneModeRadio):
+    BAUD_RATE = 9600
+    VENDOR = "Kenwood"
+    MODEL = "TH-D72 (clone mode)"
+    HARDWARE_FLOW = sys.platform == "darwin"  # only OS X driver needs hw flow
+
+    mem_upper_limit = 1022
+    _memsize = 65536
+    _model = "" # FIXME: REMOVE
+    _dirty_blocks = []
+
+    def get_features(self):
+        rf = chirp_common.RadioFeatures()
+        rf.memory_bounds = (0, 1031)
+        rf.valid_bands = [(118000000, 174000000),
+                          (320000000, 524000000)]
+        rf.has_cross = True
+        rf.can_odd_split = True
+        rf.has_dtcs_polarity = False
+        rf.has_tuning_step = False
+        rf.has_bank = False
+        rf.valid_tuning_steps = []
+        rf.valid_modes = MODES_REV.keys()
+        rf.valid_tmodes = TMODES_REV.keys()
+        rf.valid_duplexes = DUPLEX_REV.keys()
+        rf.valid_skips = ["", "S"]
+        rf.valid_characters = chirp_common.CHARSET_ALPHANUMERIC
+        rf.valid_name_length = 8
+        return rf
+
+    def process_mmap(self):
+        self._memobj = bitwise.parse(mem_format, self._mmap)
+        self._dirty_blocks = []
+
+    def _detect_baud(self):
+        for baud in [9600, 19200, 38400, 57600]:
+            self.pipe.setBaudrate(baud)
+            try:
+                self.pipe.write("\r\r")
+            except:
+                break
+            self.pipe.read(32)
+            try:
+                id = self.get_id()
+                print "Radio %s at %i baud" % (id, baud)
+                return True
+            except errors.RadioError:
+                pass
+
+        raise errors.RadioError("No response from radio")
+
+    def get_special_locations(self):
+        return sorted(THD72_SPECIAL.keys())
+
+    def add_dirty_block(self, memobj):
+        block = memobj._offset / 256
+        if block not in self._dirty_blocks:
+            self._dirty_blocks.append(block)
+        self._dirty_blocks.sort()
+        print "dirty blocks:", self._dirty_blocks
+
+    def get_channel_name(self, number):
+        if number < 999:
+            name = str(self._memobj.channel_name[number].name) + '\xff'
+        elif number >= 1020 and number < 1030:
+            number -= 1020
+            name = str(self._memobj.wx_name[number].name) + '\xff'
+        else:
+            return ''
+        return name[:name.index('\xff')].rstrip()
+
+    def set_channel_name(self, number, name):
+        name = name[:8] + '\xff'*8
+        if number < 999:
+            self._memobj.channel_name[number].name = name[:8]
+            self.add_dirty_block(self._memobj.channel_name[number])
+        elif number >= 1020 and number < 1030:
+            number -= 1020
+            self._memobj.wx_name[number].name = name[:8]
+            self.add_dirty_block(self._memobj.wx_name[number])
+
+    def get_memory(self, number):
+        if isinstance(number, str):
+            try:
+                number = THD72_SPECIAL[number]
+            except KeyError:
+                raise errors.InvalidMemoryLocation("Unknown channel %s" % \
+                                                       number)
+
+        if number < 0 or number > (max(THD72_SPECIAL.values()) + 1):
+            raise errors.InvalidMemoryLocation("Number must be between 0 and 999")
+
+        _mem = self._memobj.memory[number]
+        flag = self._memobj.flag[number]
+
+        mem = chirp_common.Memory()
+        mem.number = number
+
+        if number > 999:
+            mem.extd_number = THD72_SPECIAL_REV[number]
+        if flag.disabled == 0x7f:
+            mem.empty = True
+            return mem
+
+        mem.name = self.get_channel_name(number)
+        mem.freq = int(_mem.freq)
+        mem.tmode = TMODES[int(_mem.tone_mode)]
+        mem.rtone = chirp_common.TONES[_mem.rtone]
+        mem.ctone = chirp_common.TONES[_mem.ctone]
+        mem.dtcs = chirp_common.DTCS_CODES[_mem.dtcs]
+        mem.duplex = DUPLEX[int(_mem.duplex)]
+        mem.offset = int(_mem.offset)
+        mem.mode = MODES[int(_mem.mode)]
+
+        if number < 999:
+            mem.skip = chirp_common.SKIP_VALUES[int(flag.skip)]
+            mem.cross_mode = chirp_common.CROSS_MODES[_mem.cross_mode]
+        if number > 999:
+            mem.cross_mode = chirp_common.CROSS_MODES[0]
+            mem.immutable = ["number", "bank", "extd_number", "cross_mode"]
+            if number >= 1020 and number < 1030:
+                mem.immutable += ["freq", "offset", "tone", "mode", "tmode", "ctone", "skip"] # FIXME: ALL
+            else:
+                mem.immutable += ["name"]
+
+        return mem
+
+
+    def set_memory(self, mem):
+        print "set_memory(%d)"%mem.number
+        if mem.number < 0 or mem.number > (max(THD72_SPECIAL.values()) + 1):
+            raise errors.InvalidMemoryLocation("Number must be between 0 and 999")
+
+        # weather channels can only change name, nothing else
+        if mem.number >= 1020 and mem.number < 1030:
+            self.set_channel_name(mem.number, mem.name)
+            return
+
+        flag = self._memobj.flag[mem.number]
+        self.add_dirty_block(self._memobj.flag[mem.number])
+
+        # only delete non-WX channels
+        was_empty = flag.disabled == 0x7f
+        if mem.empty:
+            flag.disabled = 0x7f
+            return
+        flag.disabled = 0
+
+        _mem = self._memobj.memory[mem.number]
+        self.add_dirty_block(_mem)
+        if was_empty:
+            self.initialize(_mem)
+
+        _mem.freq = mem.freq
+
+        if mem.number < 999:
+            self.set_channel_name(mem.number, mem.name)
+
+        _mem.tone_mode = TMODES_REV[mem.tmode]
+        _mem.rtone = chirp_common.TONES.index(mem.rtone)
+        _mem.ctone = chirp_common.TONES.index(mem.ctone)
+        _mem.dtcs = chirp_common.DTCS_CODES.index(mem.dtcs)
+        _mem.cross_mode = chirp_common.CROSS_MODES.index(mem.cross_mode)
+        _mem.duplex = DUPLEX_REV[mem.duplex]
+        _mem.offset = mem.offset
+        _mem.mode = MODES_REV[mem.mode]
+
+        if mem.number < 999:
+            flag.skip = chirp_common.SKIP_VALUES.index(mem.skip)
+
+
+    def sync_in(self):
+        self._detect_baud()
+        self._mmap = self.download()
+        self.process_mmap()
+
+    def sync_out(self):
+        self._detect_baud()
+        if len(self._dirty_blocks):
+            self.upload(self._dirty_blocks)
+        else:
+            self.upload()
+
+    def read_block(self, block, count=256):
+        self.pipe.write(struct.pack("<cBHB", "R", 0, block, 0))
+        r = self.pipe.read(5)
+        if len(r) != 5:
+            raise Exception("Did not receive block response")
+
+        cmd, _zero, _block, zero = struct.unpack("<cBHB", r)
+        if cmd != "W" or _block != block:
+            raise Exception("Invalid response: %s %i" % (cmd, _block))
+
+        data = ""
+        while len(data) < count:
+            data += self.pipe.read(count - len(data))
+
+        self.pipe.write(chr(0x06))
+        if self.pipe.read(1) != chr(0x06):
+            raise Exception("Did not receive post-block ACK!")
+
+        return data
+
+    def write_block(self, block, map):
+        self.pipe.write(struct.pack("<cBHB", "W", 0, block, 0))
+        base = block * 256
+        self.pipe.write(map[base:base+256])
+
+        ack = self.pipe.read(1)
+
+        return ack == chr(0x06)
+
+    def download(self, raw=False, blocks=None):
+        if blocks is None:
+            blocks = range(self._memsize / 256)
+        else:
+            blocks = [b for b in blocks if b < self._memsize/256]
+
+        if self.command("0M PROGRAM") != "0M":
+            raise errors.RadioError("No response from self")
+
+        allblocks = range(self._memsize/256)
+        self.pipe.setBaudrate(57600)
+        self.pipe.getCTS()
+        self.pipe.setRTS()
+        self.pipe.read(1)
+        data = ""
+        print "reading blocks %d..%d" % (blocks[0], blocks[-1])
+        total = len(blocks)
+        count = 0
+        for i in allblocks:
+            if i not in blocks:
+                data += 256*'\xff'
+                continue
+            data += self.read_block(i)
+            count += 1
+            if self.status_fn:
+                s = chirp_common.Status()
+                s.msg = "Cloning from radio"
+                s.max = total
+                s.cur = count
+                self.status_fn(s)
+
+        self.pipe.write("E")
+
+        if raw:
+            return data
+        return memmap.MemoryMap(data)
+
+    def upload(self, blocks=None):
+        if blocks is None:
+            blocks = range((self._memsize / 256) - 2)
+        else:
+            blocks = [b for b in blocks if b < self._memsize/256]
+
+        if self.command("0M PROGRAM") != "0M":
+            raise errors.RadioError("No response from self")
+
+        self.pipe.setBaudrate(57600)
+        self.pipe.getCTS()
+        self.pipe.setRTS()
+        self.pipe.read(1)
+        print "writing blocks %d..%d" % (blocks[0], blocks[-1])
+        total = len(blocks)
+        count = 0
+        for i in blocks:
+            r = self.write_block(i, self._mmap)
+            count += 1
+            if not r:
+                raise errors.RadioError("self NAK'd block %i" % i)
+            if self.status_fn:
+                s = chirp_common.Status()
+                s.msg = "Cloning to radio"
+                s.max = total
+                s.cur = count
+                self.status_fn(s)
+
+        self.pipe.write("E")
+        # clear out blocks we uploaded from the dirty blocks list
+        self._dirty_blocks = [b for b in self._dirty_blocks if b not in blocks]
+
+
+    def command(self, cmd, timeout=0.5):
+        start = time.time()
+
+        data = ""
+        if DEBUG:
+            print "PC->D72: %s" % cmd
+        self.pipe.write(cmd + "\r")
+        while not data.endswith("\r") and (time.time() - start) < timeout:
+            data += self.pipe.read(1)
+        if DEBUG:
+            print "D72->PC: %s" % data.strip()
+        return data.strip()
+
+    def get_id(self):
+        r = self.command("ID")
+        if r.startswith("ID "):
+            return r.split(" ")[1]
+        else:
+            raise errors.RadioError("No response to ID command")
+
+
+    def initialize(self, mmap):
+        mmap[0] = \
+            "\x80\xc8\xb3\x08\x00\x01\x00\x08" + \
+            "\x08\x00\xc0\x27\x09\x00\x00\xff"
+
+
+if __name__ == "__main__":
+    import sys
+    import serial
+    import detect
+    import getopt
+    def fixopts(opts):
+        r = {}
+        for opt in opts:
+            k,v = opt
+            r[k] = v
+        return r
+
+    def usage():
+        print "Usage: %s <-i input.img>|<-o output.img> -p port [[-f first-addr] [-l last-addr] | [-b list,of,blocks]]" % sys.argv[0]
+        sys.exit(1)
+
+    opts, args = getopt.getopt(sys.argv[1:], "i:o:p:f:l:b:")
+    opts = fixopts(opts)
+    first = last = 0
+    blocks = None
+    if '-i' in opts:
+        fname = opts['-i']
+        download = False
+    elif '-o' in opts:
+        fname = opts['-o']
+        download = True
+    else:
+        usage()
+    if '-p' in opts:
+        port = opts['-p']
+    else:
+        usage()
+
+    if '-f' in opts:
+        first = int(opts['-f'],0)
+    if '-l' in opts:
+        last = int(opts['-l'],0)
+    if '-b' in opts:
+        blocks = [int(b, 0) for b in opts['-b'].split(',')]
+        blocks.sort()
+
+    ser = serial.Serial(port=port, baudrate=9600, timeout=0.25)
+    r = THD72Radio(ser)
+    memmax = r._memsize
+    if not download:
+        memmax -= 512
+
+    if blocks is None:
+        if first < 0 or first > (r._memsize - 1):
+            raise errors.RadioError("first address out of range")
+        if (last > 0 and last < first) or last > memmax:
+            raise errors.RadioError("last address out of range")
+        elif last == 0:
+            last = memmax
+        first /= 256
+        if last % 256 != 0:
+            last += 256
+        last /= 256
+        blocks = range(first, last)
+
+    if download:
+        data = r.download(True, blocks)
+        file(fname, "wb").write(data)
+    else:
+        r._mmap = file(fname, "rb").read(r._memsize)
+        r.upload(blocks)
+    print "\nDone"
+
diff --git a/chirp/thuv1f.py b/chirp/thuv1f.py
new file mode 100644
index 0000000..fb3d428
--- /dev/null
+++ b/chirp/thuv1f.py
@@ -0,0 +1,467 @@
+# Copyright 2012 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 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 struct
+
+from chirp import chirp_common, errors, util, directory, memmap
+from chirp import bitwise
+
+from chirp.settings import RadioSetting, RadioSettingGroup, \
+    RadioSettingValueInteger, RadioSettingValueList, \
+    RadioSettingValueList, RadioSettingValueBoolean, \
+    RadioSettingValueString
+
+def uvf1_identify(radio):
+    """Do identify handshake with TYT TH-UVF1"""
+    radio.pipe.write("PROG333")
+    ack = radio.pipe.read(1)
+    if ack != "\x06":
+        raise errors.RadioError("Radio did not respond")
+    radio.pipe.write("\x02")
+    ident = radio.pipe.read(16)
+    print "Ident:\n%s" % util.hexprint(ident)
+    radio.pipe.write("\x06")
+    ack = radio.pipe.read(1)
+    if ack != "\x06":
+        raise errors.RadioError("Radio did not ack identification")
+    return ident
+
+def uvf1_download(radio):
+    """Download from TYT TH-UVF1"""
+    data = uvf1_identify(radio)
+
+    for i in range(0, 0x1000, 0x10):
+        msg = struct.pack(">BHB", ord("R"), i, 0x10)
+        radio.pipe.write(msg)
+        block = radio.pipe.read(0x10 + 4)
+        if len(block) != (0x10 + 4):
+            raise errors.RadioError("Radio sent a short block")
+        radio.pipe.write("\x06")
+        ack = radio.pipe.read(1)
+        if ack != "\x06":
+            raise errors.RadioError("Radio NAKed block")
+        data += block[4:]
+
+        status = chirp_common.Status()
+        status.cur = i
+        status.max = 0x1000
+        status.msg = "Cloning from radio"
+        radio.status_fn(status)
+
+    radio.pipe.write("\x45")
+
+    return memmap.MemoryMap(data)
+
+def uvf1_upload(radio):
+    """Upload to TYT TH-UVF1"""
+    data = uvf1_identify(radio)
+
+    radio.pipe.setTimeout(1)
+
+    if data != radio._mmap[:16]:
+        raise errors.RadioError("Unable to talk to this model")
+
+    for i in range(0, 0x1000, 0x10):
+        addr = i + 0x10
+        msg = struct.pack(">BHB", ord("W"), i, 0x10)
+        msg += radio._mmap[addr:addr+0x10]
+
+        radio.pipe.write(msg)
+        ack = radio.pipe.read(1)
+        if ack != "\x06":
+            print repr(ack)
+            raise errors.RadioError("Radio did not ack block %i" % i)
+        status = chirp_common.Status()
+        status.cur = i
+        status.max = 0x1000
+        status.msg = "Cloning to radio"
+        radio.status_fn(status)
+
+    # End of clone?
+    radio.pipe.write("\x45")
+
+THUV1F_MEM_FORMAT = """
+struct mem {
+  bbcd rx_freq[4];
+  bbcd tx_freq[4];
+  lbcd rx_tone[2];
+  lbcd tx_tone[2];
+  u8 unknown1:1,
+     pttid:2, 
+     unknown2:2,
+     ishighpower:1,
+     unknown3:2;
+  u8 unknown4:4,
+     isnarrow:1,
+     vox:1,
+     bcl:2;
+  u8 unknown5:1,
+     scan:1,
+     unknown6:3,
+     scramble_code:3;
+  u8 unknown7;
+};
+
+struct name {
+  char name[7];
+};
+
+#seekto 0x0020;
+struct mem memory[128];
+
+#seekto 0x0840;
+struct {
+  u8 scans:2,
+     autolk:1,
+     unknown1:5;
+  u8 light:2,
+     unknown6:2,
+     disnm:1,
+     voice:1,
+     beep:1,
+     rxsave:1;
+  u8 led:2,
+     unknown5:3,
+     ani:1,
+     roger:1,
+     dw:1;
+  u8 opnmsg:2,
+     unknown4:1,
+     dwait:1,
+     unknown9:4;
+  u8 squelch;
+  u8 unknown2:4,
+     tot:4;
+  u8 unknown3:4,
+     vox_level:4;
+  u8 pad[10];
+  char ponmsg[6];
+} settings;
+
+#seekto 0x08D0;
+struct name names[128];
+
+"""
+
+LED_LIST = ["Off", "On", "Auto"]
+LIGHT_LIST = ["Purple", "Orange", "Blue"]
+VOX_LIST = ["1", "2", "3", "4", "5", "6", "7", "8"]
+TOT_LIST = ["Off", "30s", "60s", "90s", "120s", "150s", "180s", "210s",
+            "240s", "270s"]
+SCANS_LIST = ["Time", "Carry", "Seek"]
+OPNMSG_LIST = ["Off", "DC", "Message"]
+
+POWER_LEVELS = [chirp_common.PowerLevel("High", watts=5),
+                chirp_common.PowerLevel("Low", watts=1),
+                ]
+
+PTTID_LIST = ["Off", "BOT", "EOT", "Both"]
+BCL_LIST = ["Off", "CSQ", "QT/DQT"]
+CODES_LIST = [x for x in range(1, 9)]
+
+ at directory.register
+class TYTTHUVF1Radio(chirp_common.CloneModeRadio):
+    """TYT TH-UVF1"""
+    VENDOR = "TYT"
+    MODEL = "TH-UVF1"
+
+    def get_features(self):
+        rf = chirp_common.RadioFeatures()
+        rf.memory_bounds = (1, 128)
+        rf.has_bank = False
+        rf.has_ctone = True
+        rf.has_tuning_step = False
+        rf.has_cross = True
+        rf.has_rx_dtcs = True
+        rf.has_settings = True
+        rf.can_odd_split = True
+        rf.valid_duplexes = ["", "-", "+", "split", "off"]
+        rf.valid_tmodes = ["", "Tone", "TSQL", "DTCS", "Cross"]
+        rf.valid_characters = chirp_common.CHARSET_UPPER_NUMERIC + "-"
+        rf.valid_bands = [(136000000, 174000000),
+                          (420000000, 470000000)]
+        rf.valid_skips = ["", "S"]
+        rf.valid_power_levels = POWER_LEVELS
+        rf.valid_modes = ["FM", "NFM"]
+        rf.valid_name_length = 7
+        rf.valid_cross_modes = ["Tone->Tone", "DTCS->DTCS",
+                                "Tone->DTCS", "DTCS->Tone",
+                                "->Tone", "->DTCS", "DTCS->"]
+                                
+        return rf
+
+    def sync_in(self):
+        try:
+            self._mmap = uvf1_download(self)
+        except errors.RadioError:
+            raise
+        except Exception, e:
+            raise errors.RadioError("Failed to communicate with radio: %s" % e)
+        self.process_mmap()
+
+    def sync_out(self):
+        try:
+            uvf1_upload(self)
+        except errors.RadioError:
+            raise
+        except Exception, e:
+            raise errors.RadioError("Failed to communicate with radio: %s" % e)
+
+    @classmethod
+    def match_model(cls, filedata, filename):
+        return filedata.startswith("\x13\x60\x17\x40\x40\x00\x48\x00" +
+                                   "\x35\x00\x39\x00\x47\x00\x52\x00")
+
+    def process_mmap(self):
+        self._memobj = bitwise.parse(THUV1F_MEM_FORMAT, self._mmap)
+
+    def _decode_tone(self, toneval):
+        pol = "N"
+        rawval = (toneval[1].get_bits(0xFF) << 8) | toneval[0].get_bits(0xFF)
+                              
+        if toneval[0].get_bits(0xFF) == 0xFF:
+            mode = ""
+            val = 0
+        elif toneval[1].get_bits(0xC0) == 0xC0:
+            mode = "DTCS"
+            val = int("%x" % (rawval & 0x3FFF))
+            pol = "R"
+        elif toneval[1].get_bits(0x80):
+            mode = "DTCS"
+            val = int("%x" % (rawval & 0x3FFF))
+        else:
+            mode = "Tone"
+            val = int(toneval) / 10.0
+
+        return mode, val, pol
+
+    def _encode_tone(self, _toneval, mode, val, pol):
+        toneval = 0
+        if mode == "Tone":
+            toneval = int("%i" % (val * 10), 16)
+        elif mode == "DTCS":
+            toneval = int("%i" % val, 16)
+            toneval |= 0x8000
+            if pol == "R":
+                toneval |= 0x4000
+        else:
+            toneval = 0xFFFF
+
+        _toneval[0].set_raw(toneval & 0xFF)
+        _toneval[1].set_raw((toneval >> 8) & 0xFF)
+
+    def get_raw_memory(self, number):
+        return repr(self._memobj.memory[number - 1])
+
+    def _is_txinh(self, _mem):
+        raw_tx = ""
+        for i in range(0, 4):
+            raw_tx += _mem.tx_freq[i].get_raw()
+        return raw_tx == "\xFF\xFF\xFF\xFF"
+
+    def get_memory(self, number):
+        _mem = self._memobj.memory[number - 1]
+        mem = chirp_common.Memory()
+        mem.number = number
+        if _mem.get_raw().startswith("\xFF\xFF\xFF\xFF"):
+            mem.empty = True
+            return mem
+
+        mem.freq = int(_mem.rx_freq) * 10
+
+        txfreq = int(_mem.tx_freq) * 10
+        if self._is_txinh(_mem):
+            mem.duplex = "off"
+            mem.offset = 0
+        elif txfreq == mem.freq:
+            mem.duplex = ""
+        elif abs(txfreq - mem.freq) > 70000000:
+            mem.duplex = "split"
+            mem.offset = txfreq
+        elif txfreq < mem.freq:
+            mem.duplex = "-"
+            mem.offset = mem.freq - txfreq
+        elif txfreq > mem.freq:
+            mem.duplex = "+"
+            mem.offset = txfreq - mem.freq
+
+        txmode, txval, txpol = self._decode_tone(_mem.tx_tone)
+        rxmode, rxval, rxpol = self._decode_tone(_mem.rx_tone)
+
+        chirp_common.split_tone_decode(mem,
+                                      (txmode, txval, txpol),
+                                      (rxmode, rxval, rxpol))
+
+        mem.name = str(self._memobj.names[number - 1].name)
+        mem.name = mem.name.replace("\xFF", " ").rstrip()
+
+        mem.skip = not _mem.scan and "S" or ""
+        mem.mode = _mem.isnarrow and "NFM" or "FM"
+        mem.power = POWER_LEVELS[1 - _mem.ishighpower]
+
+        mem.extra = RadioSettingGroup("extra", "Extra Settings")
+
+        rs = RadioSetting("pttid", "PTT ID",
+                          RadioSettingValueList(PTTID_LIST,
+                                                PTTID_LIST[_mem.pttid]))
+        mem.extra.append(rs)
+
+        rs = RadioSetting("vox", "VOX",
+                          RadioSettingValueBoolean(_mem.vox))
+        mem.extra.append(rs)
+
+        rs = RadioSetting("bcl", "Busy Channel Lockout",
+                          RadioSettingValueList(BCL_LIST,
+                                                BCL_LIST[_mem.bcl]))
+        mem.extra.append(rs)
+
+        rs = RadioSetting("scramble_code", "Scramble Code",
+                          RadioSettingValueList(CODES_LIST,
+                                                CODES_LIST[_mem.scramble_code]))
+        mem.extra.append(rs)
+
+        return mem
+
+    def set_memory(self, mem):
+        _mem = self._memobj.memory[mem.number - 1]
+        if mem.empty:
+            _mem.set_raw("\xFF" * 16)
+            return
+
+        if _mem.get_raw() == ("\xFF" * 16):
+            print "Initializing empty memory"
+            _mem.set_raw("\x00" * 16)
+
+        _mem.rx_freq = mem.freq / 10
+        if mem.duplex == "off":
+            for i in range(0, 4):
+                _mem.tx_freq[i].set_raw("\xFF")
+        elif mem.duplex == "split":
+            _mem.tx_freq = mem.offset / 10
+        elif mem.duplex == "-":
+            _mem.tx_freq = (mem.freq - mem.offset) / 10
+        elif mem.duplex == "+":
+            _mem.tx_freq = (mem.freq + mem.offset) / 10
+        else:
+            _mem.tx_freq = mem.freq / 10
+
+        (txmode, txval, txpol), (rxmode, rxval, rxpol) = \
+            chirp_common.split_tone_encode(mem)
+
+        self._encode_tone(_mem.tx_tone, txmode, txval, txpol)
+        self._encode_tone(_mem.rx_tone, rxmode, rxval, rxpol)
+
+        self._memobj.names[mem.number - 1].name = mem.name.ljust(7, "\xFF")
+
+        _mem.scan = mem.skip == ""
+        _mem.isnarrow = mem.mode == "NFM"
+        _mem.ishighpower = mem.power == POWER_LEVELS[0]
+
+        for element in mem.extra:
+            setattr(_mem, element.get_name(), element.value)
+
+    def get_settings(self):
+        _settings = self._memobj.settings
+
+        group = RadioSettingGroup("top", "All Settings")
+
+        group.append(
+            RadioSetting("led", "LED Mode",
+                         RadioSettingValueList(LED_LIST,
+                                               LED_LIST[_settings.led])))
+        group.append(
+            RadioSetting("light", "Light Color",
+                         RadioSettingValueList(LIGHT_LIST,
+                                               LIGHT_LIST[_settings.light])))
+
+        group.append(
+            RadioSetting("squelch", "Squelch Level",
+                          RadioSettingValueInteger(0, 9, _settings.squelch)))
+
+        group.append(
+            RadioSetting("vox_level", "VOX Level",
+                         RadioSettingValueList(VOX_LIST,
+                                               VOX_LIST[_settings.vox_level])))
+
+        group.append(
+            RadioSetting("beep", "Beep",
+                         RadioSettingValueBoolean(_settings.beep)))
+
+        group.append(
+            RadioSetting("ani", "ANI",
+                         RadioSettingValueBoolean(_settings.ani)))
+
+        group.append(
+            RadioSetting("dwait", "D.WAIT",
+                         RadioSettingValueBoolean(_settings.dwait)))
+
+        group.append(
+            RadioSetting("tot", "Timeout Timer",
+                         RadioSettingValueList(TOT_LIST,
+                                               TOT_LIST[_settings.tot])))
+
+        group.append(
+            RadioSetting("roger", "Roger Beep",
+                         RadioSettingValueBoolean(_settings.roger)))
+
+        group.append(
+            RadioSetting("dw", "Dual Watch",
+                         RadioSettingValueBoolean(_settings.dw)))
+
+        group.append(
+            RadioSetting("rxsave", "RX Save",
+                         RadioSettingValueBoolean(_settings.rxsave)))
+
+        group.append(
+            RadioSetting("scans", "Scans",
+                         RadioSettingValueList(SCANS_LIST,
+                                               SCANS_LIST[_settings.scans])))
+
+        group.append(
+            RadioSetting("autolk", "Auto Lock",
+                         RadioSettingValueBoolean(_settings.autolk)))
+
+        group.append(
+            RadioSetting("voice", "Voice",
+                         RadioSettingValueBoolean(_settings.voice)))
+
+        group.append(
+            RadioSetting("opnmsg", "Opening Message",
+                         RadioSettingValueList(OPNMSG_LIST,
+                                               OPNMSG_LIST[_settings.opnmsg])))
+
+        group.append(
+            RadioSetting("disnm", "Display Name",
+                         RadioSettingValueBoolean(_settings.disnm)))
+
+        def _filter(name):
+            print repr(str(name))
+            return str(name).rstrip("\xFF").rstrip()
+
+        group.append(
+            RadioSetting("ponmsg", "Power-On Message",
+                         RadioSettingValueString(0, 7,
+                                                 _filter(_settings.ponmsg))))
+        
+        return group
+
+    def set_settings(self, settings):
+        _settings = self._memobj.settings
+
+        for element in settings:
+            if not isinstance(element, RadioSetting):
+                self.set_settings(element)
+                continue
+            setattr(_settings, element.get_name(), element.value)
diff --git a/chirp/tmv71.py b/chirp/tmv71.py
new file mode 100644
index 0000000..59ef226
--- /dev/null
+++ b/chirp/tmv71.py
@@ -0,0 +1,75 @@
+# Copyright 2010 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/>.
+
+from chirp import chirp_common, errors, util
+from chirp import tmv71_ll
+
+class TMV71ARadio(chirp_common.CloneModeRadio):
+    BAUD_RATE = 9600
+    VENDOR = "Kenwood"
+    MODEL = "TM-V71A"
+
+    mem_upper_limit = 1022
+    _memsize = 32512
+    _model = "" # FIXME: REMOVE
+
+    def get_features(self):
+        rf = chirp_common.RadioFeatures()
+        rf.memory_bounds = (0, 999)
+        return rf
+
+    def _detect_baud(self):
+        for baud in [9600, 19200, 38400, 57600]:
+            self.pipe.setBaudrate(baud)
+            self.pipe.write("\r\r")
+            self.pipe.read(32)
+            try:
+                id = tmv71_ll.get_id(self.pipe)
+                print "Radio %s at %i baud" % (id, baud)
+                return True
+            except errors.RadioError:
+                pass
+
+        raise errors.RadioError("No response from radio")
+
+    def get_raw_memory(self, number):
+        return util.hexprint(tmv71_ll.get_raw_mem(self._mmap, number))
+
+    def get_special_locations(self):
+        return sorted(tmv71_ll.V71_SPECIAL.keys())
+
+    def get_memory(self, number):
+        if isinstance(number, str):
+            try:
+                number = tmv71_ll.V71_SPECIAL[number]
+            except KeyError:
+                raise errors.InvalidMemoryLocation("Unknown channel %s" % \
+                                                       number)
+
+        return tmv71_ll.get_memory(self._mmap, number)
+
+    def set_memory(self, mem):
+        return tmv71_ll.set_memory(self._mmap, mem)
+
+    def erase_memory(self, number):
+        tmv71_ll.set_used(self._mmap, number, 0)
+
+    def sync_in(self):
+        self._detect_baud()
+        self._mmap = tmv71_ll.download(self)
+
+    def sync_out(self):
+        self._detect_baud()
+        tmv71_ll.upload(self)
diff --git a/chirp/tmv71_ll.py b/chirp/tmv71_ll.py
new file mode 100644
index 0000000..dac7f36
--- /dev/null
+++ b/chirp/tmv71_ll.py
@@ -0,0 +1,360 @@
+# Copyright 2010 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 struct, time
+
+from chirp import memmap, chirp_common, errors
+
+DEBUG = True
+
+POS_MODE   = 5
+POS_DUP    = 6
+POS_TMODE  = 6
+POS_RTONE  = 7
+POS_CTONE  = 8
+POS_DTCS   = 9
+POS_OFFSET = 10
+
+MEM_LOC_BASE = 0x1700
+MEM_LOC_SIZE = 16
+MEM_TAG_BASE = 0x5800
+MEM_FLG_BASE = 0x0E00
+
+V71_SPECIAL = {}
+
+for i in range(0, 10):
+    V71_SPECIAL["L%i" % i] = 1000 + (i * 2)
+    V71_SPECIAL["U%i" % i] = 1000 + (i * 2) + 1
+for i in range(0, 10):
+    V71_SPECIAL["WX%i" % (i + 1)] = 1020 + i
+V71_SPECIAL["C VHF"] = 1030
+V71_SPECIAL["C UHF"] = 1031
+
+V71_SPECIAL_REV = {}
+for k,v in V71_SPECIAL.items():
+    V71_SPECIAL_REV[v] = k
+
+def command(s, cmd, timeout=0.5):
+    start = time.time()
+
+    data = ""
+    if DEBUG:
+        print "PC->V71: %s" % cmd
+    s.write(cmd + "\r")
+    while not data.endswith("\r") and (time.time() - start) < timeout:
+        data += s.read(1)
+    if DEBUG:
+        print "V71->PC: %s" % data.strip()
+    return data.strip()
+
+def get_id(s):
+    r = command(s, "ID")
+    if r.startswith("ID "):
+        return r.split(" ")[1]
+    else:
+        raise errors.RadioError("No response to ID command")
+
+EXCH_R = "R\x00\x00\x00"
+EXCH_W = "W\x00\x00\x00"
+
+def read_block(s, block, count=256):
+    s.write(struct.pack("<cHB", "R", block, 0))
+    r = s.read(4)
+    if len(r) != 4:
+        raise Exception("Did not receive block response")
+
+    cmd, _block, zero = struct.unpack("<cHB", r)
+    if cmd != "W" or _block != block:
+        raise Exception("Invalid response: %s %i" % (cmd, _block))
+
+    data = ""
+    while len(data) < count:
+        data += s.read(count - len(data))
+
+    s.write(chr(0x06))
+    if s.read(1) != chr(0x06):
+        raise Exception("Did not receive post-block ACK!")
+
+    return data
+
+def write_block(s, block, map):
+    s.write(struct.pack("<cHB", "W", block, 0))
+    base = block * 256
+    s.write(map[base:base+256])
+
+    ack = s.read(1)
+
+    return ack == chr(0x06)
+
+def download(radio):
+    if command(radio.pipe, "0M PROGRAM") != "0M":
+        raise errors.RadioError("No response from radio")
+
+    data = ""
+    for i in range(0, 0x7F):
+        data += read_block(radio.pipe, i)
+        if radio.status_fn:
+            s = chirp_common.Status()
+            s.msg = "Cloning from radio"
+            s.max = 256 * 0x7E
+            s.cur = len(data)
+            radio.status_fn(s)
+
+    radio.pipe.write("E")
+
+    return memmap.MemoryMap(data)
+
+def upload(radio):
+    if command(radio.pipe, "0M PROGRAM") != "0M":
+        raise errors.RadioError("No response from radio")
+
+    for i in range(0, 0x7F):
+        r = write_block(radio.pipe, i, radio._mmap)
+        if not r:
+            raise errors.RadioError("Radio NAK'd block %i" % i)
+        if radio.status_fn:
+            s = chirp_common.Status()
+            s.msg = "Cloning to radio"
+            s.max = 256 * 0x7E
+            s.cur = 256 * i
+            radio.status_fn(s)
+
+    radio.pipe.write("E")
+
+def get_mem_offset(number):
+    return MEM_LOC_BASE + (MEM_LOC_SIZE * number)
+
+def get_raw_mem(map, number):
+    base = get_mem_offset(number)
+    #print "Offset for %i is %04x" % (number, base)
+    return map[base:base+MEM_LOC_SIZE]
+
+def get_used(map, number):
+    pos = MEM_FLG_BASE + (number * 2)
+    flag = ord(map[pos])
+    print "Flag byte is %02x" % flag
+    return not (flag & 0x80)
+
+def set_used(map, number, freq):
+    pos = MEM_FLG_BASE + (number * 2)
+    if freq == 0:
+        # Erase
+        map[pos] = "\xff\xff"
+    elif int(freq / 100) == 1:
+        map[pos] = "\x05\x00"
+    elif int(freq / 100) == 4:
+        map[pos] = "\x08\x00"
+
+def get_skip(map, number):
+    pos = MEM_FLG_BASE + (number * 2)
+    flag = ord(map[pos+1])
+    if flag & 0x01:
+        return "S"
+    else:
+        return ""
+
+def set_skip(map, number, skip):
+    pos = MEM_FLG_BASE + (number * 2)
+    flag = ord(map[pos+1])
+    if skip:
+        flag |= 0x01
+    else:
+        flag &= ~0x01
+    map[pos+1] = flag
+
+def get_freq(mmap):
+    freq, = struct.unpack("<I", mmap[0:4])
+    return freq / 1000000.0
+
+def set_freq(mmap, freq):
+    mmap[0] = struct.pack("<I", int(freq * 1000000))
+
+def get_name(map, number):
+    base = MEM_TAG_BASE + (8 * number)
+    return map[base:base+6].replace("\xff", "")
+
+def set_name(mmap, number, name):
+    base = MEM_TAG_BASE + (8 * number)
+    mmap[base] = name.ljust(6)[:6].upper()
+
+def get_tmode(mmap):
+    val = ord(mmap[POS_TMODE]) & 0x70
+
+    tmodemap = {
+        0x00 : "",
+        0x40 : "Tone",
+        0x20 : "TSQL",
+        0x10 : "DTCS",
+        }
+
+    return tmodemap[val]
+
+def set_tmode(mmap, tmode):
+    val = ord(mmap[POS_TMODE]) & 0x8F
+
+    tmodemap = {
+        ""     : 0x00,
+        "Tone" : 0x40,
+        "TSQL" : 0x20,
+        "DTCS" : 0x10,
+        }
+
+    mmap[POS_TMODE] = val | tmodemap[tmode]
+
+def get_tone(mmap, offset):
+    val = ord(mmap[offset])
+
+    return chirp_common.TONES[val]
+
+def set_tone(mmap, tone, offset):
+    print tone
+    mmap[offset] = chirp_common.TONES.index(tone)
+
+def get_dtcs(mmap):
+    val = ord(mmap[POS_DTCS])
+
+    return chirp_common.DTCS_CODES[val]
+
+def set_dtcs(mmap, dtcs):
+    mmap[POS_DTCS] = chirp_common.DTCS_CODES.index(dtcs)
+
+def get_duplex(mmap):
+    val = ord(mmap[POS_DUP]) & 0x03
+
+    dupmap = {
+        0x00 : "",
+        0x01 : "+",
+        0x02 : "-",
+        }
+
+    return dupmap[val]
+
+def set_duplex(mmap, duplex):
+    val = ord(mmap[POS_DUP]) & 0xFC
+
+    dupmap = {
+        ""  : 0x00,
+        "+" : 0x01,
+        "-" : 0x02,
+        }
+
+    mmap[POS_DUP] = val | dupmap[duplex]
+
+def get_offset(mmap):
+    val, = struct.unpack("<I", mmap[POS_OFFSET:POS_OFFSET+4])
+    return val / 1000000.0
+
+def set_offset(mmap, offset):
+    mmap[POS_OFFSET] = struct.pack("<I", int(offset * 1000000))
+
+def get_mode(mmap):
+    val = ord(mmap[POS_MODE]) & 0x03
+    modemap = {
+        0x00 : "FM",
+        0x01 : "NFM",
+        0x02 : "AM",
+        }
+
+    return modemap[val]
+
+def set_mode(mmap, mode):
+    val = ord(mmap[POS_MODE]) & 0xFC
+    modemap = {
+        "FM" : 0x00,
+        "NFM": 0x01,
+        "AM" : 0x02,
+        }
+
+    mmap[POS_MODE] = val | modemap[mode]
+
+def get_memory(map, number):
+    if number < 0 or number > (max(V71_SPECIAL.values()) + 1):
+        raise errors.InvalidMemoryLocation("Number must be between 0 and 999")
+
+    mem = chirp_common.Memory()
+    mem.number = number
+
+    if number > 999:
+        mem.extd_number = V71_SPECIAL_REV[number]
+    if not get_used(map, number):
+        mem.empty = True
+        return mem
+
+    mmap = get_raw_mem(map, number)
+
+    mem.freq = get_freq(mmap)
+    mem.name = get_name(map, number)
+    mem.tmode = get_tmode(mmap)
+    mem.rtone = get_tone(mmap, POS_RTONE)
+    mem.ctone = get_tone(mmap, POS_CTONE)
+    mem.dtcs = get_dtcs(mmap)
+    mem.duplex = get_duplex(mmap)
+    mem.offset = get_offset(mmap)
+    mem.mode = get_mode(mmap)
+
+    if number < 999:
+        mem.skip = get_skip(map, number)
+
+    if number > 999:
+        mem.immutable = ["number", "bank", "extd_number", "name"]
+    if number > 1020 and number < 1030:
+        mem.immutable += ["freq"] # FIXME: ALL
+
+    return mem
+
+def initialize(mmap):
+    mmap[0] = \
+        "\x80\xc8\xb3\x08\x00\x01\x00\x08" + \
+        "\x08\x00\xc0\x27\x09\x00\x00\xff"
+
+def set_memory(map, mem):
+    if mem.number < 0 or mem.number > (max(V71_SPECIAL.values()) + 1):
+        raise errors.InvalidMemoryLocation("Number must be between 0 and 999")
+
+    mmap = memmap.MemoryMap(get_raw_mem(map, mem.number))
+
+    if not get_used(map, mem.number):
+        initialize(mmap)
+
+    set_freq(mmap, mem.freq)
+    if mem.number < 999:
+        set_name(map, mem.number, mem.name)
+    set_tmode(mmap, mem.tmode)
+    set_tone(mmap, mem.rtone, POS_RTONE)
+    set_tone(mmap, mem.ctone, POS_CTONE)
+    set_dtcs(mmap, mem.dtcs)
+    set_duplex(mmap, mem.duplex)
+    set_offset(mmap, mem.offset)
+    set_mode(mmap, mem.mode)
+
+    base = get_mem_offset(mem.number)
+    map[base] = mmap.get_packed()
+
+    set_used(map, mem.number, mem.freq)
+    if mem.number < 999:
+        set_skip(map, mem.number, mem.skip)
+
+    return map
+
+if __name__ == "__main__":
+    import sys
+    import serial
+    s = serial.Serial(port=sys.argv[1], baudrate=9600, dsrdtr=True,
+                      timeout=0.25)
+    #s.write("\r\r")
+    #print get_id(s)
+    data = download(s)
+    file(sys.argv[2], "wb").write(data)
+
diff --git a/chirp/util.py b/chirp/util.py
new file mode 100644
index 0000000..de17e11
--- /dev/null
+++ b/chirp/util.py
@@ -0,0 +1,85 @@
+# 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 struct
+
+def hexprint(data):
+    """Return a hexdump-like encoding of @data"""
+    line_sz = 8
+
+    lines = len(data) / line_sz
+    
+    if (len(data) % line_sz) != 0:
+        lines += 1
+        data += "\x00" * ((lines * line_sz) - len(data))
+
+    out = ""
+        
+    for i in range(0, (len(data)/line_sz)):
+        out += "%03i: " % (i * line_sz)
+
+        left = len(data) - (i * line_sz)
+        if left < line_sz:
+            limit = left
+        else:
+            limit = line_sz
+            
+        for j in range(0, limit):
+            out += "%02x " % ord(data[(i * line_sz) + j])
+
+        out += "  "
+
+        for j in range(0, limit):
+            char = data[(i * line_sz) + j]
+
+            if ord(char) > 0x20 and ord(char) < 0x7E:
+                out += "%s" % char
+            else:
+                out += "."
+
+        out += "\n"
+
+    return out
+
+def bcd_encode(val, bigendian=True, width=None):
+    """This is really old and shouldn't be used anymore"""
+    digits = []
+    while val != 0:
+        digits.append(val % 10)
+        val /= 10
+
+    result = ""
+
+    if len(digits) % 2 != 0:
+        digits.append(0)
+
+    while width and width > len(digits):
+        digits.append(0)
+
+    for i in range(0, len(digits), 2):
+        newval = struct.pack("B", (digits[i+1] << 4) | digits[i])
+        if bigendian:
+            result =  newval + result
+        else:
+            result = result + newval
+    
+    return result
+
+def get_dict_rev(thedict, value):
+    """Return the first matching key for a given @value in @dict"""
+    _dict = {}
+    for k, v in thedict.items():
+        _dict[v] = k
+    return _dict[value]
diff --git a/chirp/uv5r.py b/chirp/uv5r.py
new file mode 100644
index 0000000..e7457d8
--- /dev/null
+++ b/chirp/uv5r.py
@@ -0,0 +1,996 @@
+# Copyright 2012 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 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 struct
+import time
+
+from chirp import chirp_common, errors, util, directory, memmap
+from chirp import bitwise
+from chirp.settings import RadioSetting, RadioSettingGroup, \
+    RadioSettingValueInteger, RadioSettingValueList, \
+    RadioSettingValueList, RadioSettingValueBoolean, \
+    RadioSettingValueString
+
+MEM_FORMAT = """
+#seekto 0x0008;
+struct {
+  lbcd rxfreq[4];
+  lbcd txfreq[4];
+  ul16 rxtone;
+  ul16 txtone;
+  u8 unused1:4,
+     scode:4;
+  u8 unknown1[1];
+  u8 unknown2:7,
+     lowpower:1;
+  u8 unknown3:1,
+     wide:1,
+     unknown4:2,
+     bcl:1,
+     scan:1,
+     pttideot:1,
+     pttidbot:1;
+} memory[128];
+
+#seekto 0x0CB2;
+struct {
+  u8 code[5];
+} ani;
+
+#seekto 0x0E28;
+struct {
+  u8 squelch;
+  u8 step;
+  u8 unknown1;
+  u8 save;
+  u8 vox;
+  u8 unknown2;
+  u8 abr;
+  u8 tdr;
+  u8 beep;
+  u8 timeout;
+  u8 unknown3[4];
+  u8 voice;
+  u8 unknown4;
+  u8 dtmfst;
+  u8 unknown5;
+  u8 screv;
+  u8 pttid;
+  u8 pttlt;
+  u8 mdfa;
+  u8 mdfb;
+  u8 bcl;
+  u8 autolk;
+  u8 sftd;
+  u8 unknown6[3];
+  u8 wtled;
+  u8 rxled;
+  u8 txled;
+  u8 almod;
+  u8 band;
+  u8 tdrab;
+  u8 ste;
+  u8 rpste;
+  u8 rptrl;
+  u8 ponmsg;
+  u8 roger;
+} settings[2];
+
+#seekto 0x0E52;
+struct {
+  u8 displayab:1,
+     unknown1:2,
+     fmradio:1,
+     alarm:1,
+     unknown2:1,
+     reset:1,
+     menu:1;
+  u8 unknown3;
+  u8 workmode;
+  u8 keylock;
+} extra;
+
+#seekto 0x0E7E;
+struct {
+  u8 unused1:1,
+     mrcha:7;
+  u8 unused2:1,
+     mrchb:7;
+} wmchannel;
+
+#seekto 0x0F10;
+struct {
+  u8 freq[8];
+  u8 unknown1;
+  u8 offset[4];
+  u8 unknown2;
+  ul16 rxtone;
+  ul16 txtone;
+  u8 unused1:7,
+     band:1;
+  u8 unknown3;
+  u8 unused2:4,
+     scode:4;
+  u8 unknown4;
+  u8 unused3:1
+     step:3,
+     unused4:4;
+  u8 txpower:1,
+     widenarr:1,
+     unknown5:6;
+} vfoa;
+
+#seekto 0x0F30;
+struct {
+  u8 freq[8];
+  u8 unknown1;
+  u8 offset[4];
+  u8 unknown2;
+  ul16 rxtone;
+  ul16 txtone;
+  u8 unused1:7,
+     band:1;
+  u8 unknown3;
+  u8 unused2:4,
+     scode:4;
+  u8 unknown4;
+  u8 unused3:1
+     step:3,
+     unused4:4;
+  u8 txpower:1,
+     widenarr:1,
+     unknown5:6;
+} vfob;
+
+#seekto 0x1000;
+struct {
+  u8 unknown1[8];
+  char name[7];
+  u8 unknown2;
+} names[128];
+
+#seekto 0x1818;
+struct {
+  char line1[7];
+  char line2[7];
+} sixpoweron_msg;
+
+#seekto 0x1828;
+struct {
+  char line1[7];
+  char line2[7];
+} poweron_msg;
+
+struct limit {
+  u8 enable;
+  bbcd lower[2];
+  bbcd upper[2];
+};
+
+#seekto 0x1908;
+struct {
+  struct limit vhf;
+  struct limit uhf;
+} limits_new;
+
+#seekto 0x1910;
+struct {
+  u8 unknown1[2];
+  struct limit vhf;
+  u8 unknown2;
+  u8 unknown3[8];
+  u8 unknown4[2];
+  struct limit uhf;
+} limits_old;
+
+"""
+
+# 0x1EC0 - 0x2000
+
+STEPS = [2.5, 5.0, 6.25, 10.0, 12.5, 25.0]
+STEP_LIST = [str(x) for x in STEPS]
+STEPS = [2.5, 5.0, 6.25, 10.0, 12.5, 20.0, 25.0, 50.0]
+STEP291_LIST = [str(x) for x in STEPS]
+TIMEOUT_LIST = ["%s sec" % x for x in range(15, 615, 15)]
+VOICE_LIST = ["Off", "English", "Chinese"]
+DTMFST_LIST = ["OFF", "DT-ST", "ANI-ST", "DT+ANI"]
+RESUME_LIST = ["TO", "CO", "SE"]
+MODE_LIST = ["Channel", "Name", "Frequency"]
+COLOR_LIST = ["Off", "Blue", "Orange", "Purple"]
+ALMOD_LIST = ["Site", "Tone", "Code"]
+TDRAB_LIST = ["Off", "A", "B"]
+PONMSG_LIST = ["Full", "Message"]
+RPSTE_LIST = ["%s" % x for x in range(1, 11, 1)]
+RPSTE_LIST.insert(0, "OFF")
+STEDELAY_LIST = ["%s ms" % x for x in range(100, 1100, 100)]
+STEDELAY_LIST.insert(0, "OFF")
+SCODE_LIST = ["%s" % x for x in range(1, 16)]
+
+SETTING_LISTS = {
+    "step" : STEP_LIST,
+    "step291" : STEP291_LIST,
+    "timeout" : TIMEOUT_LIST,
+    "voice" : VOICE_LIST,
+    "dtmfst" : DTMFST_LIST,
+    "screv" : RESUME_LIST,
+    "mdfa" : MODE_LIST,
+    "mdfb" : MODE_LIST,
+    "wtled" : COLOR_LIST,
+    "rxled" : COLOR_LIST,
+    "txled" : COLOR_LIST,
+    "almod" : ALMOD_LIST,
+    "tdrab" : TDRAB_LIST,
+    "ponmsg" : PONMSG_LIST,
+    "rpste" : RPSTE_LIST,
+    "stedelay" : STEDELAY_LIST,
+    "scode" : SCODE_LIST,
+}
+
+def _do_status(radio, block):
+    status = chirp_common.Status()
+    status.msg = "Cloning"
+    status.cur = block
+    status.max = radio.get_memsize()
+    radio.status_fn(status)
+
+def validate_orig(ident):
+    try:
+        ver = int(ident[4:7])
+        if ver >= 291:
+            raise errors.RadioError("Radio version %i not supported" % ver)
+    except ValueError:
+        raise errors.RadioError("Radio reported invalid version string")
+
+def validate_291(ident):
+    if ident[4:7] != "\x30\x04\x50":
+        raise errors.RadioError("Radio version not supported")
+
+UV5R_MODEL_ORIG = "\x50\xBB\xFF\x01\x25\x98\x4D"
+UV5R_MODEL_291 = "\x50\xBB\xFF\x20\x12\x07\x25"
+
+IDENTS = [UV5R_MODEL_ORIG,
+          UV5R_MODEL_291,
+          ]
+
+def _firmware_version_from_image(radio):
+    return radio.get_mmap()[0x1838:0x1848]
+
+def _do_ident(radio, magic):
+    serial = radio.pipe
+    serial.setTimeout(1)
+
+    print "Sending Magic: %s" % util.hexprint(magic)
+    serial.write(magic)
+    ack = serial.read(1)
+    
+    if ack != "\x06":
+        if ack:
+            print repr(ack)
+        raise errors.RadioError("Radio did not respond")
+
+    serial.write("\x02")
+    ident = serial.read(8)
+
+    print "Ident:\n%s" % util.hexprint(ident)
+
+    serial.write("\x06")
+    ack = serial.read(1)
+    if ack != "\x06":
+        raise errors.RadioError("Radio refused clone")
+
+    return ident
+
+def _read_block(radio, start, size):
+    msg = struct.pack(">BHB", ord("S"), start, size)
+    radio.pipe.write(msg)
+
+    answer = radio.pipe.read(4)
+    if len(answer) != 4:
+        raise errors.RadioError("Radio refused to send block 0x%04x" % start)
+
+    cmd, addr, length = struct.unpack(">BHB", answer)
+    if cmd != ord("X") or addr != start or length != size:
+        print "Invalid answer for block 0x%04x:" % start
+        print "CMD: %s  ADDR: %04x  SIZE: %02x" % (cmd, addr, length)
+        raise errors.RadioError("Unknown response from radio")
+
+    chunk = radio.pipe.read(0x40)
+    if not chunk:
+        raise errors.RadioError("Radio did not send block 0x%04x" % start)
+    elif len(chunk) != size:
+        print "Chunk length was 0x%04i" % len(chunk)
+        raise errors.RadioError("Radio sent incomplete block 0x%04x" % start)
+    
+    radio.pipe.write("\x06")
+
+    ack = radio.pipe.read(1)
+    if ack != "\x06":
+        raise errors.RadioError("Radio refused to send block 0x%04x" % start)
+    
+    return chunk
+
+def _get_radio_firmware_version(radio):
+    block1 = _read_block(radio, 0x1EC0, 0x40)
+    block2 = _read_block(radio, 0x1F00, 0x40)
+    block = block1 + block2
+    return block[48:64]
+
+def _ident_radio(radio):
+    for magic in IDENTS:
+        error = None
+        try:
+            data = _do_ident(radio, magic)
+            return data
+        except errors.RadioError, e:
+            print e
+            error = e
+            time.sleep(2)
+    if error:
+        raise error
+    raise errors.RadioError("Radio did not respond")
+
+def _do_download(radio):
+    data = _ident_radio(radio)
+
+    # Main block
+    for i in range(0, 0x1800, 0x40):
+        data += _read_block(radio, i, 0x40)
+        _do_status(radio, i)
+
+    # Auxiliary block starts at 0x1ECO (?)
+    for i in range(0x1EC0, 0x2000, 0x40):
+        data += _read_block(radio, i, 0x40)
+
+    return memmap.MemoryMap(data)
+
+def _send_block(radio, addr, data):
+    msg = struct.pack(">BHB", ord("X"), addr, len(data))
+    radio.pipe.write(msg + data)
+
+    ack = radio.pipe.read(1)
+    if ack != "\x06":
+        raise errors.RadioError("Radio refused to accept block 0x%04x" % addr)
+    
+def _do_upload(radio):
+    _ident_radio(radio)
+
+    image_version = _firmware_version_from_image(radio)
+    radio_version = _get_radio_firmware_version(radio)
+    print "Image is %s" % repr(image_version)
+    print "Radio is %s" % repr(radio_version)
+
+    if "BFB" not in radio_version:
+        raise errors.RadioError("Unsupported firmware version: `%s'" %
+                                radio_version)
+
+    # Main block
+    for i in range(0x08, 0x1808, 0x10):
+        _send_block(radio, i - 0x08, radio.get_mmap()[i:i+0x10])
+        _do_status(radio, i)
+
+    if len(radio.get_mmap().get_packed()) == 0x1808:
+        print "Old image, not writing aux block"
+        return # Old image, no aux block
+
+    if image_version != radio_version:
+        raise errors.RadioError("Upload finished, but the 'Other Settings' "
+                                "could not be sent because the firmware "
+                                "version of the image does not match that "
+                                "of the radio")
+
+    # Auxiliary block at radio address 0x1EC0, our offset 0x1808
+    for i in range(0x1EC0, 0x2000, 0x10):
+        addr = 0x1808 + (i - 0x1EC0)
+        _send_block(radio, i, radio.get_mmap()[addr:addr+0x10])
+
+UV5R_POWER_LEVELS = [chirp_common.PowerLevel("High", watts=4.00),
+                     chirp_common.PowerLevel("Low",  watts=1.00)]
+
+UV5R_DTCS = sorted(chirp_common.DTCS_CODES + [645])
+
+UV5R_CHARSET = chirp_common.CHARSET_UPPER_NUMERIC + \
+    "!@#$%^&*()+-=[]:\";'<>?,./"
+
+# Uncomment this to actually register this radio in CHIRP
+ at directory.register
+class BaofengUV5R(chirp_common.CloneModeRadio,
+                  chirp_common.ExperimentalRadio):
+    """Baofeng UV-5R"""
+    VENDOR = "Baofeng"
+    MODEL = "UV-5R"
+    BAUD_RATE = 9600
+
+    _memsize = 0x1808
+
+    @classmethod
+    def get_experimental_warning(cls):
+        return ('Due to the fact that the manufacturer continues to '
+                'release new versions of the firmware with obscure and '
+                'hard-to-track changes, this driver may not work with '
+                'your device. Thus far and to the best knowledge of the '
+                'author, no UV-5R radios have been harmed by using CHIRP. '
+                'However, proceed at your own risk!')
+
+    def get_features(self):
+        rf = chirp_common.RadioFeatures()
+        rf.has_settings = True
+        rf.has_bank = False
+        rf.has_cross = True
+        rf.has_rx_dtcs = True
+        rf.has_tuning_step = False
+        rf.can_odd_split = True
+        rf.valid_name_length = 7
+        rf.valid_characters = UV5R_CHARSET
+        rf.valid_skips = ["", "S"]
+        rf.valid_tmodes = ["", "Tone", "TSQL", "DTCS", "Cross"]
+        rf.valid_cross_modes = ["Tone->Tone", "Tone->DTCS", "DTCS->Tone",
+                                "->Tone", "->DTCS", "DTCS->", "DTCS->DTCS"]
+        rf.valid_power_levels = UV5R_POWER_LEVELS
+        rf.valid_duplexes = ["", "-", "+", "split", "off"]
+        rf.valid_modes = ["FM", "NFM"]
+        rf.valid_bands = [(136000000, 174000000), (400000000, 520000000)]
+        rf.memory_bounds = (0, 127)
+        return rf
+
+    @classmethod
+    def match_model(cls, filedata, filename):
+        return len(filedata) in [0x1808, 0x1948]
+
+    def process_mmap(self):
+        self._memobj = bitwise.parse(MEM_FORMAT, self._mmap)
+
+    def sync_in(self):
+        try:
+            self._mmap = _do_download(self)
+        except errors.RadioError:
+            raise
+        except Exception, e:
+            raise errors.RadioError("Failed to communicate with radio: %s" % e)
+        self.process_mmap()
+
+    def sync_out(self):
+        try:
+            _do_upload(self)
+        except errors.RadioError:
+            raise
+        except Exception, e:
+            raise errors.RadioError("Failed to communicate with radio: %s" % e)
+
+    def get_raw_memory(self, number):
+        return repr(self._memobj.memory[number])
+
+    def _is_txinh(self, _mem):
+        raw_tx = ""
+        for i in range(0, 4):
+            raw_tx += _mem.txfreq[i].get_raw()
+        return raw_tx == "\xFF\xFF\xFF\xFF"
+
+    def get_memory(self, number):
+        _mem = self._memobj.memory[number]
+        _nam = self._memobj.names[number]
+
+        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 self._is_txinh(_mem):
+            mem.duplex = "off"
+            mem.offset = 0
+        elif int(_mem.rxfreq) == int(_mem.txfreq):
+            mem.duplex = ""
+            mem.offset = 0
+        elif abs(int(_mem.rxfreq) * 10 - int(_mem.txfreq) * 10) > 70000000:
+            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 _nam.name:
+            if str(char) == "\xFF":
+                char = " " # The UV-5R software may have 0xFF mid-name
+            mem.name += str(char)
+        mem.name = mem.name.rstrip()
+
+        dtcs_pol = ["N", "N"]
+
+        if _mem.txtone in [0, 0xFFFF]:
+            txmode = ""
+        elif _mem.txtone >= 0x0258:
+            txmode = "Tone"
+            mem.rtone = int(_mem.txtone) / 10.0
+        elif _mem.txtone <= 0x0258:
+            txmode = "DTCS"
+            if _mem.txtone > 0x69:
+                index = _mem.txtone - 0x6A
+                dtcs_pol[0] = "R"
+            else:
+                index = _mem.txtone - 1
+            mem.dtcs = UV5R_DTCS[index]
+        else:
+            print "Bug: txtone is %04x" % _mem.txtone
+
+        if _mem.rxtone in [0, 0xFFFF]:
+            rxmode = ""
+        elif _mem.rxtone >= 0x0258:
+            rxmode = "Tone"
+            mem.ctone = int(_mem.rxtone) / 10.0
+        elif _mem.rxtone <= 0x0258:
+            rxmode = "DTCS"
+            if _mem.rxtone >= 0x6A:
+                index = _mem.rxtone - 0x6A
+                dtcs_pol[1] = "R"
+            else:
+                index = _mem.rxtone - 1
+            mem.rx_dtcs = UV5R_DTCS[index]
+        else:
+            print "Bug: rxtone is %04x" % _mem.rxtone
+
+        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)
+
+        if not _mem.scan:
+            mem.skip = "S"
+
+        mem.power = UV5R_POWER_LEVELS[_mem.lowpower]
+        mem.mode = _mem.wide and "FM" or "NFM"
+
+        mem.extra = RadioSettingGroup("Extra", "extra")
+
+        rs = RadioSetting("bcl", "BCL",
+                          RadioSettingValueBoolean(_mem.bcl))
+        mem.extra.append(rs)
+
+        return mem
+
+    def set_memory(self, mem):
+        _mem = self._memobj.memory[mem.number]
+        _nam = self._memobj.names[mem.number]
+
+        if mem.empty:
+            _mem.set_raw("\xff" * 16)
+            return
+
+        _mem.set_raw("\x00" * 16)
+
+        _mem.rxfreq = mem.freq / 10
+
+        if mem.duplex == "off":
+            for i in range(0, 4):
+                _mem.txfreq[i].set_raw("\xFF")
+        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
+
+        for i in range(0, 7):
+            try:
+                _nam.name[i] = mem.name[i]
+            except IndexError:
+                _nam.name[i] = "\xFF"
+
+        rxmode = txmode = ""
+        if mem.tmode == "Tone":
+            _mem.txtone = int(mem.rtone * 10)
+            _mem.rxtone = 0
+        elif mem.tmode == "TSQL":
+            _mem.txtone = int(mem.ctone * 10)
+            _mem.rxtone = int(mem.ctone * 10)
+        elif mem.tmode == "DTCS":
+            rxmode = txmode = "DTCS"
+            _mem.txtone = UV5R_DTCS.index(mem.dtcs) + 1
+            _mem.rxtone = UV5R_DTCS.index(mem.dtcs) + 1
+        elif mem.tmode == "Cross":
+            txmode, rxmode = mem.cross_mode.split("->", 1)
+            if txmode == "Tone":
+                _mem.txtone = int(mem.rtone * 10)
+            elif txmode == "DTCS":
+                _mem.txtone = UV5R_DTCS.index(mem.dtcs) + 1
+            else:
+                _mem.txtone = 0
+            if rxmode == "Tone":
+                _mem.rxtone = int(mem.ctone * 10)
+            elif rxmode == "DTCS":
+                _mem.rxtone = UV5R_DTCS.index(mem.rx_dtcs) + 1
+            else:
+                _mem.rxtone = 0
+        else:
+            _mem.rxtone = 0
+            _mem.txtone = 0
+
+        if txmode == "DTCS" and mem.dtcs_polarity[0] == "R":
+            _mem.txtone += 0x69
+        if rxmode == "DTCS" and mem.dtcs_polarity[1] == "R":
+            _mem.rxtone += 0x69
+
+        _mem.scan = mem.skip != "S"
+        _mem.wide = mem.mode == "FM"
+        _mem.lowpower = mem.power == UV5R_POWER_LEVELS[1]
+
+        for setting in mem.extra:
+            setattr(_mem, setting.get_name(), setting.value)
+
+    def _is_orig(self):
+        version_tag = _firmware_version_from_image(self)
+        try:
+            if 'BFB' in version_tag:
+                idx = version_tag.index("BFB") + 3
+                version = int(version_tag[idx:idx+3])
+                return version < 291
+        except:
+            pass
+        raise errors.RadioError("Unable to parse version string %s" %
+                                version_tag)
+
+    def _my_version(self):
+        version_tag = _firmware_version_from_image(self)
+        if 'BFB' in version_tag:
+            idx = version_tag.index("BFB") + 3
+            return int(version_tag[idx:idx+3])
+        raise Exception("Unrecognized firmware version string")
+
+    def _get_settings(self):
+        _settings = self._memobj.settings[0]
+        basic = RadioSettingGroup("basic", "Basic Settings")
+        advanced = RadioSettingGroup("advanced", "Advanced Settings")
+        group = RadioSettingGroup("top", "All Settings", basic, advanced)
+
+        rs = RadioSetting("squelch", "Carrier Squelch Level",
+                          RadioSettingValueInteger(0, 9, _settings.squelch))
+        basic.append(rs)
+
+        rs = RadioSetting("dtmfst", "DTMF Sidetone",
+                          RadioSettingValueList(DTMFST_LIST,
+                                                DTMFST_LIST[_settings.dtmfst]))
+        advanced.append(rs)
+
+        rs = RadioSetting("save", "Battery Saver",
+                          RadioSettingValueInteger(0, 4, _settings.save))
+        basic.append(rs)
+
+        rs = RadioSetting("vox", "VOX Sensitivity",
+                          RadioSettingValueInteger(0, 10, _settings.vox))
+        advanced.append(rs)
+
+        rs = RadioSetting("abr", "Backlight Timeout",
+                          RadioSettingValueInteger(0, 24, _settings.abr))
+        basic.append(rs)
+
+        rs = RadioSetting("tdr", "Dual Watch",
+                          RadioSettingValueBoolean(_settings.tdr))
+        advanced.append(rs)
+
+        rs = RadioSetting("tdrab", "Dual Watch Priority",
+                          RadioSettingValueList(TDRAB_LIST,
+                                                TDRAB_LIST[_settings.tdrab]))
+        advanced.append(rs)
+
+        rs = RadioSetting("almod", "Alarm Mode",
+                          RadioSettingValueList(ALMOD_LIST,
+                                                ALMOD_LIST[_settings.almod]))
+        advanced.append(rs)
+
+        rs = RadioSetting("beep", "Beep",
+                          RadioSettingValueBoolean(_settings.beep))
+        basic.append(rs)
+
+        rs = RadioSetting("timeout", "Timeout Timer",
+                          RadioSettingValueList(TIMEOUT_LIST,
+                                                TIMEOUT_LIST[_settings.timeout]))
+        basic.append(rs)
+
+        if self._my_version() >= 251:
+            rs = RadioSetting("voice", "Voice",
+                              RadioSettingValueList(VOICE_LIST,
+                                                    VOICE_LIST[_settings.voice]))
+            advanced.append(rs)
+        else:
+            rs = RadioSetting("voice", "Voice",
+                              RadioSettingValueBoolean(_settings.voice))
+            advanced.append(rs)
+
+        rs = RadioSetting("screv", "Scan Resume",
+                          RadioSettingValueList(RESUME_LIST,
+                                                RESUME_LIST[_settings.screv]))
+        advanced.append(rs)
+
+        rs = RadioSetting("mdfa", "Display Mode (A)",
+                          RadioSettingValueList(MODE_LIST,
+                                                MODE_LIST[_settings.mdfa]))
+        basic.append(rs)
+
+        rs = RadioSetting("mdfb", "Display Mode (B)",
+                          RadioSettingValueList(MODE_LIST,
+                                                MODE_LIST[_settings.mdfb]))
+        basic.append(rs)
+
+        rs = RadioSetting("bcl", "Busy Channel Lockout",
+                          RadioSettingValueBoolean(_settings.bcl))
+        advanced.append(rs)
+
+        rs = RadioSetting("autolk", "Automatic Key Lock",
+                          RadioSettingValueBoolean(_settings.autolk))
+        advanced.append(rs)
+
+        rs = RadioSetting("extra.fmradio", "Broadcast FM Radio",
+                          RadioSettingValueBoolean(self._memobj.extra.fmradio))
+        advanced.append(rs)
+
+        rs = RadioSetting("wtled", "Standby LED Color",
+                          RadioSettingValueList(COLOR_LIST,
+                                                COLOR_LIST[_settings.wtled]))
+        basic.append(rs)
+
+        rs = RadioSetting("rxled", "RX LED Color",
+                          RadioSettingValueList(COLOR_LIST,
+                                                COLOR_LIST[_settings.rxled]))
+        basic.append(rs)
+
+        rs = RadioSetting("txled", "TX LED Color",
+                          RadioSettingValueList(COLOR_LIST,
+                                                COLOR_LIST[_settings.txled]))
+        basic.append(rs)
+
+        rs = RadioSetting("roger", "Roger Beep",
+                          RadioSettingValueBoolean(_settings.roger))
+        basic.append(rs)
+
+        try:
+            _ani = self._memobj.ani.code
+            rs = RadioSetting("ani.code", "ANI Code",
+                              RadioSettingValueInteger(0, 9, _ani[0]),
+                              RadioSettingValueInteger(0, 9, _ani[1]),
+                              RadioSettingValueInteger(0, 9, _ani[2]),
+                              RadioSettingValueInteger(0, 9, _ani[3]),
+                              RadioSettingValueInteger(0, 9, _ani[4]))
+            advanced.append(rs)
+        except Exception:
+            print ("Your ANI code is not five digits, which is not currently"
+                   " supported in CHIRP.")
+
+        rs = RadioSetting("ste", "Squelch Tail Eliminate (HT to HT)",
+                          RadioSettingValueBoolean(_settings.ste))
+        advanced.append(rs)
+
+        rs = RadioSetting("rpste", "Squelch Tail Eliminate (repeater)",
+                          RadioSettingValueList(RPSTE_LIST,
+                                                RPSTE_LIST[_settings.rpste]))
+        advanced.append(rs)
+
+        rs = RadioSetting("rptrl", "STE Repeater Delay",
+                          RadioSettingValueList(STEDELAY_LIST,
+                                                STEDELAY_LIST[_settings.rptrl]))
+        advanced.append(rs)
+
+        rs = RadioSetting("extra.reset", "RESET Menu",
+                          RadioSettingValueBoolean(self._memobj.extra.reset))
+        advanced.append(rs)
+
+        rs = RadioSetting("extra.menu", "All Menus",
+                          RadioSettingValueBoolean(self._memobj.extra.menu))
+        advanced.append(rs)
+
+        if len(self._mmap.get_packed()) == 0x1808:
+            # Old image, without aux block
+            return group
+
+        other = RadioSettingGroup("other", "Other Settings")
+        group.append(other)
+
+        def _filter(name):
+            filtered = ""
+            for char in str(name):
+                if char in chirp_common.CHARSET_ASCII:
+                    filtered += char
+                else:
+                    filtered += " "
+            return filtered
+
+        _msg = self._memobj.sixpoweron_msg
+        rs = RadioSetting("sixpoweron_msg.line1", "6+Power-On Message 1",
+                          RadioSettingValueString(0, 7, _filter(_msg.line1)))
+        other.append(rs)
+        rs = RadioSetting("sixpoweron_msg.line2", "6+Power-On Message 2",
+                          RadioSettingValueString(0, 7, _filter(_msg.line2)))
+        other.append(rs)
+
+        _msg = self._memobj.poweron_msg
+        rs = RadioSetting("poweron_msg.line1", "Power-On Message 1",
+                          RadioSettingValueString(0, 7, _filter(_msg.line1)))
+        other.append(rs)
+        rs = RadioSetting("poweron_msg.line2", "Power-On Message 2",
+                          RadioSettingValueString(0, 7, _filter(_msg.line2)))
+        other.append(rs)
+
+        rs = RadioSetting("ponmsg", "Power-On Message",
+                          RadioSettingValueList(PONMSG_LIST,
+                                                PONMSG_LIST[_settings.ponmsg]))
+        other.append(rs)
+
+        if self._is_orig():
+            limit = "limits_old"
+        else:
+            limit = "limits_new"
+
+        vhf_limit = getattr(self._memobj, limit).vhf
+        rs = RadioSetting("%s.vhf.lower" % limit, "VHF Lower Limit (MHz)",
+                          RadioSettingValueInteger(1, 1000,
+                                                   vhf_limit.lower))
+        other.append(rs)
+
+        rs = RadioSetting("%s.vhf.upper" % limit, "VHF Upper Limit (MHz)",
+                          RadioSettingValueInteger(1, 1000,
+                                                   vhf_limit.upper))
+        other.append(rs)
+
+        rs = RadioSetting("%s.vhf.enable" % limit, "VHF TX Enabled",
+                          RadioSettingValueBoolean(vhf_limit.enable))
+        other.append(rs)
+
+        uhf_limit = getattr(self._memobj, limit).uhf
+        rs = RadioSetting("%s.uhf.lower" % limit, "UHF Lower Limit (MHz)",
+                          RadioSettingValueInteger(1, 1000,
+                                                   uhf_limit.lower))
+        other.append(rs)
+        rs = RadioSetting("%s.uhf.upper" % limit, "UHF Upper Limit (MHz)",
+                          RadioSettingValueInteger(1, 1000,
+                                                   uhf_limit.upper))
+        other.append(rs)
+        rs = RadioSetting("%s.uhf.enable" % limit, "UHF TX Enabled",
+                          RadioSettingValueBoolean(uhf_limit.enable))
+        other.append(rs)
+
+        workmode = RadioSettingGroup("workmode", "Work Mode Settings")
+        group.append(workmode)
+
+        options = ["A", "B"]
+        rs = RadioSetting("extra.displayab", "Display",
+                          RadioSettingValueList(options,
+                                                options[self._memobj.extra.displayab]))
+        workmode.append(rs)
+
+        options = ["Frequency", "Channel"]
+        rs = RadioSetting("extra.workmode", "VFO/MR Mode",
+                          RadioSettingValueList(options,
+                                                options[self._memobj.extra.workmode]))
+        workmode.append(rs)
+
+        rs = RadioSetting("extra.keylock", "Keypad Lock",
+                          RadioSettingValueBoolean(self._memobj.extra.keylock))
+        workmode.append(rs)
+
+        _mrcna = self._memobj.wmchannel.mrcha
+        rs = RadioSetting("wmchannel.mrcha", "MR A Channel",
+                          RadioSettingValueInteger(0, 127, _mrcna))
+        workmode.append(rs)
+
+        _mrcnb = self._memobj.wmchannel.mrchb
+        rs = RadioSetting("wmchannel.mrchb", "MR B Channel",
+                          RadioSettingValueInteger(0, 127, _mrcnb))
+        workmode.append(rs)
+
+        options = ["VHF", "UHF"]
+        rs = RadioSetting("vfoa.band", "VFO A Band",
+                          RadioSettingValueList(options,
+                                                options[self._memobj.vfoa.band]))
+        workmode.append(rs)
+
+        rs = RadioSetting("vfob.band", "VFO B Band",
+                          RadioSettingValueList(options,
+                                                options[self._memobj.vfob.band]))
+        workmode.append(rs)
+
+        options = ["High", "Low"]
+        rs = RadioSetting("vfoa.txpower", "VFO A Power",
+                          RadioSettingValueList(options,
+                                                options[self._memobj.vfoa.txpower]))
+        workmode.append(rs)
+
+        rs = RadioSetting("vfob.txpower", "VFO B Power",
+                          RadioSettingValueList(options,
+                                                options[self._memobj.vfob.txpower]))
+        workmode.append(rs)
+
+        options = ["Wide", "Narrow"]
+        rs = RadioSetting("vfoa.widenarr", "VFO A Bandwidth",
+                          RadioSettingValueList(options,
+                                                options[self._memobj.vfoa.widenarr]))
+        workmode.append(rs)
+
+        rs = RadioSetting("vfob.widenarr", "VFO B Bandwidth",
+                          RadioSettingValueList(options,
+                                                options[self._memobj.vfob.widenarr]))
+        workmode.append(rs)
+
+        options = ["%s" % x for x in range(1, 16)]
+        rs = RadioSetting("vfoa.scode", "VFO A PTT-ID",
+                          RadioSettingValueList(options,
+                                                options[self._memobj.vfoa.scode]))
+        workmode.append(rs)
+
+        rs = RadioSetting("vfob.scode", "VFO B PTT-ID",
+                          RadioSettingValueList(options,
+                                                options[self._memobj.vfob.scode]))
+        workmode.append(rs)
+
+        if self._my_version() >= 291:
+            rs = RadioSetting("vfoa.step", "VFO A Tuning Step",
+                              RadioSettingValueList(STEP291_LIST,
+                                                    STEP291_LIST[self._memobj.vfoa.step]))
+            workmode.append(rs)
+            rs = RadioSetting("vfob.step", "VFO B Tuning Step",
+                              RadioSettingValueList(STEP291_LIST,
+                                                    STEP291_LIST[self._memobj.vfob.step]))
+            workmode.append(rs)
+        else:
+            rs = RadioSetting("vfoa.step", "VFO A Tuning Step",
+                              RadioSettingValueList(STEP_LIST,
+                                                    STEP_LIST[self._memobj.vfoa.step]))
+            workmode.append(rs)
+            rs = RadioSetting("vfob.step", "VFO B Tuning Step",
+                              RadioSettingValueList(STEP_LIST,
+                                                    STEP_LIST[self._memobj.vfob.step]))
+            workmode.append(rs)
+
+        return group
+
+    def get_settings(self):
+        try:
+            return self._get_settings()
+        except:
+            import traceback
+            print "Failed to parse settings:"
+            traceback.print_exc()
+            return None
+
+    def set_settings(self, settings):
+        _settings = self._memobj.settings[0]
+        for element in settings:
+            if not isinstance(element, RadioSetting):
+                self.set_settings(element)
+                continue
+            try:
+                if "." in element.get_name():
+                    bits = element.get_name().split(".")
+                    obj = self._memobj
+                    for bit in bits[:-1]:
+                        obj = getattr(obj, bit)
+                    setting = bits[-1]
+                else:
+                    obj = _settings
+                    setting = element.get_name()
+                print "Setting %s = %s" % (setting, element.value)
+                setattr(obj, setting, element.value)
+            except Exception, e:
+                print element.get_name()
+                raise
diff --git a/chirp/vx3.py b/chirp/vx3.py
new file mode 100644
index 0000000..a06c910
--- /dev/null
+++ b/chirp/vx3.py
@@ -0,0 +1,264 @@
+# Copyright 2011 Rick Farina <sidhayn at gmail.com>
+#     based on modification of Dan Smith's original work
+#
+# 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/>.
+
+from chirp import chirp_common, yaesu_clone, directory
+from chirp import bitwise
+
+#interesting offsets which may be checksums needed later
+#0x0393 checksum1?
+#0x0453 checksum1a?
+#0x0409 checksum2?
+#0x04C9 checksum2a?
+
+MEM_FORMAT = """
+#seekto 0x7F4A;
+u8 checksum;
+
+#seekto 0x0B7A;
+struct {
+  u8 name[6];
+} bank_names[24];
+
+#seekto 0x20CA;
+struct {
+  u8 even_pskip:1,
+     even_skip:1,
+     even_valid:1,
+     even_masked:1,
+     odd_pskip:1,
+     odd_skip:1,
+     odd_valid:1,
+     odd_masked:1;
+} flags[900];
+
+#seekto 0x244A;
+struct {
+  u8   unknown1;
+  u8   mode:2,
+       duplex:2,
+       tune_step:4;
+  bbcd freq[3];
+  u8   power:2,
+       unknown2:4,
+       tmode:2;
+  u8   name[6];
+  bbcd offset[3];
+  u8   unknown3:2,
+       tone:6;
+  u8   unknown4:1,
+       dcs:7;
+  u8   unknown5;
+  u8   unknown6;
+  u8   unknown7:4,
+       automode:1,
+       unknown8:3;
+} memory[900];
+"""
+
+#fix auto mode setting and auto step setting
+
+DUPLEX = ["", "-", "+", "split"]
+MODES  = ["FM", "AM", "WFM", "FM"] # last is auto
+TMODES = ["", "Tone", "TSQL", "DTCS"]
+
+#still need to verify 9 is correct, and add auto: look at byte 1 and 20
+STEPS = [ 5.0, 9, 10.0, 12.5, 15.0, 20.0, 25.0, 50.0, 100.0 ]
+#STEPS = list(chirp_common.TUNING_STEPS)
+#STEPS.remove(6.25)
+#STEPS.remove(30.0)
+#STEPS.append(100.0)
+#STEPS.append(9.0) #this fails because 9 is out of order in the list
+
+#Empty char should be 0xFF but right now we are coding in a space
+CHARSET = list("0123456789" + \
+                   "ABCDEFGHIJKLMNOPQRSTUVWXYZ " + \
+                   "+-/\x00[](){}\x00\x00_" + \
+                   ("\x00" * 13) + "*" + "\x00\x00,'|\x00\x00\x00\x00" + \
+                   ("\x00" * 64))
+
+POWER_LEVELS = [chirp_common.PowerLevel("High", watts=1.50),
+                chirp_common.PowerLevel("Low", watts=0.10)]
+
+class VX3Bank(chirp_common.NamedBank):
+    """A VX3 Bank"""
+    def get_name(self):
+        _bank = self._model._radio._memobj.bank_names[self.index]
+        name = ""
+        for i in _bank.name:
+            if i == 0xFF:
+                break
+            name += CHARSET[i & 0x7F]
+        return name
+
+    def set_name(self, name):
+        name = name.upper()
+        _bank = self._model._radio._memobj.bank_names[self.index]
+        _bank.name = [CHARSET.index(x) for x in name.ljust(6)[:6]]
+
+class VX3BankModel(chirp_common.BankModel):
+    """A VX-3 bank model"""
+    def get_num_banks(self):
+        return 24
+
+    def get_banks(self):
+        _banks = self._radio._memobj.bank_names
+
+        banks = []
+        for i in range(0, self.get_num_banks()):
+            bank = VX3Bank(self, "%i" % i, "Bank-%i" % i)
+            bank.index = i
+            banks.append(bank)
+        return banks
+
+def _wipe_memory(mem):
+    mem.set_raw("\x00" * (mem.size() / 8))
+    #the following settings are set to match the defaults
+    #on the radio, some of these fields are unknown
+    mem.name = [0xFF for _i in range(0, 6)]
+    mem.unknown5 = 0x0D #not sure what this is
+    mem.unknown7 = 0x01 #this likely is part of autostep
+    mem.automode = 0x01 #autoselect mode
+
+ at directory.register
+class VX3Radio(yaesu_clone.YaesuCloneModeRadio):
+    """Yaesu VX-3"""
+    BAUD_RATE = 19200
+    VENDOR = "Yaesu"
+    MODEL = "VX-3"
+
+    # 41 48 30 32 38
+    _model = "AH028"
+    _memsize = 32587
+    _block_lengths = [ 10, 32577 ]
+    #right now this reads in 45 seconds and writes in 123 seconds
+    #attempts to speed it up appear unstable, more testing required
+    _block_size = 8
+
+    def _checksums(self):
+        return [ yaesu_clone.YaesuChecksum(0x0000, 0x7F49) ]
+
+    def process_mmap(self):
+        self._memobj = bitwise.parse(MEM_FORMAT, self._mmap)
+
+    def get_features(self):
+        rf = chirp_common.RadioFeatures()
+        rf.has_bank = False
+        rf.has_bank_names = True
+        rf.has_dtcs_polarity = False
+        rf.valid_modes = list(set(MODES))
+        rf.valid_tmodes = list(TMODES)
+        rf.valid_duplexes = list(DUPLEX)
+        rf.valid_tuning_steps = list(STEPS)
+        rf.valid_bands = [(500000, 999000000)]
+        rf.valid_skips = ["", "S", "P"]
+        rf.valid_power_levels = POWER_LEVELS
+        rf.valid_characters = "".join(CHARSET)
+        rf.valid_name_length = 6
+        rf.memory_bounds = (1, 900)
+        rf.can_odd_split = True
+        rf.has_ctone = False
+        return rf
+
+    def get_raw_memory(self, number):
+        return repr(self._memobj.memory[number])
+
+    def get_memory(self, number):
+        _mem = self._memobj.memory[number-1]
+        _flag = self._memobj.flags[(number-1)/2]
+
+        nibble = ((number-1) % 2) and "even" or "odd"
+        used = _flag["%s_masked" % nibble]
+        valid = _flag["%s_valid" % nibble]
+        pskip = _flag["%s_pskip" % nibble]
+        skip = _flag["%s_skip" % nibble]
+
+        mem = chirp_common.Memory()
+        mem.number = number
+
+        if not used:
+            mem.empty = True
+        if not valid:
+            mem.empty = True
+            mem.power = POWER_LEVELS[0]
+            return mem
+
+        mem.freq = chirp_common.fix_rounded_step(int(_mem.freq) * 1000)
+        mem.offset = int(_mem.offset) * 1000
+        mem.rtone = mem.ctone = chirp_common.TONES[_mem.tone]
+        mem.tmode = TMODES[_mem.tmode]
+        mem.duplex = DUPLEX[_mem.duplex]
+        if mem.duplex == "split":
+            mem.offset = chirp_common.fix_rounded_step(mem.offset)
+        mem.mode = MODES[_mem.mode]
+        mem.dtcs = chirp_common.DTCS_CODES[_mem.dcs]
+        mem.tuning_step = STEPS[_mem.tune_step]
+        mem.skip = pskip and "P" or skip and "S" or ""
+        mem.power = POWER_LEVELS[~_mem.power & 0x01]
+
+        for i in _mem.name:
+            if i == 0xFF:
+                break
+            mem.name += CHARSET[i & 0x7F]
+        mem.name = mem.name.rstrip()
+        return mem
+
+    def set_memory(self, mem):
+        _mem = self._memobj.memory[mem.number-1]
+        _flag = self._memobj.flags[(mem.number-1)/2]
+
+        nibble = ((mem.number-1) % 2) and "even" or "odd"
+      
+        used = _flag["%s_masked" % nibble]
+        valid = _flag["%s_valid" % nibble]
+
+        if not mem.empty and not valid:
+            _wipe_memory(_mem)
+
+        if mem.empty and valid and not used:
+            _flag["%s_valid" % nibble] = False
+            return
+        _flag["%s_masked" % nibble] = not mem.empty
+
+        if mem.empty:
+            return
+
+        _mem.freq = mem.freq / 1000
+        _mem.offset = mem.offset / 1000
+        _mem.tone = chirp_common.TONES.index(mem.rtone)
+        _mem.tmode = TMODES.index(mem.tmode)
+        _mem.duplex = DUPLEX.index(mem.duplex)
+        _mem.mode = MODES.index(mem.mode)
+        _mem.dcs = chirp_common.DTCS_CODES.index(mem.dtcs)
+        _mem.tune_step = STEPS.index(mem.tuning_step)
+        if mem.power == POWER_LEVELS[1]: # Low
+            _mem.power = 0x00
+        else: # Default to High
+            _mem.power = 0x03
+
+        _flag["%s_pskip" % nibble] = mem.skip == "P"
+        _flag["%s_skip" % nibble] = mem.skip == "S"
+
+        for i in range(0, 6):
+            _mem.name[i] = CHARSET.index(mem.name.ljust(6)[i])
+        if mem.name.strip():
+            _mem.name[0] |= 0x80
+      
+    def validate_memory(self, mem):
+        msgs = yaesu_clone.YaesuCloneModeRadio.validate_memory(self, mem)
+        return msgs
+
+    def get_bank_model(self):
+        return VX3BankModel(self)
diff --git a/chirp/vx5.py b/chirp/vx5.py
new file mode 100644
index 0000000..e532146
--- /dev/null
+++ b/chirp/vx5.py
@@ -0,0 +1,276 @@
+# Copyright 2011 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 chirp import chirp_common, yaesu_clone, directory
+from chirp import bitwise
+
+MEM_FORMAT = """
+#seekto 0x002A;
+struct {
+  u8 current_member;
+} bank_used[5];
+
+#seekto 0x0032;
+struct {
+  struct {
+    u8 status;
+    u8 channel;
+  } members[24];
+} bank_groups[5];
+
+#seekto 0x012A;
+struct {
+  u8 zeros:4,
+     pskip: 1,
+     skip: 1,
+     visible: 1,
+     used: 1;
+} flag[220];
+
+#seekto 0x0269;
+struct {
+  u8 unknown1;
+  u8 unknown2:2,
+     half_deviation:1,
+     unknown3:5;
+  u8 unknown4:4,
+     tuning_step:4;
+  bbcd freq[3];
+  u8 icon:6,
+     mode:2;
+  char name[8];
+  bbcd offset[3];
+  u8 tmode:4,
+     power:2,
+     duplex:2;
+  u8 unknown7:2,
+     tone:6;
+  u8 unknown8:1,
+     dtcs:7;
+  u8 unknown9;
+} memory[220];
+
+#seekto 0x1D03;
+u8 current_bank;
+"""
+
+TMODES = ["", "Tone", "TSQL", "DTCS"]
+DUPLEX = ["", "-", "+", "split"]
+MODES = ["FM", "AM", "WFM"]
+STEPS = list(chirp_common.TUNING_STEPS)
+STEPS.remove(6.25)
+STEPS.remove(30.0)
+STEPS.append(100.0)
+STEPS.append(9.0)
+
+POWER_LEVELS = [chirp_common.PowerLevel("Hi", watts=5.00),
+                chirp_common.PowerLevel("L3", watts=2.50),
+                chirp_common.PowerLevel("L2", watts=1.00),
+                chirp_common.PowerLevel("L1", watts=0.05)]
+
+class VX5BankModel(chirp_common.BankModel):
+    def get_num_banks(self):
+        return 5
+
+    def get_banks(self):
+        banks = []
+        for i in range(0, self.get_num_banks()):
+            bank = chirp_common.Bank(self, "%i" % (i+1), "MG%i" % (i+1))
+            bank.index = i
+            banks.append(bank)
+        return banks
+
+    def add_memory_to_bank(self, memory, bank):
+        _members = self._radio._memobj.bank_groups[bank.index].members
+        _bank_used = self._radio._memobj.bank_used[bank.index]
+        for i in range(0, len(_members)):
+            if _members[i].status == 0xFF:
+                #print "empty found, inserting %d at %d" % (memory.number, i)
+                if self._radio._memobj.current_bank == 0xFF:
+                    self._radio._memobj.current_bank = bank.index
+                _members[i].status = 0x00
+                _members[i].channel = memory.number - 1
+                _bank_used.current_member = i
+                return True
+        raise Exception(_("{bank} is full").format(bank=bank))
+
+    def remove_memory_from_bank(self, memory, bank):
+        _members = self._radio._memobj.bank_groups[bank.index].members
+        _bank_used = self._radio._memobj.bank_used[bank.index]
+
+        found = False
+        remaining_members = 0
+        for i in range(0, len(_members)):
+            if _members[i].status == 0x00:
+                if _members[i].channel == (memory.number - 1):
+                    _members[i].status = 0xFF
+                    found = True
+                else:
+                    remaining_members += 1
+
+        if not found:
+            raise Exception(_("Memory {num} not in "
+                              "bank {bank}").format(num=memory.number,
+                                                    bank=bank))
+        if not remaining_members:
+            _bank_used.current_member = 0xFF
+
+    def get_bank_memories(self, bank):
+        memories = []
+
+        _members = self._radio._memobj.bank_groups[bank.index].members
+        _bank_used = self._radio._memobj.bank_used[bank.index]
+
+        if _bank_used.current_member == 0xFF:
+            return memories
+
+        for member in _members:
+            if member.status == 0xFF:
+                continue
+            memories.append(self._radio.get_memory(member.channel+1))
+        return memories
+
+    def get_memory_banks(self, memory):
+        banks = []
+        for bank in self.get_banks():
+            if memory.number in [x.number for x in self.get_bank_memories(bank)]:
+                    banks.append(bank)
+        return banks
+
+ at directory.register
+class VX5Radio(yaesu_clone.YaesuCloneModeRadio):
+    """Yaesu VX-5"""
+    BAUD_RATE = 9600
+    VENDOR = "Yaesu"
+    MODEL = "VX-5"
+
+    _model = ""
+    _memsize = 8123
+    _block_lengths = [10, 16, 8097]
+    _block_size = 8
+
+    def _checksums(self):
+        return [ yaesu_clone.YaesuChecksum(0x0000, 0x1FB9) ]
+
+    def get_features(self):
+        rf = chirp_common.RadioFeatures()
+        rf.can_odd_split = True
+        rf.has_bank = True
+        rf.has_ctone = False
+        rf.has_dtcs_polarity = False
+        rf.valid_modes = MODES + ["NFM"]
+        rf.valid_tmodes = TMODES
+        rf.valid_duplexes = DUPLEX
+        rf.memory_bounds = (1, 220)
+        rf.valid_bands = [(   500000,  16000000),
+                          ( 48000000, 729000000),
+                          (800000000, 999000000)]
+        rf.valid_skips = ["", "S", "P"]
+        rf.valid_power_levels = POWER_LEVELS
+        rf.valid_name_length = 8
+        rf.valid_characters = chirp_common.CHARSET_ASCII
+        return rf
+
+    def process_mmap(self):
+        self._memobj = bitwise.parse(MEM_FORMAT, self._mmap)
+
+    def get_raw_memory(self, number):
+        return repr(self._memobj.memory[number-1])
+
+    def get_memory(self, number):
+        _mem = self._memobj.memory[number-1]
+        _flg = self._memobj.flag[number-1]
+
+        mem = chirp_common.Memory()
+        mem.number = number
+
+        if not _flg.visible:
+            mem.empty = True
+        if not _flg.used:
+            mem.empty = True
+            return mem
+
+        mem.freq = chirp_common.fix_rounded_step(int(_mem.freq) * 1000)
+        mem.duplex = DUPLEX[_mem.duplex]
+        mem.name = self.filter_name(str(_mem.name).rstrip())
+        mem.mode = MODES[_mem.mode]
+        if mem.mode == "FM" and _mem.half_deviation:
+            mem.mode = "NFM"
+        mem.tuning_step = STEPS[_mem.tuning_step]
+        mem.offset = int(_mem.offset) * 1000
+        mem.power = POWER_LEVELS[3 - _mem.power]
+        mem.tmode = TMODES[_mem.tmode & 0x3] # masked so bad mems can be read
+        if mem.duplex == "split":
+            mem.offset = chirp_common.fix_rounded_step(mem.offset)
+        mem.rtone = mem.ctone = chirp_common.TONES[_mem.tone]
+        mem.dtcs = chirp_common.DTCS_CODES[_mem.dtcs]
+
+        mem.skip = _flg.pskip and "P" or _flg.skip and "S" or ""
+
+        return mem
+
+    def set_memory(self, mem):
+        _mem = self._memobj.memory[mem.number-1]
+        _flg = self._memobj.flag[mem.number-1]
+        
+        # initialize new channel to safe defaults
+        if not mem.empty and not _flg.used:
+            _flg.used = True
+            _mem.unknown1 = 0x00
+            _mem.unknown2 = 0x00
+            _mem.unknown3 = 0x00
+            _mem.unknown4 = 0x00
+            _mem.icon = 12 # file cabinet icon
+            _mem.unknown7 = 0x00
+            _mem.unknown8 = 0x00
+            _mem.unknown9 = 0x00
+            
+        if mem.empty and _flg.used and not _flg.visible:
+            _flg.used = False
+            return
+        _flg.visible = not mem.empty
+        if mem.empty:
+            self._wipe_memory_banks(mem)
+            return
+
+        _mem.freq = int(mem.freq / 1000)
+        _mem.duplex = DUPLEX.index(mem.duplex)
+        _mem.name = mem.name.ljust(8)
+        if mem.mode == "NFM":
+            _mem.mode = MODES.index("FM")
+            _mem.half_deviation = 1
+        else:
+            _mem.mode = MODES.index(mem.mode)
+            _mem.half_deviation = 0
+        _mem.tuning_step = STEPS.index(mem.tuning_step)
+        _mem.offset = int(mem.offset / 1000)
+        if mem.power:
+            _mem.power = 3 - POWER_LEVELS.index(mem.power)
+        else:
+            _mem.power = 0
+        _mem.tmode = TMODES.index(mem.tmode)
+        _mem.tone = chirp_common.TONES.index(mem.rtone)
+        _mem.dtcs = chirp_common.DTCS_CODES.index(mem.dtcs)
+
+        _flg.skip = mem.skip == "S"
+        _flg.pskip = mem.skip == "P"
+
+    @classmethod
+    def match_model(cls, filedata, filename):
+        return len(filedata) == cls._memsize
+
+    def get_bank_model(self):
+        return VX5BankModel(self)
diff --git a/chirp/vx6.py b/chirp/vx6.py
new file mode 100644
index 0000000..fbf0287
--- /dev/null
+++ b/chirp/vx6.py
@@ -0,0 +1,269 @@
+# Copyright 2010 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/>.
+
+from chirp import chirp_common, yaesu_clone, directory
+from chirp import bitwise
+
+# flags.{even|odd}_pskip: These are actually "preferential *scan* channels".
+# Is that what they mean on other radios as well?
+
+# memory {
+#   step_changed: Channel step has been changed. Bit stays on even after
+#                 you switch back to default step. Don't know why you would
+#                 care
+#   half_deviation: 2.5 kHz deviation
+#   cpu_shifted:  CPU freq has been shifted (to move a birdie out of channel)
+#   power:        0-3: ["L1", "L2", "L3", "Hi"]
+#   pager:        Set if this is a paging memory
+#   tmodes:       0-7: ["", "Tone", "TSQL", "DTCS", "Rv Tn", "D Code",
+#                       "T DCS", "D Tone"]
+#                      Rv Tn: Reverse CTCSS - mutes receiver on tone
+#                      The final 3 are for split:
+#                      D Code: DCS Encode only
+#                      T DCS:  Encodes tone, decodes DCS code
+#                      D Tone: Encodes DCS code, decodes tone
+# }
+MEM_FORMAT = """
+#seekto 0x018A;
+u16 bank_sizes[24];
+
+#seekto 0x097A;
+struct {
+  u8 name[6];
+} bank_names[24];
+
+#seekto 0x0C0A;
+struct {
+  u16 channel[100];
+} bank_channels[24];
+
+#seekto 0x1ECA;
+struct {
+  u8 even_pskip:1,
+     even_skip:1,
+     even_valid:1,
+     even_masked:1,
+     odd_pskip:1,
+     odd_skip:1,
+     odd_valid:1,
+     odd_masked:1;
+} flags[450];
+
+#seekto 0x21CA;
+struct {
+  u8 unknown11:1,
+     step_changed:1,
+     half_deviation:1,
+     cpu_shifted:1,
+     unknown12:4;
+  u8 mode:2,
+     duplex:2,
+     tune_step:4;
+  bbcd freq[3];
+  u8 power:2,
+     unknown2:2,
+     pager:1,
+     tmode:3;
+  u8 name[6];
+  bbcd offset[3];
+  u8 tone;
+  u8 dcs;
+  u8 unknown5;
+} memory[900];
+"""
+
+DUPLEX = ["", "-", "+", "split"]
+MODES  = ["FM", "AM", "WFM", "FM"] # last is auto
+TMODES = ["", "Tone", "TSQL", "DTCS"]
+STEPS =  [5.0, 10.0, 12.5, 15.0, 20.0, 25.0, 50.0, 100.0,
+          9.0, 200.0, 5.0]  # last is auto, 9.0k and 200.0k are unadvertised
+
+
+CHARSET = ["%i" % int(x) for x in range(0, 10)] + \
+    [chr(x) for x in range(ord("A"), ord("Z")+1)] + \
+    list(" +-/\x00[]__" + ("\x00" * 9) + "$%%\x00**.|=\\\x00@") + \
+    list("\x00" * 100)
+
+POWER_LEVELS = [chirp_common.PowerLevel("Hi", watts=5.00),
+                chirp_common.PowerLevel("L3", watts=2.50),
+                chirp_common.PowerLevel("L2", watts=1.00),
+                chirp_common.PowerLevel("L1", watts=0.30)]
+POWER_LEVELS_220 = [chirp_common.PowerLevel("Hi", watts=1.50),
+                chirp_common.PowerLevel("L3", watts=1.00),
+                chirp_common.PowerLevel("L2", watts=0.50),
+                chirp_common.PowerLevel("L1", watts=0.20)]
+
+ at directory.register
+class VX6Radio(yaesu_clone.YaesuCloneModeRadio):
+    """Yaesu VX-6"""
+    BAUD_RATE = 19200
+    VENDOR = "Yaesu"
+    MODEL = "VX-6"
+
+    _model = "AH021"
+    _memsize = 32587
+    _block_lengths = [10, 32578]
+    _block_size = 16
+
+    def _checksums(self):
+        return [ yaesu_clone.YaesuChecksum(0x0000, 0x7F49) ]
+
+    def process_mmap(self):
+        self._memobj = bitwise.parse(MEM_FORMAT, self._mmap)
+
+    def get_features(self):
+        rf = chirp_common.RadioFeatures()
+        rf.has_bank = False
+        rf.has_dtcs_polarity = False
+        rf.valid_modes = ["FM", "WFM", "AM", "NFM"]
+        rf.valid_tmodes = ["", "Tone", "TSQL", "DTCS"]
+        rf.valid_duplexes = DUPLEX
+        rf.valid_tuning_steps = STEPS
+        rf.valid_power_levels = POWER_LEVELS
+        rf.memory_bounds = (1, 900)
+        rf.valid_bands = [(500000, 998990000)]
+        rf.valid_characters = "".join(CHARSET)
+        rf.valid_name_length = 6
+        rf.can_odd_split = True
+        rf.has_ctone = False
+        return rf
+
+    def get_raw_memory(self, number):
+        return repr(self._memobj.memory[number-1])
+
+    def get_memory(self, number):
+        _mem = self._memobj.memory[number-1]
+        _flg = self._memobj.flags[(number-1)/2]
+
+        nibble = ((number-1) % 2) and "even" or "odd"
+        used = _flg["%s_masked" % nibble]
+        valid = _flg["%s_valid" % nibble]
+        pskip = _flg["%s_pskip" % nibble]
+        skip = _flg["%s_skip" % nibble]
+
+        mem = chirp_common.Memory()
+        mem.number = number
+
+        if not used:
+            mem.empty = True
+        if not valid:
+            mem.empty = True
+            mem.power = POWER_LEVELS[0]
+            return mem
+
+        mem.freq = chirp_common.fix_rounded_step(int(_mem.freq) * 1000)
+        mem.offset = chirp_common.fix_rounded_step(int(_mem.offset) * 1000)
+        mem.rtone = mem.ctone = chirp_common.TONES[_mem.tone & 0x3f]
+        mem.tmode = TMODES[_mem.tmode]
+        mem.duplex = DUPLEX[_mem.duplex]
+        mem.mode = MODES[_mem.mode]
+        if mem.mode == "FM" and _mem.half_deviation:
+            mem.mode = "NFM"
+        mem.dtcs = chirp_common.DTCS_CODES[_mem.dcs & 0x7f]
+        mem.tuning_step = STEPS[_mem.tune_step]
+        mem.skip = pskip and "P" or skip and "S" or ""
+        
+        if mem.freq > 220000000 and mem.freq < 225000000:
+            mem.power = POWER_LEVELS_220[3 - _mem.power]
+        else:
+            mem.power = POWER_LEVELS[3 - _mem.power]
+
+        for i in _mem.name:
+            if i == 0xFF:
+                break
+            mem.name += CHARSET[i & 0x7F]
+        mem.name = mem.name.rstrip()
+
+        return mem
+
+    def set_memory(self, mem):
+        _mem = self._memobj.memory[mem.number-1]
+        _flag = self._memobj.flags[(mem.number-1)/2]
+
+        nibble = ((mem.number-1) % 2) and "even" or "odd"
+        used = _flag["%s_masked" % nibble]
+        valid = _flag["%s_valid" % nibble]
+
+        # initialize new channel to safe defaults
+        if not mem.empty and not used:
+            _mem.unknown11 = 0
+            _mem.step_changed = 0
+            _mem.cpu_shifted = 0
+            _mem.unknown12 = 0
+            _mem.unknown2 = 0
+            _mem.pager = 0
+            _mem.unknown5 = 0
+
+        if mem.empty and valid and not used:
+            _flag["%s_valid" % nibble] = False
+            return
+        _flag["%s_masked" % nibble] = not mem.empty
+
+        if mem.empty:
+            return
+
+        _mem.freq = mem.freq / 1000
+        _mem.offset = mem.offset / 1000
+        _mem.tone = chirp_common.TONES.index(mem.rtone)
+        _mem.tmode = TMODES.index(mem.tmode)
+        _mem.duplex = DUPLEX.index(mem.duplex)
+        if mem.mode == "NFM":
+            _mem.mode = MODES.index("FM")
+            _mem.half_deviation = 1
+        else:
+            _mem.mode = MODES.index(mem.mode)
+            _mem.half_deviation = 0
+        _mem.dcs = chirp_common.DTCS_CODES.index(mem.dtcs)
+        _mem.tune_step = STEPS.index(mem.tuning_step)
+        if mem.power:
+            _mem.power = 3 - POWER_LEVELS.index(mem.power)
+        else:
+            _mem.power = 0
+
+        _flag["%s_pskip" % nibble] = mem.skip == "P"
+        _flag["%s_skip" % nibble] = mem.skip == "S"
+
+        _mem.name = [0xFF] * 6
+        for i in range(0, 6):
+            _mem.name[i] = CHARSET.index(mem.name.ljust(6)[i])
+
+        if mem.name.strip():
+            _mem.name[0] |= 0x80
+
+#    def get_banks(self):
+#        _banks = self._memobj.bank_names
+#
+#        banks = []
+#        for bank in _banks:
+#            name = ""
+#            for i in bank.name:
+#                name += CHARSET[i & 0x7F]
+#            banks.append(name.rstrip())
+#
+#        return banks
+#
+#    # Return channels for a bank. Bank given as number
+#    def get_bank_channels(self, bank):
+#        nchannels = 0
+#        size = self._memobj.bank_sizes[bank]
+#        if size <= 198:
+#            nchannels = 1 + size/2
+#        _channels = self._memobj.bank_channels[bank]
+#        channels = []
+#        for i in range(0, nchannels):
+#            channels.append(int(_channels.channel[i]))
+#
+#        return channels
+
diff --git a/chirp/vx7.py b/chirp/vx7.py
new file mode 100644
index 0000000..d657ddb
--- /dev/null
+++ b/chirp/vx7.py
@@ -0,0 +1,341 @@
+# Copyright 2010 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/>.
+
+from chirp import chirp_common, yaesu_clone, directory
+from chirp import bitwise
+
+MEM_FORMAT = """
+#seekto 0x0611;
+u8 checksum1;
+
+#seekto 0x0691;
+u8 checksum2;
+
+#seekto 0x0742;
+struct {
+  u16 in_use;
+} bank_used[9];
+
+#seekto 0x0EA2;
+struct {
+  u16 members[48];
+} bank_members[9];
+
+#seekto 0x3F52;
+u8 checksum3;
+
+#seekto 0x1202;
+struct {
+  u8 even_pskip:1,
+     even_skip:1,
+     even_valid:1,
+     even_masked:1,
+     odd_pskip:1,
+     odd_skip:1,
+     odd_valid:1,
+     odd_masked:1;
+} flags[225];
+
+#seekto 0x1322;
+struct {
+  u8   unknown1;
+  u8   power:2,
+       duplex:2,
+       tune_step:4;
+  bbcd freq[3];
+  u8   zeros1:2,
+       ones:2,
+       zeros2:2,
+       mode:2;
+  u8   name[8];
+  u8 zero;
+  bbcd offset[3];
+  u8   zeros3:2,
+       tone:6;
+  u8   zeros4:1,
+       dcs:7;
+  u8   zeros5:5,
+       is_split_tone:1,
+       tmode:2;
+  u8   charset;
+} memory[450];
+"""
+
+DUPLEX = ["", "-", "+", "split"]
+MODES  = ["FM", "AM", "WFM", "Auto"]
+TMODES = ["", "Tone", "TSQL", "DTCS", "Cross"]
+CROSS_MODES = ["DTCS->", "Tone->DTCS", "DTCS->Tone"]
+STEPS = list(chirp_common.TUNING_STEPS)
+STEPS.remove(6.25)
+STEPS.remove(30.0)
+STEPS.append(100.0)
+STEPS.append(9.0)
+
+CHARSET = ["%i" % int(x) for x in range(0, 10)] + \
+    [" "] + \
+    [chr(x) for x in range(ord("A"), ord("Z")+1)] + \
+    [chr(x) for x in range(ord("a"), ord("z")+1)] + \
+    list(".,:;!\"#$%&'()*+-.=<>?@[?]^_\\{|}") + \
+    list("\x00" * 100)
+
+POWER_LEVELS = [chirp_common.PowerLevel("L1", watts=0.05),
+                chirp_common.PowerLevel("L2", watts=1.00),
+                chirp_common.PowerLevel("L3", watts=2.50),
+                chirp_common.PowerLevel("Hi", watts=5.00)
+                ]
+POWER_LEVELS_220 = [chirp_common.PowerLevel("L1", watts=0.05),
+                    chirp_common.PowerLevel("L2", watts=0.30)]
+
+def _is220(freq):
+    return freq >= 222000000 and freq <= 225000000
+
+class VX7BankModel(chirp_common.BankModel):
+    """A VX-7 Bank model"""
+    def get_num_banks(self):
+        return 9
+
+    def get_banks(self):
+        banks = []
+        for i in range(0, self.get_num_banks()):
+            bank = chirp_common.Bank(self, "%i" % (i+1), "MG%i" % (i+1))
+            bank.index = i
+            banks.append(bank)
+        return banks
+
+    def add_memory_to_bank(self, memory, bank):
+        _members = self._radio._memobj.bank_members[bank.index]
+        _bank_used = self._radio._memobj.bank_used[bank.index]
+        for i in range(0, 48):
+            if _members.members[i] == 0xFFFF:
+                _members.members[i] = memory.number - 1
+                _bank_used.in_use = 0x0000
+                break
+
+    def remove_memory_from_bank(self, memory, bank):
+        _members = self._radio._memobj.bank_members[bank.index].members
+        _bank_used = self._radio._memobj.bank_used[bank.index]
+
+        found = False
+        remaining_members = 0
+        for i in range(0, len(_members)):
+            if _members[i] == (memory.number - 1):
+                _members[i] = 0xFFFF
+                found = True
+            elif _members[i] != 0xFFFF:
+                remaining_members += 1
+
+        if not found:
+            raise Exception("Memory {num} not in " +
+                            "bank {bank}".format(num=memory.number,
+                                                    bank=bank))
+        if not remaining_members:
+            _bank_used.in_use = 0xFFFF
+
+    def get_bank_memories(self, bank):
+        memories = []
+
+        _members = self._radio._memobj.bank_members[bank.index].members
+        _bank_used = self._radio._memobj.bank_used[bank.index]
+
+        if _bank_used.in_use == 0xFFFF:
+            return memories
+
+        for number in _members:
+            if number == 0xFFFF:
+                continue
+            memories.append(self._radio.get_memory(number+1))
+        return memories
+
+    def get_memory_banks(self, memory):
+        banks = []
+        for bank in self.get_banks():
+            if memory.number in [x.number for x in
+                                 self.get_bank_memories(bank)]:
+                banks.append(bank)
+        return banks
+
+def _wipe_memory(mem):
+    mem.set_raw("\x00" * (mem.size() / 8))
+    mem.unknown1 = 0x05
+    mem.ones = 0x03
+
+ at directory.register
+class VX7Radio(yaesu_clone.YaesuCloneModeRadio):
+    """Yaesu VX-7"""
+    BAUD_RATE = 19200
+    VENDOR = "Yaesu"
+    MODEL = "VX-7"
+
+    _model = ""
+    _memsize = 16211
+    _block_lengths = [ 10, 8, 16193 ]
+    _block_size = 8
+
+    def _checksums(self):
+        return [ yaesu_clone.YaesuChecksum(0x0592, 0x0610),
+                 yaesu_clone.YaesuChecksum(0x0612, 0x0690),
+                 yaesu_clone.YaesuChecksum(0x0000, 0x3F51),
+                 ]
+
+    def process_mmap(self):
+        self._memobj = bitwise.parse(MEM_FORMAT, self._mmap)
+
+    def get_features(self):
+        rf = chirp_common.RadioFeatures()
+        rf.has_bank = True
+        rf.has_dtcs_polarity = False
+        rf.valid_modes = list(set(MODES))
+        rf.valid_tmodes = list(TMODES)
+        rf.valid_duplexes = list(DUPLEX)
+        rf.valid_tuning_steps = list(STEPS)
+        rf.valid_bands = [(500000, 999000000)]
+        rf.valid_skips = ["", "S", "P"]
+        rf.valid_power_levels = POWER_LEVELS
+        rf.valid_characters = "".join(CHARSET)
+        rf.valid_name_length = 8
+        rf.memory_bounds = (1, 450)
+        rf.can_odd_split = True
+        rf.has_ctone = False
+        rf.has_cross = True
+        rf.valid_cross_modes = list(CROSS_MODES)
+        return rf
+
+    def get_raw_memory(self, number):
+        return repr(self._memobj.memory[number-1])
+
+    def get_memory(self, number):
+        _mem = self._memobj.memory[number-1]
+        _flag = self._memobj.flags[(number-1)/2]
+
+        nibble = ((number-1) % 2) and "even" or "odd"
+        used = _flag["%s_masked" % nibble]
+        valid = _flag["%s_valid" % nibble]
+        pskip = _flag["%s_pskip" % nibble]
+        skip = _flag["%s_skip" % nibble]
+
+        mem = chirp_common.Memory()
+        mem.number = number
+        if not used:
+            mem.empty = True
+        if not valid:
+            mem.empty = True
+            mem.power = POWER_LEVELS[0]
+            return mem
+
+        mem.freq = chirp_common.fix_rounded_step(int(_mem.freq) * 1000)
+        mem.offset = int(_mem.offset) * 1000
+        mem.rtone = mem.ctone = chirp_common.TONES[_mem.tone]
+        if not _mem.is_split_tone:
+            mem.tmode = TMODES[_mem.tmode]
+            mem.cross_mode = CROSS_MODES[0]
+        else:
+            mem.tmode = "Cross"
+            mem.cross_mode = CROSS_MODES[int(_mem.tmode)]
+        mem.duplex = DUPLEX[_mem.duplex]
+        if mem.duplex == "split":
+            mem.offset = chirp_common.fix_rounded_step(mem.offset)
+        mem.mode = MODES[_mem.mode]
+        mem.dtcs = chirp_common.DTCS_CODES[_mem.dcs]
+        mem.tuning_step = STEPS[_mem.tune_step]
+        mem.skip = pskip and "P" or skip and "S" or ""
+
+        if _is220(mem.freq):
+            levels = POWER_LEVELS_220
+        else:
+            levels = POWER_LEVELS
+        try:
+            mem.power = levels[_mem.power]
+        except IndexError:
+            print "Radio reported invalid power level %s (in %s)" % (
+                _mem.power, levels)
+            mem.power = levels[0]
+
+        for i in _mem.name:
+            if i == "\xFF":
+                break
+            mem.name += CHARSET[i]
+        mem.name = mem.name.rstrip()
+
+        return mem
+
+    def set_memory(self, mem):
+        _mem = self._memobj.memory[mem.number-1]
+        _flag = self._memobj.flags[(mem.number-1)/2]
+
+        nibble = ((mem.number-1) % 2) and "even" or "odd"
+        
+        valid = _flag["%s_valid" % nibble]
+        used = _flag["%s_masked" % nibble]
+
+        if not mem.empty and not valid:
+            _wipe_memory(_mem)
+            self._wipe_memory_banks(mem)
+
+        if mem.empty and valid and not used:
+            _flag["%s_valid" % nibble] = False
+            return
+        _flag["%s_masked" % nibble] = not mem.empty
+
+        if mem.empty:
+            return
+
+        _flag["%s_valid" % nibble] = True
+
+        _mem.freq = mem.freq / 1000
+        _mem.offset = mem.offset / 1000
+        _mem.tone = chirp_common.TONES.index(mem.rtone)
+        if mem.tmode != "Cross":
+            _mem.is_split_tone = 0
+            _mem.tmode = TMODES.index(mem.tmode)
+        else:
+            _mem.is_split_tone = 1
+            _mem.tmode = CROSS_MODES.index(mem.cross_mode)
+        _mem.duplex = DUPLEX.index(mem.duplex)
+        _mem.mode = MODES.index(mem.mode)
+        _mem.dcs = chirp_common.DTCS_CODES.index(mem.dtcs)
+        _mem.tune_step = STEPS.index(mem.tuning_step)
+
+        if mem.power:
+            if _is220(mem.freq):
+                levels = [str(l) for l in POWER_LEVELS_220]
+                _mem.power = levels.index(str(mem.power))
+            else:
+                _mem.power = POWER_LEVELS.index(mem.power)
+        else:
+            _mem.power = 0
+
+        _flag["%s_pskip" % nibble] = mem.skip == "P"
+        _flag["%s_skip" % nibble] = mem.skip == "S"
+
+        for i in range(0, 8):
+            _mem.name[i] = CHARSET.index(mem.name.ljust(8)[i])
+        
+    def validate_memory(self, mem):
+        msgs = yaesu_clone.YaesuCloneModeRadio.validate_memory(self, mem)
+
+        if _is220(mem.freq):
+            if str(mem.power) not in [str(l) for l in POWER_LEVELS_220]:
+                msgs.append(chirp_common.ValidationError(\
+                        "Power level %s not supported on 220MHz band" % \
+                            mem.power))
+
+        return msgs
+
+    @classmethod
+    def match_model(cls, filedata, filename):
+        return len(filedata) == cls._memsize
+
+    def get_bank_model(self):
+        return VX7BankModel(self)
diff --git a/chirp/vx8.py b/chirp/vx8.py
new file mode 100644
index 0000000..5c28cda
--- /dev/null
+++ b/chirp/vx8.py
@@ -0,0 +1,309 @@
+# Copyright 2010 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/>.
+
+from chirp import chirp_common, yaesu_clone, directory
+from chirp import bitwise
+
+MEM_FORMAT = """
+#seekto 0x54a;
+struct {
+    u16 in_use;
+} bank_used[24];
+
+#seekto 0x135A;
+struct {
+  u8 unknown[2];
+  u8 name[16];
+} bank_info[24];
+
+#seekto 0x198a;
+struct {
+    u16 channel[100];
+} bank_members[24];
+
+#seekto 0x2C4A;
+struct {
+  u8 nosubvfo:1,
+     unknown:3,
+     pskip:1,
+     skip:1,
+     used:1,
+     valid:1;
+} flag[900];
+
+#seekto 0x328A;
+struct {
+  u8 unknown1;
+  u8 mode:2,
+     duplex:2,
+     tune_step:4;
+  bbcd freq[3];
+  u8 power:2,
+     unknown2:4,
+     tone_mode:2;
+  u8 charsetbits[2];
+  char label[16];
+  bbcd offset[3];
+  u8 unknown5:2,
+     tone:6;
+  u8 unknown6:1,
+     dcs:7;
+  u8 unknown7[3];
+} memory[900];
+
+#seekto 0xFECA;
+u8 checksum;
+"""
+
+TMODES = ["", "Tone", "TSQL", "DTCS"]
+DUPLEX = ["", "-", "+", "split"]
+MODES  = ["FM", "AM", "WFM"]
+STEPS = list(chirp_common.TUNING_STEPS)
+STEPS.remove(30.0)
+STEPS.append(100.0)
+STEPS.insert(2, 0.0) # There is a skipped tuning step ad index 2 (?)
+SKIPS = ["", "S", "P"]
+
+CHARSET = ["%i" % int(x) for x in range(0, 10)] + \
+    [chr(x) for x in range(ord("A"), ord("Z")+1)] + \
+    [" ",] + \
+    [chr(x) for x in range(ord("a"), ord("z")+1)] + \
+    list(".,:;*#_-/&()@!?^ ") + list("\x00" * 100)
+
+POWER_LEVELS = [chirp_common.PowerLevel("Hi", watts=5.00),
+                chirp_common.PowerLevel("L3", watts=2.50),
+                chirp_common.PowerLevel("L2", watts=1.00),
+                chirp_common.PowerLevel("L1", watts=0.05)]
+
+class VX8Bank(chirp_common.NamedBank):
+    """A VX-8 bank"""
+
+    def get_name(self):
+        _bank = self._model._radio._memobj.bank_info[self.index]
+        _bank_used = self._model._radio._memobj.bank_used[self.index]
+                      
+        name = ""
+        for i in _bank.name:
+            if i == 0xFF:
+                break
+            name += CHARSET[i & 0x7F]
+        return name.rstrip()
+
+    def set_name(self, name):
+        _bank = self._model._radio._memobj.bank_info[self.index]
+        _bank.name = [CHARSET.index(x) for x in name.ljust(16)[:16]]
+
+class VX8BankModel(chirp_common.BankModel):
+    """A VX-8 bank model"""
+    def get_num_banks(self):
+        return 24
+
+    def get_banks(self):
+        banks = []
+        _banks = self._radio._memobj.bank_info
+
+        index = 0
+        for _bank in _banks:
+            bank = VX8Bank(self, "%i" % index, "BANK-%i" % index)
+            bank.index = index
+            banks.append(bank)
+            index += 1
+
+        return banks
+
+    def add_memory_to_bank(self, memory, bank):
+        _members = self._radio._memobj.bank_members[bank.index]
+        _bank_used = self._radio._memobj.bank_used[bank.index]
+        for i in range(0, 100):
+            if _members.channel[i] == 0xFFFF:
+                _members.channel[i] = memory.number - 1
+                _bank_used.in_use = 0x06
+                break
+
+    def remove_memory_from_bank(self, memory, bank):
+        _members = self._radio._memobj.bank_members[bank.index]
+        _bank_used = self._radio._memobj.bank_used[bank.index]
+
+        remaining_members = 0
+        found = False
+        for i in range(0, len(_members.channel)):
+            if _members.channel[i] == (memory.number - 1):
+                _members.channel[i] = 0xFFFF
+                found = True
+            elif _members.channel[i] != 0xFFFF:
+                remaining_members += 1
+
+        if not found:
+            raise Exception("Memory %i is not in bank %s. Cannot remove" % \
+                                (memory.number, bank))
+
+        if not remaining_members:
+            _bank_used.in_use = 0xFFFF
+
+    def get_bank_memories(self, bank):
+        memories = []
+        _members = self._radio._memobj.bank_members[bank.index]
+        _bank_used = self._radio._memobj.bank_used[bank.index]
+
+        if _bank_used.in_use == 0xFFFF:
+            return memories
+
+        for channel in _members.channel:
+            if channel != 0xFFFF:
+                memories.append(self._radio.get_memory(int(channel)+1))
+
+        return memories
+
+    def get_memory_banks(self, memory):
+        banks = []
+        for bank in self.get_banks():
+            if memory.number in \
+                    [x.number for x in self.get_bank_memories(bank)]:
+                banks.append(bank)
+
+        return banks
+
+def _wipe_memory(mem):
+    mem.set_raw("\x00" * (mem.size() / 8))
+    mem.unknown1 = 0x05
+
+ at directory.register
+class VX8Radio(yaesu_clone.YaesuCloneModeRadio):
+    """Yaesu VX-8"""
+    BAUD_RATE = 38400
+    VENDOR = "Yaesu"
+    MODEL = "VX-8"
+    VARIANT = "R"
+
+    _model = "AH029"
+    _memsize = 65227
+    _block_lengths = [ 10, 65217 ]
+    _block_size = 32
+
+    def process_mmap(self):
+        self._memobj = bitwise.parse(MEM_FORMAT, self._mmap)
+
+    def get_features(self):
+        rf = chirp_common.RadioFeatures()
+        rf.has_dtcs_polarity = False
+        rf.valid_modes = list(MODES)
+        rf.valid_tmodes = list(TMODES)
+        rf.valid_duplexes = list(DUPLEX)
+        rf.valid_tuning_steps = list(STEPS)
+        rf.valid_bands = [(500000, 999900000)]
+        rf.valid_skips = SKIPS
+        rf.valid_power_levels = POWER_LEVELS
+        rf.valid_characters = "".join(CHARSET)
+        rf.valid_name_length = 16
+        rf.memory_bounds = (1, 900)
+        rf.can_odd_split = True
+        rf.has_ctone = False
+        rf.has_bank_names = True
+        return rf
+
+    def get_raw_memory(self, number):
+        return repr(self._memobj.memory[number])
+
+    def _checksums(self):
+        return [ yaesu_clone.YaesuChecksum(0x0000, 0xFEC9) ]
+
+    def get_memory(self, number):
+        flag = self._memobj.flag[number-1]
+        _mem = self._memobj.memory[number-1]
+
+        mem = chirp_common.Memory()
+        mem.number = number
+        if not flag.used:
+            mem.empty = True
+        if not flag.valid:
+            mem.empty = True
+            return mem
+        mem.freq = chirp_common.fix_rounded_step(int(_mem.freq) * 1000)
+        mem.offset = int(_mem.offset) * 1000
+        mem.rtone = mem.ctone = chirp_common.TONES[_mem.tone]
+        mem.tmode = TMODES[_mem.tone_mode]
+        mem.duplex = DUPLEX[_mem.duplex]
+        if mem.duplex == "split":
+            mem.offset = chirp_common.fix_rounded_step(mem.offset)
+        mem.mode = MODES[_mem.mode]
+        mem.dtcs = chirp_common.DTCS_CODES[_mem.dcs]
+        mem.tuning_step = STEPS[_mem.tune_step]
+        mem.power = POWER_LEVELS[3 - _mem.power]
+        mem.skip = flag.pskip and "P" or flag.skip and "S" or ""
+
+        for i in str(_mem.label):
+            if i == "\xFF":
+                break
+            mem.name += CHARSET[ord(i)]
+
+        return mem
+
+    def _debank(self, mem):
+        bm = self.get_bank_model()
+        for bank in bm.get_memory_banks(mem):
+            bm.remove_memory_from_bank(mem, bank)
+
+    def set_memory(self, mem):
+        _mem = self._memobj.memory[mem.number-1]
+        flag = self._memobj.flag[mem.number-1]
+
+        if not mem.empty and not flag.valid:
+            _wipe_memory(_mem)
+
+        if mem.empty and flag.valid and not flag.used:
+            flag.valid = False
+            return
+        flag.used = not mem.empty
+
+        if mem.empty:
+            return
+
+        if mem.freq < 30000000 or \
+                (mem.freq > 88000000 and mem.freq < 108000000) or \
+                mem.freq > 580000000:
+            flag.nosubvfo = True  # Masked from VFO B
+        else:
+            flag.nosubvfo = False # Available in both VFOs
+
+        _mem.freq = int(mem.freq / 1000)
+        _mem.offset = int(mem.offset / 1000)
+        _mem.tone = chirp_common.TONES.index(mem.rtone)
+        _mem.tone_mode = TMODES.index(mem.tmode)
+        _mem.duplex = DUPLEX.index(mem.duplex)
+        _mem.mode = MODES.index(mem.mode)
+        _mem.dcs = chirp_common.DTCS_CODES.index(mem.dtcs)
+        _mem.tune_step = STEPS.index(mem.tuning_step)
+        if mem.power:
+            _mem.power = 3 - POWER_LEVELS.index(mem.power)
+        else:
+            _mem.power = 0
+
+        label = "".join([chr(CHARSET.index(x)) for x in mem.name.rstrip()])
+        _mem.label = label.ljust(16, "\xFF")
+        # We only speak english here in chirpville
+        _mem.charsetbits[0] = 0x00
+        _mem.charsetbits[1] = 0x00
+
+        flag.skip = mem.skip == "S"
+        flag.pskip = mem.skip == "P"
+
+    def get_bank_model(self):
+        return VX8BankModel(self)
+
+ at directory.register
+class VX8DRadio(VX8Radio):
+    """Yaesu VX-8DR"""
+    _model = "AH29D"
+    VARIANT = "DR"
diff --git a/chirp/vxa700.py b/chirp/vxa700.py
new file mode 100644
index 0000000..bfccfbd
--- /dev/null
+++ b/chirp/vxa700.py
@@ -0,0 +1,311 @@
+# Copyright 2012 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/>.
+
+from chirp import chirp_common, util, directory, memmap, errors
+from chirp import bitwise
+
+import time
+import struct
+
+def _debug(string):
+    pass
+    print string
+
+def _send(radio, data):
+    _debug("Sending %s" % repr(data))
+    radio.pipe.write(data)
+    radio.pipe.flush()
+    echo = radio.pipe.read(len(data))
+    if len(echo) != len(data):
+        raise errors.RadioError("Invalid echo")
+
+def _spoonfeed(radio, data):
+    #count = 0
+    _debug("Writing %i:\n%s" % (len(data), util.hexprint(data)))
+    for byte in data:
+        radio.pipe.write(byte)
+        radio.pipe.flush()
+        time.sleep(0.01)
+        continue
+        # This is really unreliable for some reason,
+        # so just blindly send the data
+        echo = radio.pipe.read(1)
+        if echo != byte:
+            print "%02x != %02x" % (ord(echo), ord(byte))
+            raise errors.RadioError("No echo?")
+        #count += 1
+
+def _download(radio):
+    count = 0
+    data = ""
+    while len(data) < radio.get_memsize():
+        count += 1
+        chunk = radio.pipe.read(133)
+        if len(chunk) == 0 and len(data) == 0 and count < 30:
+            continue
+        if len(chunk) != 132:
+            raise errors.RadioError("Got short block (length %i)" % len(chunk))
+
+        checksum = ord(chunk[-1])
+        _flag, _length, _block, _data, checksum = \
+            struct.unpack("BBB128sB", chunk)
+
+        cs = 0
+        for byte in chunk[:-1]:
+            cs += ord(byte)
+        if (cs % 256) != checksum:
+            raise errors.RadioError("Invalid checksum at 0x%02x" % len(data))
+
+        data += _data
+        _send(radio, "\x06")
+
+        if radio.status_fn:
+            status = chirp_common.Status()
+            status.msg = "Cloning from radio"
+            status.cur = len(data)
+            status.max = radio.get_memsize()
+            radio.status_fn(status)
+
+    return memmap.MemoryMap(data)
+
+def _upload(radio):
+    for i in range(0, radio.get_memsize(), 128):
+        chunk = radio.get_mmap()[i:i+128]
+        cs = 0x20 + 130 + (i / 128)
+        for byte in chunk:
+            cs += ord(byte)
+        _spoonfeed(radio,
+                   struct.pack("BBB128sB",
+                               0x20,
+                               130,
+                               i / 128,
+                               chunk,
+                               cs % 256))
+        radio.pipe.write("")
+        # This is really unreliable for some reason, so just
+        # blindly proceed
+        # ack = radio.pipe.read(1)
+        ack = "\x06"
+        time.sleep(0.5)
+        if ack != "\x06":
+            print repr(ack)
+            raise errors.RadioError("Radio did not ack block %i" % (i / 132))
+        #radio.pipe.read(1)
+        if radio.status_fn:
+            status = chirp_common.Status()
+            status.msg = "Cloning to radio"
+            status.cur = i
+            status.max = radio.get_memsize()
+            radio.status_fn(status)
+
+MEM_FORMAT = """
+struct memory_struct {
+  u8 unknown1;
+  u8 unknown2:2,
+     isfm:1,
+     power:2,
+     step:3;
+  u8 unknown5:2,
+     showname:1,
+     skip:1,
+     duplex:2,
+     unknown6:2;
+  u8 tmode:2,
+     unknown7:6;
+  u8 unknown8;
+  u8 unknown9:2,
+     tone:6;
+  u8 dtcs;
+  u8 name[8];
+  u16 freq;
+  u8 offset;
+};
+
+u8 headerbytes[6];
+
+#seekto 0x0006;
+u8 invisible_bits[13];
+u8 bitfield_pad[3];
+u8 invalid_bits[13];
+
+#seekto 0x017F;
+struct memory_struct memory[100];
+"""
+
+CHARSET = "".join(["%i" % i for i in range(0, 10)]) + \
+    "".join([chr(ord("A") + i) for i in range(0, 26)]) + \
+    "".join([chr(ord("a") + i) for i in range(0,26)]) + \
+    "., :;!\"#$%&'()*+-/=<>?@[?]^_`{|}????~??????????????????????????"
+            
+TMODES = ["", "Tone", "TSQL", "DTCS"]
+DUPLEX = ["", "-", "+", ""]
+POWER = [chirp_common.PowerLevel("Low1", watts=0.050),
+         chirp_common.PowerLevel("Low2", watts=1.000),
+         chirp_common.PowerLevel("Low3", watts=2.500),
+         chirp_common.PowerLevel("High", watts=5.000)]
+
+def _wipe_memory(_mem):
+    _mem.set_raw("\x00" * (_mem.size() / 8))
+
+ at directory.register
+class VXA700Radio(chirp_common.CloneModeRadio):
+    """Vertex Standard VXA-700"""
+    VENDOR = "Vertex Standard"
+    MODEL = "VXA-700"
+    _memsize = 4096
+
+    def sync_in(self):
+        try:
+            self.pipe.setTimeout(2)
+            self._mmap = _download(self)
+        except errors.RadioError:
+            raise
+        except Exception, e:
+            raise errors.RadioError("Failed to communicate " +
+                                    "with the radio: %s" % e)
+        self.process_mmap()
+
+    def sync_out(self):
+        #header[4] = 0x00 <- default
+        #            0xFF <- air band only
+        #            0x01 <- air band only
+        #            0x02 <- air band only
+        try:
+            self.pipe.setTimeout(2)
+            _upload(self)
+        except errors.RadioError:
+            raise
+        except Exception, e:
+            raise errors.RadioError("Failed to communicate " +
+                                    "with the radio: %s" % e)
+
+    def process_mmap(self):
+        self._memobj = bitwise.parse(MEM_FORMAT, self._mmap)
+
+    def get_features(self):
+        rf = chirp_common.RadioFeatures()
+        rf.has_bank = False
+        rf.has_ctone = False
+        rf.has_dtcs_polarity = False
+        rf.has_tuning_step = False
+        rf.valid_tmodes = TMODES
+        rf.valid_name_length = 8
+        rf.valid_characters = CHARSET
+        rf.valid_skips = ["", "S"]
+        rf.valid_bands = [(88000000, 165000000)]
+        rf.valid_tuning_steps = [5.0, 10.0, 12.5, 15.0, 20.0, 25.0, 50.0, 100.0]
+        rf.valid_modes = ["AM", "FM"]
+        rf.valid_power_levels = POWER
+        rf.memory_bounds = (1, 100)
+        return rf
+
+    def _get_mem(self, number):
+        return self._memobj.memory[number - 1]
+
+    def get_raw_memory(self, number):
+        _mem = self._get_mem(number)
+        return repr(_mem) + util.hexprint(_mem.get_raw())
+
+    def get_memory(self, number):
+        _mem = self._get_mem(number)
+        byte = (number - 1) / 8
+        bit = 1 << ((number - 1) % 8)
+
+        mem = chirp_common.Memory()
+        mem.number = number
+
+        if self._memobj.invisible_bits[byte] & bit:
+            mem.empty = True
+        if self._memobj.invalid_bits[byte] & bit:
+            mem.empty = True
+            return mem
+
+        if _mem.step & 0x05: # Not sure this is right, but it seems to be
+            mult = 6250
+        else:
+            mult = 5000
+
+        mem.freq = int(_mem.freq) * mult
+        mem.rtone = chirp_common.TONES[_mem.tone]
+        mem.dtcs = chirp_common.DTCS_CODES[_mem.dtcs]
+        mem.tmode = TMODES[_mem.tmode]
+        mem.duplex = DUPLEX[_mem.duplex]
+        mem.offset = int(_mem.offset) * 5000 * 10
+        mem.mode = _mem.isfm and "FM" or "AM"
+        mem.skip = _mem.skip and "S" or ""
+        mem.power = POWER[_mem.power]
+
+        for char in _mem.name:
+            try:
+                mem.name += CHARSET[char]
+            except IndexError:
+                break
+        mem.name = mem.name.rstrip()
+
+        return mem
+
+    def set_memory(self, mem):
+        _mem = self._get_mem(mem.number)
+        byte = (mem.number - 1) / 8
+        bit = 1 << ((mem.number - 1) % 8)
+
+        if mem.empty and self._memobj.invisible_bits[byte] & bit:
+            self._memobj.invalid_bits[byte] |= bit
+            return
+        if mem.empty:
+            self._memobj.invisible_bits[byte] |= bit
+            return
+
+        if self._memobj.invalid_bits[byte] & bit:
+            _wipe_memory(_mem)
+
+        self._memobj.invisible_bits[byte] &= ~bit
+        self._memobj.invalid_bits[byte] &= ~bit
+
+        _mem.unknown2 = 0x02 # Channels don't display without this
+        _mem.unknown7 = 0x01 # some bit in this field is related to
+        _mem.unknown8 = 0xFF # being able to transmit
+
+        if chirp_common.required_step(mem.freq) == 12.5:
+            mult = 6250
+            _mem.step = 0x05
+        else:
+            mult = 5000
+            _mem.step = 0x00
+
+        _mem.freq = mem.freq / mult
+        _mem.tone = chirp_common.TONES.index(mem.rtone)
+        _mem.dtcs = chirp_common.DTCS_CODES.index(mem.dtcs)
+        _mem.tmode = TMODES.index(mem.tmode)
+        _mem.duplex = DUPLEX.index(mem.duplex)
+        _mem.offset = mem.offset / 5000 / 10
+        _mem.isfm = mem.mode == "FM"
+        _mem.skip = mem.skip == "S"
+        try:
+            _mem.power = POWER.index(mem.power)
+        except ValueError:
+            _mem.power = 3 # High
+
+        for i in range(0, 8):
+            try:
+                _mem.name[i] = CHARSET.index(mem.name[i])
+            except IndexError:
+                _mem.name[i] = 0x40
+        _mem.showname = bool(mem.name.strip())
+
+    @classmethod
+    def match_model(cls, filedata, filename):
+        return len(filedata) == cls._memsize and \
+            ord(filedata[5]) == 0x0F
diff --git a/chirp/wouxun.py b/chirp/wouxun.py
new file mode 100644
index 0000000..bb1f3c9
--- /dev/null
+++ b/chirp/wouxun.py
@@ -0,0 +1,1012 @@
+# Copyright 2011 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/>.
+
+"""Wouxun radios management module"""
+
+import time
+import os
+from chirp import util, chirp_common, bitwise, memmap, errors, directory
+from chirp.settings import RadioSetting, RadioSettingGroup, \
+                RadioSettingValueBoolean, RadioSettingValueList, \
+                RadioSettingValueInteger, RadioSettingValueString
+from chirp.wouxun_common import wipe_memory, do_download, do_upload
+
+FREQ_ENCODE_TABLE = [ 0x7, 0xa, 0x0, 0x9, 0xb, 0x2, 0xe, 0x1, 0x3, 0xf ]
+ 
+def encode_freq(freq):
+    """Convert frequency (4 decimal digits) to wouxun format (2 bytes)"""
+    enc = 0
+    div = 1000
+    for i in range(0, 4):
+        enc <<= 4
+        enc |= FREQ_ENCODE_TABLE[ (freq/div) % 10 ]
+        div /= 10
+    return enc
+
+def decode_freq(data):
+    """Convert from wouxun format (2 bytes) to frequency (4 decimal digits)"""
+    freq = 0
+    shift = 12
+    for i in range(0, 4):
+        freq *= 10
+        freq += FREQ_ENCODE_TABLE.index( (data>>shift) & 0xf )
+        shift -= 4
+        # print "data %04x freq %d shift %d" % (data, freq, shift)
+    return freq
+
+ at directory.register
+class KGUVD1PRadio(chirp_common.CloneModeRadio,
+        chirp_common.ExperimentalRadio):
+    """Wouxun KG-UVD1P,UV2,UV3"""
+    VENDOR = "Wouxun"
+    MODEL = "KG-UVD1P"
+    _model = "KG669V"
+    
+    _querymodel = "HiWOUXUN\x02"
+    
+    CHARSET = list("0123456789") + [chr(x + ord("A")) for x in range(0, 26)] + \
+        list("?+-")
+
+    POWER_LEVELS = [chirp_common.PowerLevel("High", watts=5.00),
+                    chirp_common.PowerLevel("Low", watts=1.00)]
+                    
+    valid_freq = [(136000000, 175000000), (216000000, 520000000)]
+
+
+    _MEM_FORMAT = """
+        #seekto 0x0010;
+        struct {
+          lbcd rx_freq[4];
+          lbcd tx_freq[4];
+          ul16 rx_tone;
+          ul16 tx_tone;
+          u8 _3_unknown_1:4,
+             bcl:1,
+             _3_unknown_2:3;
+          u8 splitdup:1,
+             skip:1,
+             power_high:1,
+             iswide:1,
+             _2_unknown_2:4;
+          u8 unknown[2];
+        } memory[199];
+        
+        #seekto 0x0970;
+        struct {
+            u16 vhf_rx_start;
+            u16 vhf_rx_stop;
+            u16 uhf_rx_start;
+            u16 uhf_rx_stop;
+            u16 vhf_tx_start;
+            u16 vhf_tx_stop;
+            u16 uhf_tx_start;
+            u16 uhf_tx_stop;
+        } freq_ranges;
+
+        #seekto 0x0E5C;
+        struct {
+          u8 unknown_flag1:7,
+             menu_available:1;
+        } settings;
+
+        #seekto 0x1008;
+        struct {
+          u8 unknown[8];
+          u8 name[6];
+          u8 pad[2];
+        } names[199];
+    """
+
+    @classmethod
+    def get_experimental_warning(cls):
+        return ('This version of the Wouxun driver allows you to modify the '
+                'frequency range settings of your radio. This has been tested '
+                'and reports from other users indicate that it is a safe '
+                'thing to do. However, modifications to this value may have '
+                'unintended consequences, including damage to your device. '
+                'You have been warned. Proceed at your own risk!')
+
+    def _identify(self):
+        """Do the original wouxun identification dance"""
+        for _i in range(0, 5):
+            self.pipe.write(self._querymodel)
+            resp = self.pipe.read(9)
+            if len(resp) != 9:
+                print "Got:\n%s" % util.hexprint(resp)
+                print "Retrying identification..."
+                time.sleep(1)
+                continue
+            if resp[2:8] != self._model:
+                raise Exception("I can't talk to this model (%s)" % 
+                    util.hexprint(resp))
+            return
+        if len(resp) == 0:
+            raise Exception("Radio not responding")
+        else:
+            raise Exception("Unable to identify radio")
+
+    def _start_transfer(self):
+        """Tell the radio to go into transfer mode"""
+        self.pipe.write("\x02\x06")
+        time.sleep(0.05)
+        ack = self.pipe.read(1)
+        if ack != "\x06":
+            raise Exception("Radio refused transfer mode")    
+    def _download(self):
+        """Talk to an original wouxun and do a download"""
+        try:
+            self._identify()
+            self._start_transfer()
+            return do_download(self, 0x0000, 0x2000, 0x0040)
+        except errors.RadioError:
+            raise
+        except Exception, e:
+            raise errors.RadioError("Failed to communicate with radio: %s" % e)
+
+    def _upload(self):
+        """Talk to an original wouxun and do an upload"""
+        try:
+            self._identify()
+            self._start_transfer()
+            return do_upload(self, 0x0000, 0x2000, 0x0010)
+        except errors.RadioError:
+            raise
+        except Exception, e:
+            raise errors.RadioError("Failed to communicate with radio: %s" % e)
+
+
+    def sync_in(self):
+        self._mmap = self._download()
+        self.process_mmap()
+
+    def sync_out(self):
+        self._upload()
+
+    def process_mmap(self):
+        if len(self._mmap.get_packed()) != 8192:
+            print "NOTE: Fixing old-style Wouxun image"
+            # Originally, CHIRP's wouxun image had eight bytes of
+            # static data, followed by the first memory at offset
+            # 0x0008.  Between 0.1.11 and 0.1.12, this was fixed to 16
+            # bytes of (whatever) followed by the first memory at
+            # offset 0x0010, like the radio actually stores it.  So,
+            # if we find one of those old ones, convert it to the new
+            # format, padding 16 bytes of 0xFF in front.
+            self._mmap = memmap.MemoryMap(("\xFF" * 16) + \
+                                              self._mmap.get_packed()[8:8184])
+        self._memobj = bitwise.parse(self._MEM_FORMAT, self._mmap)
+
+    def get_features(self):
+        rf = chirp_common.RadioFeatures()
+        rf.valid_tmodes = ["", "Tone", "TSQL", "DTCS", "Cross"]
+        rf.valid_cross_modes = [
+                        "Tone->Tone",
+                        "Tone->DTCS",
+                        "DTCS->Tone",
+                        "DTCS->",
+                        "->Tone",
+                        "->DTCS",
+                        "DTCS->DTCS",
+                    ]
+        rf.valid_modes = ["FM", "NFM"]
+        rf.valid_power_levels = self.POWER_LEVELS
+        rf.valid_bands = self.valid_freq
+        rf.valid_characters = "".join(self.CHARSET)
+        rf.valid_name_length = 6
+        rf.valid_duplexes = ["", "+", "-", "split", "off"]
+        rf.has_ctone = True
+        rf.has_rx_dtcs = True
+        rf.has_cross = True
+        rf.has_tuning_step = False
+        rf.has_bank = False
+        rf.has_settings = True
+        rf.memory_bounds = (1, 128)
+        rf.can_odd_split = True
+        return rf
+
+    def get_settings(self):
+        freqranges = RadioSettingGroup("freqranges", "Freq ranges")
+        top = RadioSettingGroup("top", "All Settings", freqranges)
+
+        rs = RadioSetting("menu_available", "Menu Available",
+                          RadioSettingValueBoolean(
+                            self._memobj.settings.menu_available))
+        top.append(rs)
+
+        rs = RadioSetting("vhf_rx_start", "VHF RX Lower Limit (MHz)",
+                          RadioSettingValueInteger(136, 174, 
+                                decode_freq(
+                                    self._memobj.freq_ranges.vhf_rx_start)))
+        freqranges.append(rs)
+        rs = RadioSetting("vhf_rx_stop", "VHF RX Upper Limit (MHz)",
+                          RadioSettingValueInteger(136, 174, 
+                                decode_freq(
+                                    self._memobj.freq_ranges.vhf_rx_stop)))
+        freqranges.append(rs)
+        rs = RadioSetting("uhf_rx_start", "UHF RX Lower Limit (MHz)",
+                          RadioSettingValueInteger(216, 520, 
+                                decode_freq(
+                                    self._memobj.freq_ranges.uhf_rx_start)))
+        freqranges.append(rs)
+        rs = RadioSetting("uhf_rx_stop", "UHF RX Upper Limit (MHz)",
+                          RadioSettingValueInteger(216, 520, 
+                                decode_freq(
+                                    self._memobj.freq_ranges.uhf_rx_stop)))
+        freqranges.append(rs)
+        rs = RadioSetting("vhf_tx_start", "VHF TX Lower Limit (MHz)",
+                          RadioSettingValueInteger(136, 174, 
+                                decode_freq(
+                                    self._memobj.freq_ranges.vhf_tx_start)))
+        freqranges.append(rs)
+        rs = RadioSetting("vhf_tx_stop", "VHF TX Upper Limit (MHz)",
+                          RadioSettingValueInteger(136, 174, 
+                                decode_freq(
+                                    self._memobj.freq_ranges.vhf_tx_stop)))
+        freqranges.append(rs)
+        rs = RadioSetting("uhf_tx_start", "UHF TX Lower Limit (MHz)",
+                          RadioSettingValueInteger(216, 520, 
+                                decode_freq(
+                                    self._memobj.freq_ranges.uhf_tx_start)))
+        freqranges.append(rs)
+        rs = RadioSetting("uhf_tx_stop", "UHF TX Upper Limit (MHz)",
+                          RadioSettingValueInteger(216, 520, 
+                                decode_freq(
+                                    self._memobj.freq_ranges.uhf_tx_stop)))
+        freqranges.append(rs)
+        
+        # tell the decoded ranges to UI
+        self.valid_freq = [
+            ( decode_freq(self._memobj.freq_ranges.vhf_rx_start) * 1000000, 
+             (decode_freq(self._memobj.freq_ranges.vhf_rx_stop)+1) * 1000000), 
+            ( decode_freq(self._memobj.freq_ranges.uhf_rx_start)  * 1000000,
+             (decode_freq(self._memobj.freq_ranges.uhf_rx_stop)+1) * 1000000)]
+
+        return top
+
+    def set_settings(self, settings):
+        for element in settings:
+            if not isinstance(element, RadioSetting):
+                if element.get_name() != "freqranges" :
+                    self.set_settings(element)
+                else:
+                    self._set_freq_settings(element)
+            else:
+                try:
+                    if "." in element.get_name():
+                        bits = element.get_name().split(".")
+                        obj = self._memobj
+                        for bit in bits[:-1]:
+                            obj = getattr(obj, bit)
+                        setting = bits[-1]
+                    else:
+                        obj = self._memobj.settings
+                        setting = element.get_name()
+                    print "Setting %s = %s" % (setting, element.value)
+                    setattr(obj, setting, element.value)
+                except Exception, e:
+                    print element.get_name()
+                    raise
+
+    def _set_freq_settings(self, settings):
+        for element in settings:
+            try:
+                setattr(self._memobj.freq_ranges,
+                        element.get_name(),
+                        encode_freq(int(element.value)))
+            except Exception, e:
+                print element.get_name()
+                raise
+
+    def get_raw_memory(self, number):
+        return repr(self._memobj.memory[number - 1])
+
+    def _get_tone(self, _mem, mem):
+        def _get_dcs(val):
+            code = int("%03o" % (val & 0x07FF))
+            pol = (val & 0x8000) and "R" or "N"
+            return code, pol
+            
+        if _mem.tx_tone != 0xFFFF and _mem.tx_tone > 0x2800:
+            tcode, tpol = _get_dcs(_mem.tx_tone)
+            mem.dtcs = tcode
+            txmode = "DTCS"
+        elif _mem.tx_tone != 0xFFFF:
+            mem.rtone = _mem.tx_tone / 10.0
+            txmode = "Tone"
+        else:
+            txmode = ""
+
+        if _mem.rx_tone != 0xFFFF and _mem.rx_tone > 0x2800:
+            rcode, rpol = _get_dcs(_mem.rx_tone)
+            mem.rx_dtcs = rcode
+            rxmode = "DTCS"
+        elif _mem.rx_tone != 0xFFFF:
+            mem.ctone = _mem.rx_tone / 10.0
+            rxmode = "Tone"
+        else:
+            rxmode = ""
+
+        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)
+
+        if mem.tmode == "DTCS":
+            mem.dtcs_polarity = "%s%s" % (tpol, rpol)
+
+        if os.getenv("CHIRP_DEBUG"):
+            print "Got TX %s (%i) RX %s (%i)" % (txmode, _mem.tx_tone,
+                                                 rxmode, _mem.rx_tone)
+
+    def _is_txinh(self, _mem):
+        raw_tx = ""
+        for i in range(0, 4):
+            raw_tx += _mem.tx_freq[i].get_raw()
+        return raw_tx == "\xFF\xFF\xFF\xFF"
+
+    def get_memory(self, number):
+        _mem = self._memobj.memory[number - 1]
+        _nam = self._memobj.names[number - 1]
+
+        mem = chirp_common.Memory()
+        mem.number = number
+
+        if _mem.get_raw() == ("\xff" * 16):
+            mem.empty = True
+            return mem
+
+        mem.freq = int(_mem.rx_freq) * 10
+        if _mem.splitdup:
+            mem.duplex = "split"
+        elif self._is_txinh(_mem):
+            mem.duplex = "off"
+        elif int(_mem.rx_freq) < int(_mem.tx_freq):
+            mem.duplex = "+"
+        elif int(_mem.rx_freq) > int(_mem.tx_freq):
+            mem.duplex = "-"
+
+        if mem.duplex == "" or mem.duplex == "off":
+            mem.offset = 0
+        elif mem.duplex == "split":
+            mem.offset = int(_mem.tx_freq) * 10
+        else:
+            mem.offset = abs(int(_mem.tx_freq) - int(_mem.rx_freq)) * 10
+
+        if not _mem.skip:
+            mem.skip = "S"
+        if not _mem.iswide:
+            mem.mode = "NFM"
+
+        self._get_tone(_mem, mem)
+
+        mem.power = self.POWER_LEVELS[not _mem.power_high]
+
+        for i in _nam.name:
+            if i == 0xFF:
+                break
+            mem.name += self.CHARSET[i]
+
+        mem.extra = RadioSettingGroup("extra", "Extra")
+        bcl = RadioSetting("bcl", "BCL",
+                           RadioSettingValueBoolean(bool(_mem.bcl)))
+        bcl.set_doc("Busy Channel Lockout")
+        mem.extra.append(bcl)
+
+        return mem
+
+    def _set_tone(self, mem, _mem):
+        def _set_dcs(code, pol):
+            val = int("%i" % code, 8) + 0x2800
+            if pol == "R":
+                val += 0xA000
+            return val
+
+        if mem.tmode == "Cross":
+            tx_mode, rx_mode = mem.cross_mode.split("->")
+        elif mem.tmode == "Tone":
+            tx_mode = mem.tmode
+            rx_mode = None
+        else:
+            tx_mode = rx_mode = mem.tmode
+
+
+        if tx_mode == "DTCS":
+            _mem.tx_tone = mem.tmode != "DTCS" and \
+                _set_dcs(mem.dtcs, mem.dtcs_polarity[0]) or \
+                _set_dcs(mem.rx_dtcs, mem.dtcs_polarity[0])
+        elif tx_mode:
+            _mem.tx_tone = tx_mode == "Tone" and \
+                int(mem.rtone * 10) or int(mem.ctone * 10)
+        else:
+            _mem.tx_tone = 0xFFFF
+
+        if rx_mode == "DTCS":
+            _mem.rx_tone = _set_dcs(mem.rx_dtcs, mem.dtcs_polarity[1])
+        elif rx_mode:
+            _mem.rx_tone = int(mem.ctone * 10)
+        else:
+            _mem.rx_tone = 0xFFFF
+
+        if os.getenv("CHIRP_DEBUG"):
+            print "Set TX %s (%i) RX %s (%i)" % (tx_mode, _mem.tx_tone,
+                                                 rx_mode, _mem.rx_tone)
+
+    def set_memory(self, mem):
+        _mem = self._memobj.memory[mem.number - 1]
+        _nam = self._memobj.names[mem.number - 1]
+
+        if mem.empty:
+            wipe_memory(_mem, "\xFF")
+            return
+
+        if _mem.get_raw() == ("\xFF" * 16):
+            wipe_memory(_mem, "\x00")
+
+        _mem.rx_freq = int(mem.freq / 10)
+        if mem.duplex == "split":
+            _mem.tx_freq = int(mem.offset / 10)
+        elif mem.duplex == "off":
+            for i in range(0, 4):
+                _mem.tx_freq[i].set_raw("\xFF")
+        elif mem.duplex == "+":
+            _mem.tx_freq = int(mem.freq / 10) + int(mem.offset / 10)
+        elif mem.duplex == "-":
+            _mem.tx_freq = int(mem.freq / 10) - int(mem.offset / 10)
+        else:
+            _mem.tx_freq = int(mem.freq / 10)
+        _mem.splitdup = mem.duplex == "split"
+        _mem.skip = mem.skip != "S"
+        _mem.iswide = mem.mode != "NFM"
+
+        self._set_tone(mem, _mem)
+
+        if mem.power:
+            _mem.power_high = not self.POWER_LEVELS.index(mem.power)
+        else:
+            _mem.power_high = True
+
+        _nam.name = [0xFF] * 6
+        for i in range(0, len(mem.name)):
+            try:
+                _nam.name[i] = self.CHARSET.index(mem.name[i])
+            except IndexError:
+                raise Exception("Character `%s' not supported")
+
+        for setting in mem.extra:
+            setattr(_mem, setting.get_name(), setting.value)
+
+    @classmethod
+    def match_model(cls, filedata, filename):
+        # New-style image (CHIRP 0.1.12)
+        if len(filedata) == 8192 and \
+                filedata[0x60:0x64] != "2009" and \
+                filedata[0x1f77:0x1f7d] == "\xff\xff\xff\xff\xff\xff" and \
+                filedata[0x0d70:0x0d80] == "\xff\xff\xff\xff\xff\xff\xff\xff" \
+                                           "\xff\xff\xff\xff\xff\xff\xff\xff": 
+                # those areas are (seems to be) unused
+            return True
+        # Old-style image (CHIRP 0.1.11)
+        if len(filedata) == 8200 and \
+                filedata[0:4] == "\x01\x00\x00\x00":
+            return True
+        return False
+
+ at directory.register
+class KGUV6DRadio(KGUVD1PRadio):
+    """Wouxun KG-UV6 (D and X variants)"""
+    MODEL = "KG-UV6"
+    
+    _querymodel = "HiWXUVD1\x02"
+    
+    _MEM_FORMAT = """
+        #seekto 0x0010;
+        struct {
+          lbcd rx_freq[4];
+          lbcd tx_freq[4];
+          ul16 rx_tone;
+          ul16 tx_tone;
+          u8 _3_unknown_1:4,
+             bcl:1,
+             _3_unknown_2:3;
+          u8 splitdup:1,
+             skip:1,
+             power_high:1,
+             iswide:1,
+             _2_unknown_2:4;
+          u8 pad[2];
+        } memory[199];
+
+        #seekto 0x0F00;
+        struct {
+          char welcome1[6];
+          char welcome2[6];
+          char single_band[6];
+        } strings;
+
+        #seekto 0x0F20;
+        struct {
+          u8 unknown_flag_01:6,
+             vfo_b_ch_disp:2;
+          u8 unknown_flag_02:5,
+             vfo_a_fr_step:3;
+          u8 unknown_flag_03:4,
+             vfo_a_squelch:4;
+          u8 unknown_flag_04:7,
+             power_save:1;
+          u8 unknown_flag_05:5,
+             pf2_function:3;
+          u8 unknown_flag_06:6,
+             roger_beep:2;
+          u8 unknown_flag_07:2,
+             transmit_time_out:6;
+          u8 unknown_flag_08:4,
+             vox:4;
+          u8 unknown_1[4];
+          u8 unknown_flag_09:6,
+             voice:2;
+          u8 unknown_flag_10:7,
+             beep:1;
+          u8 unknown_flag_11:7,
+             ani_id_enable:1;
+          u8 unknown_2[2];
+          u8 unknown_flag_12:5,
+             vfo_b_fr_step:3;
+          u8 unknown_3[1];
+          u8 unknown_flag_13:3,
+             ani_id_tx_delay:5;
+          u8 unknown_4[1];
+          u8 unknown_flag_14:6,
+             ani_id_sidetone:2;
+          u8 unknown_flag_15:4,
+             tx_time_out_alert:4;
+          u8 unknown_flag_16:6,
+             vfo_a_ch_disp:2;
+          u8 unknown_flag_15:6,
+             scan_mode:2;
+          u8 unknown_flag_16:7,
+             kbd_lock:1;
+          u8 unknown_flag_17:6,
+             ponmsg:2;
+          u8 unknown_flag_18:5,
+             pf1_function:3;
+          u8 unknown_5[1];
+          u8 unknown_flag_19:7,
+             auto_backlight:1;
+          u8 unknown_flag_20:7,
+             sos_ch:1;
+          u8 unknown_6[2];
+          u8 unknown_flag_21:7,
+             auto_lock_kbd:1;
+          u8 unknown_flag_22:4,
+             vfo_b_squelch:4;
+          u8 unknown_7[1];
+          u8 unknown_flag_23:7,
+             stopwatch:1;
+          u8 vfo_a_cur_chan;
+          u8 unknown_flag_24:7,
+             dual_band_receive:1;
+          u8 current_vfo:1,
+             unknown_flag_24:7;
+          u8 unknown_8[2];
+          u8 mode_password[6];
+          u8 reset_password[6];
+          u8 ani_id_content[6];
+          u8 unknown_flag_25:7,
+             menu_available:1;
+          u8 unknown_9[1];
+          u8 priority_chan;
+          u8 vfo_b_cur_chan;
+        } settings;
+
+        #seekto 0x0f60;
+        struct {
+          lbcd rx_freq[4];
+          lbcd tx_freq[4];
+          ul16 rx_tone;
+          ul16 tx_tone;
+          u8 _3_unknown_3:4,
+             bcl:1,
+             _3_unknown_4:3;
+          u8 splitdup:1,
+             _2_unknown_3:1,
+             power_high:1,
+             iswide:1,
+             _2_unknown_4:4;
+          u8 pad[2];
+        } vfo_settings[2];
+	
+        #seekto 0x0f80;
+        u16 fm_presets_0[9];
+
+        #seekto 0x0ff0;
+        struct {
+            u16 vhf_rx_start;
+            u16 vhf_rx_stop;
+            u16 uhf_rx_start;
+            u16 uhf_rx_stop;
+            u16 vhf_tx_start;
+            u16 vhf_tx_stop;
+            u16 uhf_tx_start;
+            u16 uhf_tx_stop;
+        } freq_ranges;
+
+        #seekto 0x1010;
+        struct {
+          u8 name[6];
+          u8 pad[10];
+        } names[199];
+
+        #seekto 0x1f60;
+        struct {
+            u8 unknown_flag_26:6,
+               tx_offset_dir:2;
+            u8 tx_offset[6];
+            u8 pad[9];
+        } vfo_offset[2];
+
+        #seekto 0x1f80;
+        u16 fm_presets_1[9];
+    """
+
+
+    def get_features(self):
+        rf = KGUVD1PRadio.get_features(self)
+        rf.memory_bounds = (1, 199)
+        return rf
+
+    def get_settings(self):
+        freqranges = RadioSettingGroup("freqranges", "Freq ranges")
+        top = RadioSettingGroup("top", "All Settings", freqranges)
+
+        rs = RadioSetting("menu_available", "Menu Available",
+                          RadioSettingValueBoolean(
+                            self._memobj.settings.menu_available))
+        top.append(rs)
+
+        rs = RadioSetting("vhf_rx_start", "VHF RX Lower Limit (MHz)",
+                          RadioSettingValueInteger(1, 1000, 
+                                decode_freq(
+                                    self._memobj.freq_ranges.vhf_rx_start)))
+        freqranges.append(rs)
+        rs = RadioSetting("vhf_rx_stop", "VHF RX Upper Limit (MHz)",
+                          RadioSettingValueInteger(1, 1000, 
+                                decode_freq(
+                                    self._memobj.freq_ranges.vhf_rx_stop)))
+        freqranges.append(rs)
+        rs = RadioSetting("uhf_rx_start", "UHF RX Lower Limit (MHz)",
+                          RadioSettingValueInteger(1, 1000, 
+                                decode_freq(
+                                    self._memobj.freq_ranges.uhf_rx_start)))
+        freqranges.append(rs)
+        rs = RadioSetting("uhf_rx_stop", "UHF RX Upper Limit (MHz)",
+                          RadioSettingValueInteger(1, 1000, 
+                                decode_freq(
+                                    self._memobj.freq_ranges.uhf_rx_stop)))
+        freqranges.append(rs)
+        rs = RadioSetting("vhf_tx_start", "VHF TX Lower Limit (MHz)",
+                          RadioSettingValueInteger(1, 1000, 
+                                decode_freq(
+                                    self._memobj.freq_ranges.vhf_tx_start)))
+        freqranges.append(rs)
+        rs = RadioSetting("vhf_tx_stop", "VHF TX Upper Limit (MHz)",
+                          RadioSettingValueInteger(1, 1000, 
+                                decode_freq(
+                                    self._memobj.freq_ranges.vhf_tx_stop)))
+        freqranges.append(rs)
+        rs = RadioSetting("uhf_tx_start", "UHF TX Lower Limit (MHz)",
+                          RadioSettingValueInteger(1, 1000, 
+                                decode_freq(
+                                    self._memobj.freq_ranges.uhf_tx_start)))
+        freqranges.append(rs)
+        rs = RadioSetting("uhf_tx_stop", "UHF TX Upper Limit (MHz)",
+                          RadioSettingValueInteger(1, 1000, 
+                                decode_freq(
+                                    self._memobj.freq_ranges.uhf_tx_stop)))
+        freqranges.append(rs)
+        
+        # tell the decoded ranges to UI
+        self.valid_freq = [
+            ( decode_freq(self._memobj.freq_ranges.vhf_rx_start) * 1000000, 
+             (decode_freq(self._memobj.freq_ranges.vhf_rx_stop)+1) * 1000000), 
+            ( decode_freq(self._memobj.freq_ranges.uhf_rx_start)  * 1000000,
+             (decode_freq(self._memobj.freq_ranges.uhf_rx_stop)+1) * 1000000)]
+
+        def _filter(name):
+            filtered = ""
+            for char in str(name):
+                if char in chirp_common.CHARSET_ASCII:
+                    filtered += char
+                else:
+                    filtered += " "
+            return filtered
+
+        # add some radio specific settings
+        options = ["Off", "Welcome", "V bat"]
+        rs = RadioSetting("ponmsg", "Poweron message",
+                          RadioSettingValueList(options,
+                                        options[self._memobj.settings.ponmsg]))
+        top.append(rs)
+        rs = RadioSetting("strings.welcome1", "Power-On Message 1",
+                          RadioSettingValueString(0, 6, _filter(self._memobj.strings.welcome1)))
+        top.append(rs)
+        rs = RadioSetting("strings.welcome2", "Power-On Message 2",
+                          RadioSettingValueString(0, 6, _filter(self._memobj.strings.welcome2)))
+        top.append(rs)
+        rs = RadioSetting("strings.single_band", "Single Band Message",
+                          RadioSettingValueString(0, 6, _filter(self._memobj.strings.single_band)))
+        top.append(rs)
+        options = ["Channel", "ch/freq","Name", "VFO"]
+        rs = RadioSetting("vfo_a_ch_disp", "VFO A Channel disp mode",
+                          RadioSettingValueList(options,
+                                        options[self._memobj.settings.vfo_a_ch_disp]))
+        top.append(rs)
+        rs = RadioSetting("vfo_b_ch_disp", "VFO B Channel disp mode",
+                          RadioSettingValueList(options,
+                                        options[self._memobj.settings.vfo_b_ch_disp]))
+        top.append(rs)
+	# TODO - vfo_a_fr_step
+	# TODO -vfo_b_fr_step:3;
+        rs = RadioSetting("vfo_a_squelch", "VFO A Squelch",
+                          RadioSettingValueInteger(0, 9, self._memobj.settings.vfo_a_squelch))
+        top.append(rs)
+        rs = RadioSetting("vfo_b_squelch", "VFO B Squelch",
+                          RadioSettingValueInteger(0, 9, self._memobj.settings.vfo_b_squelch))
+        top.append(rs)
+        rs = RadioSetting("vfo_a_cur_chan", "VFO A current channel",
+                          RadioSettingValueInteger(1, 199, self._memobj.settings.vfo_a_cur_chan))
+        top.append(rs)
+        rs = RadioSetting("vfo_b_cur_chan", "VFO B current channel",
+                          RadioSettingValueInteger(0, 199, self._memobj.settings.vfo_b_cur_chan))
+        top.append(rs)
+        rs = RadioSetting("priority_chan", "Priority channel",
+                          RadioSettingValueInteger(0, 199, self._memobj.settings.priority_chan))
+        top.append(rs)
+        rs = RadioSetting("power_save", "Power save",
+                          RadioSettingValueBoolean(self._memobj.settings.power_save))
+        top.append(rs)
+        options = ["Off", "Scan", "Lamp", "SOS", "Radio"]
+        rs = RadioSetting("pf1_function", "PF1 Function select",
+                          RadioSettingValueList(options,
+                                        options[self._memobj.settings.pf1_function]))
+        top.append(rs)
+        options = ["Off", "Radio", "fr/ch", "Rpt", "Stopwatch", "Lamp", "SOS"]
+        rs = RadioSetting("pf2_function", "PF2 Function select",
+                          RadioSettingValueList(options,
+                                        options[self._memobj.settings.pf2_function]))
+        top.append(rs)
+        options = ["Off", "Begin", "End", "Both"]
+        rs = RadioSetting("roger_beep", "Roger beep select",
+                          RadioSettingValueList(options,
+                                        options[self._memobj.settings.roger_beep]))
+        top.append(rs)
+        # TODO - transmit_time_out:6;
+        rs = RadioSetting("vox", "Vox",
+                          RadioSettingValueInteger(0, 10, self._memobj.settings.vox))
+        top.append(rs)
+        options = ["Off", "Chinese", "English"]
+        rs = RadioSetting("voice", "Voice",
+                          RadioSettingValueList(options,
+                                        options[self._memobj.settings.voice]))
+        top.append(rs)
+        rs = RadioSetting("beep", "Beep",
+                          RadioSettingValueBoolean(self._memobj.settings.beep))
+        top.append(rs)
+        rs = RadioSetting("ani_id_enable", "ANI id enable",
+                          RadioSettingValueBoolean(self._memobj.settings.ani_id_enable))
+        top.append(rs)
+        rs = RadioSetting("ani_id_tx_delay", "ANI id tx delay",
+                          RadioSettingValueInteger(0, 30, self._memobj.settings.ani_id_tx_delay))
+        top.append(rs)
+        options = ["Off", "Key", "ANI", "Key+ANI"]
+        rs = RadioSetting("ani_id_sidetone", "ANI id sidetone",
+                          RadioSettingValueList(options,
+                                        options[self._memobj.settings.ani_id_sidetone]))
+        top.append(rs)
+        # TODO tx_time_out_alert:4;
+        options = ["Time", "Carrier", "Search"]
+        rs = RadioSetting("scan_mode", "Scan mode",
+                          RadioSettingValueList(options,
+                                        options[self._memobj.settings.scan_mode]))
+        top.append(rs)
+        rs = RadioSetting("kbd_lock", "Keyboard lock",
+                          RadioSettingValueBoolean(self._memobj.settings.kbd_lock))
+        top.append(rs)
+        rs = RadioSetting("auto_lock_kbd", "Auto lock keyboard",
+                          RadioSettingValueBoolean(self._memobj.settings.auto_lock_kbd))
+        top.append(rs)
+        rs = RadioSetting("auto_backlight", "Auto backlight",
+                          RadioSettingValueBoolean(self._memobj.settings.auto_backlight))
+        top.append(rs)
+        options = ["CH A", "CH B"]
+        rs = RadioSetting("sos_ch", "SOS CH",
+                          RadioSettingValueList(options,
+                                        options[self._memobj.settings.sos_ch]))
+        top.append(rs)
+        rs = RadioSetting("stopwatch", "Stopwatch",
+                          RadioSettingValueBoolean(self._memobj.settings.stopwatch))
+        top.append(rs)
+        rs = RadioSetting("dual_band_receive", "Dual band receive",
+                          RadioSettingValueBoolean(self._memobj.settings.dual_band_receive))
+        top.append(rs)
+        options = ["VFO A", "VFO B"]
+        rs = RadioSetting("current_vfo", "Current VFO",
+                          RadioSettingValueList(options,
+                                        options[self._memobj.settings.current_vfo]))
+        top.append(rs)
+        _pwd = self._memobj.settings.mode_password
+        rs = RadioSetting("mode_password", "Mode password (000000 disabled)",
+                  RadioSettingValueInteger(0, 9, _pwd[0]),
+                  RadioSettingValueInteger(0, 9, _pwd[1]),
+                  RadioSettingValueInteger(0, 9, _pwd[2]),
+                  RadioSettingValueInteger(0, 9, _pwd[3]),
+                  RadioSettingValueInteger(0, 9, _pwd[4]),
+                  RadioSettingValueInteger(0, 9, _pwd[5]))
+        top.append(rs)
+        _pwd = self._memobj.settings.reset_password
+        rs = RadioSetting("reset_password", "Reset password (000000 disabled)",
+                  RadioSettingValueInteger(0, 9, _pwd[0]),
+                  RadioSettingValueInteger(0, 9, _pwd[1]),
+                  RadioSettingValueInteger(0, 9, _pwd[2]),
+                  RadioSettingValueInteger(0, 9, _pwd[3]),
+                  RadioSettingValueInteger(0, 9, _pwd[4]),
+                  RadioSettingValueInteger(0, 9, _pwd[5]))
+        top.append(rs)
+        try:
+            _ani = self._memobj.settings.ani_id_content
+            rs = RadioSetting("ani_id_content", "ANI Code",
+                              RadioSettingValueInteger(0, 9, _ani[0]),
+                              RadioSettingValueInteger(0, 9, _ani[1]),
+                              RadioSettingValueInteger(0, 9, _ani[2]),
+                              RadioSettingValueInteger(0, 9, _ani[3]),
+                              RadioSettingValueInteger(0, 9, _ani[4]),
+                              RadioSettingValueInteger(0, 9, _ani[5]))
+            top.append(rs)
+        except Exception:
+            print ("Your ANI code is not five digits, which is not currently"
+                   " supported in CHIRP.")            
+
+        return top
+
+    @classmethod
+    def match_model(cls, filedata, filename):
+        if len(filedata) == 8192 and \
+                filedata[0x1f77:0x1f7d] == "WELCOM":
+            return True
+        return False
+
+ at directory.register
+class KG816Radio(KGUVD1PRadio,
+        chirp_common.ExperimentalRadio):
+    """Wouxun KG816"""
+    MODEL = "KG816"
+
+    _MEM_FORMAT = """
+        #seekto 0x0010;
+        struct {
+          lbcd rx_freq[4];
+          lbcd tx_freq[4];
+          ul16 rx_tone;
+          ul16 tx_tone;
+          u8 _3_unknown_1:4,
+             bcl:1,
+             _3_unknown_2:3;
+          u8 splitdup:1,
+             skip:1,
+             power_high:1,
+             iswide:1,
+             _2_unknown_2:4;
+          u8 unknown[2];
+        } memory[199];
+        
+        #seekto 0x0d70;
+        struct {
+            u16 vhf_rx_start;
+            u16 vhf_rx_stop;
+            u16 uhf_rx_start;
+            u16 uhf_rx_stop;
+            u16 vhf_tx_start;
+            u16 vhf_tx_stop;
+            u16 uhf_tx_start;
+            u16 uhf_tx_stop;
+        } freq_ranges;
+
+        #seekto 0x1010;
+        struct {
+		u8 name[6];
+		u8 pad[10];
+        } names[199];
+	
+    """
+
+    @classmethod
+    def get_experimental_warning(cls):
+        return ('We have not that much information on this model '
+                'up to now we only know it has the same memory '
+                'organization of KGUVD1 but uses 199 memories. '
+                'it has been reported to work but '
+                'proceed at your own risk!')
+    
+    def get_features(self):
+        rf = KGUVD1PRadio.get_features(self)
+        rf.memory_bounds = (1, 199) # this is the only known difference
+        return rf
+
+    def get_settings(self):
+        freqranges = RadioSettingGroup("freqranges", "Freq ranges (read only)")
+        top = RadioSettingGroup("top", "All Settings", freqranges)
+
+        rs = RadioSetting("vhf_rx_start", "vhf rx start",
+                          RadioSettingValueInteger(136, 520, 
+                                decode_freq(
+                                    self._memobj.freq_ranges.vhf_rx_start)))
+        freqranges.append(rs)
+        rs = RadioSetting("vhf_rx_stop", "vhf rx stop",
+                          RadioSettingValueInteger(136, 520, 
+                                decode_freq(
+                                    self._memobj.freq_ranges.vhf_rx_stop)))
+        freqranges.append(rs)
+        rs = RadioSetting("uhf_rx_start", "uhf rx start",
+                          RadioSettingValueInteger(136, 520, 
+                                decode_freq(
+                                    self._memobj.freq_ranges.uhf_rx_start)))
+        freqranges.append(rs)
+        rs = RadioSetting("uhf_rx_stop", "uhf rx stop",
+                          RadioSettingValueInteger(136, 520, 
+                                decode_freq(
+                                    self._memobj.freq_ranges.uhf_rx_stop)))
+        freqranges.append(rs)
+        rs = RadioSetting("vhf_tx_start", "vhf tx start",
+                          RadioSettingValueInteger(136, 520, 
+                                decode_freq(
+                                    self._memobj.freq_ranges.vhf_tx_start)))
+        freqranges.append(rs)
+        rs = RadioSetting("vhf_tx_stop", "vhf tx stop",
+                          RadioSettingValueInteger(136, 520, 
+                                decode_freq(
+                                    self._memobj.freq_ranges.vhf_tx_stop)))
+        freqranges.append(rs)
+        rs = RadioSetting("uhf_tx_start", "uhf tx start",
+                          RadioSettingValueInteger(136, 520, 
+                                decode_freq(
+                                    self._memobj.freq_ranges.uhf_tx_start)))
+        freqranges.append(rs)
+        rs = RadioSetting("uhf_tx_stop", "uhf tx stop",
+                          RadioSettingValueInteger(136, 520, 
+                                decode_freq(
+                                    self._memobj.freq_ranges.uhf_tx_stop)))
+        freqranges.append(rs)
+        
+        # tell the decoded ranges to UI
+        self.valid_freq = [
+            ( decode_freq(self._memobj.freq_ranges.vhf_rx_start) * 1000000, 
+             (decode_freq(self._memobj.freq_ranges.vhf_rx_stop)+1) * 1000000)]
+
+        return top
+
+    @classmethod
+    def match_model(cls, filedata, filename):
+        if len(filedata) == 8192 and \
+                filedata[0x60:0x64] != "2009" and \
+                filedata[0x1f77:0x1f7d] == "\xff\xff\xff\xff\xff\xff" and \
+                filedata[0x0d70:0x0d80] != "\xff\xff\xff\xff\xff\xff\xff\xff" \
+                                           "\xff\xff\xff\xff\xff\xff\xff\xff": 
+            return True
+        return False
+
diff --git a/chirp/wouxun_common.py b/chirp/wouxun_common.py
new file mode 100644
index 0000000..3c78b3a
--- /dev/null
+++ b/chirp/wouxun_common.py
@@ -0,0 +1,79 @@
+#
+# Copyright 2012 Filippi Marco <iz3gme.marco at gmail.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/>.
+
+"""vcommon function for wouxun (or similar) radios"""
+
+import struct
+import os
+from chirp import util, chirp_common, memmap
+
+def wipe_memory(_mem, byte):
+    """Cleanup a memory"""
+    _mem.set_raw(byte * (_mem.size() / 8))
+    
+def do_download(radio, start, end, blocksize):
+    """Initiate a download of @radio between @start and @end"""
+    image = ""
+    for i in range(start, end, blocksize):
+        cmd = struct.pack(">cHb", "R", i, blocksize)
+        if os.getenv("CHIRP_DEBUG"):
+            print util.hexprint(cmd)
+        radio.pipe.write(cmd)
+        length = len(cmd) + blocksize
+        resp = radio.pipe.read(length)
+        if len(resp) != (len(cmd) + blocksize):
+            print util.hexprint(resp)
+            raise Exception("Failed to read full block (%i!=%i)" % \
+                                (len(resp),
+                                 len(cmd) + blocksize))
+        
+        radio.pipe.write("\x06")
+        radio.pipe.read(1)
+        image += resp[4:]
+
+        if radio.status_fn:
+            status = chirp_common.Status()           
+            status.cur = i
+            status.max = end
+            status.msg = "Cloning from radio"
+            radio.status_fn(status)
+    
+    return memmap.MemoryMap(image)
+
+def do_upload(radio, start, end, blocksize):
+    """Initiate an upload of @radio between @start and @end"""
+    ptr = start
+    for i in range(start, end, blocksize):
+        cmd = struct.pack(">cHb", "W", i, blocksize)
+        chunk = radio.get_mmap()[ptr:ptr+blocksize]
+        ptr += blocksize
+        radio.pipe.write(cmd + chunk)
+        if os.getenv("CHIRP_DEBUG"):
+            print util.hexprint(cmd + chunk)
+
+        ack = radio.pipe.read(1)
+        if not ack == "\x06":
+            raise Exception("Radio did not ack block %i" % ptr)
+        #radio.pipe.write(ack)
+
+        if radio.status_fn:
+            status = chirp_common.Status()
+            status.cur = i
+            status.max = end
+            status.msg = "Cloning to radio"
+            radio.status_fn(status)
+
+
diff --git a/chirp/xml_ll.py b/chirp/xml_ll.py
new file mode 100644
index 0000000..28ec333
--- /dev/null
+++ b/chirp/xml_ll.py
@@ -0,0 +1,250 @@
+# 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 re
+
+from chirp import chirp_common, errors
+
+def get_memory(doc, number):
+    """Extract a Memory object from @doc"""
+    ctx = doc.xpathNewContext()
+
+    base = "//radio/memories/memory[@location=%i]" % number
+
+    fields = ctx.xpathEval(base)
+    if len(fields) > 1:
+        raise errors.RadioError("%i memories claiming to be %i" % (len(fields),
+                                                                   number))
+    elif len(fields) == 0:
+        raise errors.InvalidMemoryLocation("%i does not exist" % number)
+
+    memnode = fields[0]
+
+    def _get(ext):
+        path = base + ext
+        result = ctx.xpathEval(path)
+        if result:
+            return result[0].getContent()
+        else:
+            return ""
+
+    if _get("/mode/text()") == "DV":
+        mem = chirp_common.DVMemory()
+        mem.dv_urcall = _get("/dv/urcall/text()")
+        mem.dv_rpt1call = _get("/dv/rpt1call/text()")
+        mem.dv_rpt2call = _get("/dv/rpt2call/text()")
+        try:
+            mem.dv_code = _get("/dv/digitalCode/text()")
+        except ValueError:
+            mem.dv_code = 0
+    else:
+        mem = chirp_common.Memory()
+
+    mem.number = int(memnode.prop("location"))
+    mem.name = _get("/longName/text()")
+    mem.freq = chirp_common.parse_freq(_get("/frequency/text()"))
+    mem.rtone = float(_get("/squelch[@id='rtone']/tone/text()"))
+    mem.ctone = float(_get("/squelch[@id='ctone']/tone/text()"))
+    mem.dtcs = int(_get("/squelch[@id='dtcs']/code/text()"), 10)
+    mem.dtcs_polarity = _get("/squelch[@id='dtcs']/polarity/text()")
+    
+    try:
+        sql = _get("/squelchSetting/text()")
+        if sql == "rtone":
+            mem.tmode = "Tone"
+        elif sql == "ctone":
+            mem.tmode = "TSQL"
+        elif sql == "dtcs":
+            mem.tmode = "DTCS"
+        else:
+            mem.tmode = ""
+    except IndexError:
+        mem.tmode = ""
+
+    dmap = {"positive" : "+", "negative" : "-", "none" : ""}
+    dupx = _get("/duplex/text()")
+    mem.duplex = dmap.get(dupx, "")
+
+    mem.offset = chirp_common.parse_freq(_get("/offset/text()"))
+    mem.mode = _get("/mode/text()")
+    mem.tuning_step = float(_get("/tuningStep/text()"))
+
+    skip = _get("/skip/text()")
+    if skip == "none":
+        mem.skip = ""
+    else:
+        mem.skip = skip
+
+    #FIXME: bank support in .chirp files needs to be re-written
+    #bank_id = _get("/bank/@bankId")
+    #if bank_id:
+    #    mem.bank = int(bank_id)
+    #    bank_index = _get("/bank/@bankIndex")
+    #    if bank_index:
+    #        mem.bank_index = int(bank_index)
+
+    return mem
+
+def set_memory(doc, mem):
+    """Set @mem in @doc"""
+    ctx = doc.xpathNewContext()
+
+    base = "//radio/memories/memory[@location=%i]" % mem.number
+
+    fields = ctx.xpathEval(base)
+    if len(fields) > 1:
+        raise errors.RadioError("%i memories claiming to be %i" % (len(fields),
+                                                                   mem.number))
+    elif len(fields) == 1:
+        fields[0].unlinkNode()
+
+    radio = ctx.xpathEval("//radio/memories")[0]
+    memnode = radio.newChild(None, "memory", None)
+    memnode.newProp("location", "%i" % mem.number)
+
+    sname_filter = "[^A-Z0-9/ >-]"
+    sname = memnode.newChild(None, "shortName", None)
+    sname.addContent(re.sub(sname_filter, "", mem.name.upper()[:6]))
+
+    lname_filter = "[^.A-Za-z0-9/ >-]"
+    lname = memnode.newChild(None, "longName", None)
+    lname.addContent(re.sub(lname_filter, "", mem.name[:16]))
+    
+    freq = memnode.newChild(None, "frequency", None)
+    freq.newProp("units", "MHz")
+    freq.addContent(chirp_common.format_freq(mem.freq))
+    
+    rtone = memnode.newChild(None, "squelch", None)
+    rtone.newProp("id", "rtone")
+    rtone.newProp("type", "repeater")
+    tone = rtone.newChild(None, "tone", None)
+    tone.addContent("%.1f" % mem.rtone)
+
+    ctone = memnode.newChild(None, "squelch", None)
+    ctone.newProp("id", "ctone")
+    ctone.newProp("type", "ctcss")
+    tone = ctone.newChild(None, "tone", None)
+    tone.addContent("%.1f" % mem.ctone)
+
+    dtcs = memnode.newChild(None, "squelch", None)
+    dtcs.newProp("id", "dtcs")
+    dtcs.newProp("type", "dtcs")
+    code = dtcs.newChild(None, "code", None)
+    code.addContent("%03i" % mem.dtcs)
+    polr = dtcs.newChild(None, "polarity", None)
+    polr.addContent(mem.dtcs_polarity)
+
+    sset = memnode.newChild(None, "squelchSetting", None)
+    if mem.tmode == "Tone":
+        sset.addContent("rtone")
+    elif mem.tmode == "TSQL":
+        sset.addContent("ctone")
+    elif mem.tmode == "DTCS":
+        sset.addContent("dtcs")
+
+    dmap = {"+" : "positive", "-" : "negative", "" : "none"}
+    dupx = memnode.newChild(None, "duplex", None)
+    dupx.addContent(dmap[mem.duplex])
+
+    oset = memnode.newChild(None, "offset", None)
+    oset.newProp("units", "MHz")
+    oset.addContent(chirp_common.format_freq(mem.offset))
+
+    mode = memnode.newChild(None, "mode", None)
+    mode.addContent(mem.mode)
+
+    step = memnode.newChild(None, "tuningStep", None)
+    step.newProp("units", "kHz")
+    step.addContent("%.5f" % mem.tuning_step)
+    
+    if mem.skip:
+        skip = memnode.newChild(None, "skip", None)
+        skip.addContent(mem.skip)
+
+    #FIXME: .chirp bank support needs to be redone
+    #if mem.bank is not None:
+    #    bank = memnode.newChild(None, "bank", None)
+    #    bank.newProp("bankId", str(int(mem.bank)))
+    #    if mem.bank_index >= 0:
+    #        bank.newProp("bankIndex", str(int(mem.bank_index)))
+
+    if isinstance(mem, chirp_common.DVMemory):
+        dv = memnode.newChild(None, "dv", None)
+
+        ur = dv.newChild(None, "urcall", None)
+        ur.addContent(mem.dv_urcall)
+
+        r1 = dv.newChild(None, "rpt1call", None)
+        if mem.dv_rpt1call and mem.dv_rpt1call != "*NOTUSE*":
+            r1.addContent(mem.dv_rpt1call)
+
+        r2 = dv.newChild(None, "rpt2call", None)
+        if mem.dv_rpt2call and mem.dv_rpt2call != "*NOTUSE*":
+            r2.addContent(mem.dv_rpt2call)
+
+        dc = dv.newChild(None, "digitalCode", None)
+        dc.addContent(str(mem.dv_code))
+
+def del_memory(doc, number):
+    """Remove memory @number from @doc"""
+    path = "//radio/memories/memory[@location=%i]" % number
+    ctx = doc.xpathNewContext()
+    fields = ctx.xpathEval(path)
+
+    for field in fields:
+        field.unlinkNode()
+    
+def _get_bank(node):
+    bank = chirp_common.Bank(node.prop("label"))
+    ident = int(node.prop("id"))
+
+    return ident, bank
+
+def get_banks(doc):
+    """Return a list of banks from @doc"""
+    path = "//radio/banks/bank"
+    ctx = doc.xpathNewContext()
+    fields = ctx.xpathEval(path)
+
+    banks = []
+    for field in fields:
+        banks.append(_get_bank(field))
+
+    def _cmp(itema, itemb):
+        return itema[0] - itemb[0]
+
+    banks.sort(cmp=_cmp)
+
+    return [x[1] for x in banks]
+
+def set_banks(doc, banklist):
+    """Set the list of banks in @doc"""
+    path = "//radio/banks/bank"
+    ctx = doc.xpathNewContext()
+    fields = ctx.xpathEval(path)
+
+    for field in fields:
+        field.unlinkNode()
+
+    path = "//radio/banks"
+    ctx = doc.xpathNewContext()
+    banks = ctx.xpathEval(path)[0]
+
+    i = 0
+    for bank in banklist:
+        banknode = banks.newChild(None, "bank", None)
+        banknode.newProp("id", "%i" % i)
+        banknode.newProp("label", "%s" % bank)
+        i += 1
diff --git a/chirp/yaesu_clone.py b/chirp/yaesu_clone.py
new file mode 100644
index 0000000..d94b796
--- /dev/null
+++ b/chirp/yaesu_clone.py
@@ -0,0 +1,215 @@
+# Copyright 2010 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/>.
+
+CMD_ACK = 0x06
+
+from chirp import chirp_common, util, memmap, errors
+import time, os
+
+def _safe_read(pipe, count):
+    buf = ""
+    first = True
+    for _i in range(0, 60):
+        buf += pipe.read(count - len(buf))
+        #print "safe_read: %i/%i\n" % (len(buf), count)
+        if buf:
+            if first and buf[0] == chr(CMD_ACK):
+                #print "Chewed an ack"
+                buf = buf[1:] # Chew an echo'd ack if using a 2-pin cable
+            first = False
+        if len(buf) == count:
+            break
+    print util.hexprint(buf)
+    return buf
+
+def _chunk_read(pipe, count, status_fn):
+    block = 32
+    data = ""
+    for _i in range(0, count, block):
+        data += pipe.read(block)
+        if data:
+            if data[0] == chr(CMD_ACK):
+                data = data[1:] # Chew an echo'd ack if using a 2-pin cable
+                #print "Chewed an ack"
+        status = chirp_common.Status()
+        status.msg = "Cloning from radio"
+        status.max = count
+        status.cur = len(data)
+        status_fn(status)
+        if os.getenv("CHIRP_DEBUG"):
+            print "Read %i/%i" % (len(data), count)
+    return data        
+
+def __clone_in(radio):
+    pipe = radio.pipe
+
+    start = time.time()
+
+    data = ""
+    blocks = 0
+    for block in radio._block_lengths:
+        blocks += 1
+        if blocks == len(radio._block_lengths):
+            chunk = _chunk_read(pipe, block, radio.status_fn)
+        else:
+            chunk = _safe_read(pipe, block)
+            pipe.write(chr(CMD_ACK))
+        if not chunk:
+            raise errors.RadioError("No response from radio")
+        data += chunk
+
+    if len(data) != radio.get_memsize():
+        raise errors.RadioError("Received incomplete image from radio")
+
+    print "Clone completed in %i seconds" % (time.time() - start)
+
+    return memmap.MemoryMap(data)
+
+def _clone_in(radio):
+    try:
+        return __clone_in(radio)
+    except Exception, e:
+        raise errors.RadioError("Failed to communicate with the radio: %s" % e)
+
+def _chunk_write(pipe, data, status_fn, block):
+    delay = 0.03
+    count = 0
+    for i in range(0, len(data), block):
+        chunk = data[i:i+block]
+        pipe.write(chunk)
+        count += len(chunk)
+        #print "Count is %i" % count
+        time.sleep(delay)
+
+        status = chirp_common.Status()
+        status.msg = "Cloning to radio"
+        status.max = len(data)
+        status.cur = count
+        status_fn(status)
+        
+def __clone_out(radio):
+    pipe = radio.pipe
+    block_lengths = radio._block_lengths
+    total_written = 0
+
+    def _status():
+        status = chirp_common.Status()
+        status.msg = "Cloning to radio"
+        status.max = block_lengths[0] + block_lengths[1] + block_lengths[2]
+        status.cur = total_written
+        radio.status_fn(status)
+
+    start = time.time()
+
+    blocks = 0
+    pos = 0
+    for block in radio._block_lengths:
+        blocks += 1
+        if blocks != len(radio._block_lengths):
+            #print "Sending %i-%i" % (pos, pos+block)
+            pipe.write(radio.get_mmap()[pos:pos+block])
+            buf = pipe.read(1)
+            if buf and buf[0] != chr(CMD_ACK):
+                buf = pipe.read(block)
+            if not buf or buf[-1] != chr(CMD_ACK):
+                raise Exception("Radio did not ack block %i" % blocks)
+        else:
+            _chunk_write(pipe, radio.get_mmap()[pos:],
+                         radio.status_fn, radio._block_size)
+        pos += block
+
+    pipe.read(pos) # Chew the echo if using a 2-pin cable
+
+    print "Clone completed in %i seconds" % (time.time() - start)
+
+def _clone_out(radio):
+    try:
+        return __clone_out(radio)
+    except Exception, e:
+        raise errors.RadioError("Failed to communicate with the radio: %s" % e)
+
+class YaesuChecksum:
+    """A Yaesu Checksum Object"""
+    def __init__(self, start, stop, address=None):
+        self._start = start
+        self._stop = stop
+        if address:
+            self._address = address
+        else:
+            self._address = stop + 1
+
+    def get_existing(self, mmap):
+        """Return the existing checksum in mmap"""
+        return ord(mmap[self._address])
+
+    def get_calculated(self, mmap):
+        """Return the calculated value of the checksum"""
+        cs = 0
+        for i in range(self._start, self._stop+1):
+            cs += ord(mmap[i])
+        return cs % 256
+
+    def update(self, mmap):
+        """Update the checksum with the data in @mmap"""
+        mmap[self._address] = self.get_calculated(mmap)
+
+    def __str__(self):
+        return "%04X-%04X (@%04X)" % (self._start,
+                                      self._stop,
+                                      self._address)
+
+class YaesuCloneModeRadio(chirp_common.CloneModeRadio):
+    """Base class for all Yaesu clone-mode radios"""
+    _block_lengths = [8, 65536]
+    _block_size = 8
+
+    VENDOR = "Yaesu"
+    _model = "ABCDE"
+
+    def _checksums(self):
+        """Return a list of checksum objects that need to be calculated"""
+        return []
+
+    def update_checksums(self):
+        """Update the radio's checksums from the current memory map"""
+        for checksum in self._checksums():
+            checksum.update(self._mmap)
+
+    def check_checksums(self):
+        """Validate the checksums stored in the memory map"""
+        for checksum in self._checksums():
+            if checksum.get_existing(self._mmap) != \
+                    checksum.get_calculated(self._mmap):
+                raise errors.RadioError("Checksum Failed [%s]" % checksum)
+            print "Checksum %s: OK" % checksum
+
+    def sync_in(self):
+        self._mmap = _clone_in(self)
+        self.check_checksums()
+        self.process_mmap()
+
+    def sync_out(self):
+        self.update_checksums()
+        _clone_out(self)
+
+    @classmethod
+    def match_model(cls, filedata, filename):
+        return filedata[:5] == cls._model and len(filedata) == cls._memsize
+
+    def _wipe_memory_banks(self, mem):
+        """Remove @mem from all the banks it is currently in"""
+        bm = self.get_bank_model()
+        for bank in bm.get_memory_banks(mem):
+            bm.remove_memory_from_bank(mem, bank)
diff --git a/chirp_banks.xsd b/chirp_banks.xsd
new file mode 100644
index 0000000..14e577b
--- /dev/null
+++ b/chirp_banks.xsd
@@ -0,0 +1,8 @@
+<xsd:schema xmlns:xsd="http://www.w3.org/2001/XMLSchema">
+
+  <xsd:complexType name="bankType">
+    <xsd:attribute name="id" type="xsd:nonNegativeInteger" use="required"/>
+    <xsd:attribute name="label" type="xsd:string" use="required"/>
+  </xsd:complexType>
+
+</xsd:schema>
diff --git a/chirp_memory.xsd b/chirp_memory.xsd
new file mode 100644
index 0000000..ba4bc8a
--- /dev/null
+++ b/chirp_memory.xsd
@@ -0,0 +1,121 @@
+<xsd:schema xmlns:xsd="http://www.w3.org/2001/XMLSchema">
+
+  <xsd:complexType name="memoryType">
+    <xsd:sequence>
+      <xsd:element name="shortName" type="shortNameType"/>
+      <xsd:element name="longName" type="longNameType" minOccurs="0"/>
+      <xsd:element name="frequency" type="frequencyType"/>
+      <xsd:element name="squelch" type="squelchType"
+		   minOccurs="0" maxOccurs="3"/>
+      <xsd:element name="squelchSetting" type="xsd:string" minOccurs="0"/>
+      <xsd:element name="duplex" type="duplexType"/>
+      <xsd:element name="offset" type="frequencyType"/>
+      <xsd:element name="mode" type="modeType"/>
+      <xsd:element name="tuningStep" type="frequencyType"/>
+      <xsd:element name="skip" type="skipType" minOccurs="0" maxOccurs="1"/>
+      <xsd:element name="bank" type="bankInfoType" minOccurs="0" maxOccurs="1"/>
+      <xsd:element name="dv" type="dvType" minOccurs="0"/>
+    </xsd:sequence>
+    <xsd:attribute name="location" type="xsd:nonNegativeInteger"/>
+  </xsd:complexType>
+
+  <xsd:simpleType name="shortNameType">
+    <xsd:restriction base="xsd:string">
+      <xsd:pattern value="[A-Z0-9/ >-]{0,6}"/>
+    </xsd:restriction>
+  </xsd:simpleType>
+
+  <xsd:complexType name="frequencyType">
+    <xsd:simpleContent>
+      <xsd:extension base="xsd:decimal">
+	<xsd:attribute name="units" type="freqUnitsType" use="required"/>
+      </xsd:extension>
+    </xsd:simpleContent>
+  </xsd:complexType>
+  
+  <xsd:simpleType name="freqUnitsType">
+    <xsd:restriction base="xsd:string">
+      <xsd:enumeration value="Hz"/>
+      <xsd:enumeration value="kHz"/>
+      <xsd:enumeration value="MHz"/>
+      <xsd:enumeration value="GHz"/>
+    </xsd:restriction>
+  </xsd:simpleType>
+
+  <xsd:simpleType name="longNameType">
+    <xsd:restriction base="xsd:string">
+      <xsd:pattern value="[.A-Za-z0-9/ >-]{0,16}"/>
+    </xsd:restriction>
+  </xsd:simpleType>
+
+
+  <xsd:complexType name="squelchType">
+    <xsd:sequence>
+      <xsd:element name="tone" type="xsd:decimal" minOccurs="0"/>
+      <xsd:element name="code" type="xsd:positiveInteger" minOccurs="0"/>
+      <xsd:element name="polarity" type="dtcsPolarityType" minOccurs="0"/>
+    </xsd:sequence>
+    <xsd:attribute name="id"/>
+    <xsd:attribute name="type"/>
+  </xsd:complexType>
+
+  <xsd:simpleType name="dtcsPolarityType">
+    <xsd:restriction base="xsd:string">
+      <xsd:pattern value="[RN]{2}"/>
+    </xsd:restriction>
+  </xsd:simpleType>
+
+  <xsd:simpleType name="duplexType">
+    <xsd:restriction base="xsd:string">
+      <xsd:enumeration value="positive"/>
+      <xsd:enumeration value="negative"/>
+      <xsd:enumeration value="none"/>
+    </xsd:restriction>
+  </xsd:simpleType>
+
+  <xsd:simpleType name="modeType">
+    <xsd:restriction base="xsd:string">
+      <xsd:enumeration value="FM"/>
+      <xsd:enumeration value="NFM"/>
+      <xsd:enumeration value="WFM"/>
+      <xsd:enumeration value="AM"/>
+      <xsd:enumeration value="NAM"/>
+      <xsd:enumeration value="DV"/>
+    </xsd:restriction>
+  </xsd:simpleType>
+
+  <xsd:complexType name="dvType">
+    <xsd:sequence>
+      <xsd:element name="urcall" type="callsignType"/>
+      <xsd:element name="rpt1call" type="callsignType"/>
+      <xsd:element name="rpt2call" type="callsignType"/>
+      <xsd:element name="digitalCode" type="digitalCodeType" minOccurs="0"/>
+    </xsd:sequence>
+  </xsd:complexType>
+
+  <xsd:simpleType name="callsignType">
+    <xsd:restriction base="xsd:string">
+      <xsd:pattern value="[A-Z0-9/ ]*"/>
+    </xsd:restriction>
+  </xsd:simpleType>
+
+  <xsd:simpleType name="digitalCodeType">
+    <xsd:restriction base="xsd:integer">
+      <xsd:minInclusive value="0"/>
+    </xsd:restriction>
+  </xsd:simpleType>
+
+  <xsd:simpleType name="skipType">
+    <xsd:restriction base="xsd:string">
+      <xsd:enumeration value="S"/>
+      <xsd:enumeration value="P"/>
+      <xsd:enumeration value=""/>
+    </xsd:restriction>
+  </xsd:simpleType>
+
+  <xsd:complexType name="bankInfoType">
+    <xsd:attribute name="bankId" type="xsd:nonNegativeInteger" use="required"/>
+    <xsd:attribute name="bankIndex" type="xsd:nonNegativeInteger"/>
+  </xsd:complexType>
+  
+</xsd:schema>
diff --git a/chirpui/__init__.py b/chirpui/__init__.py
new file mode 100644
index 0000000..a2a6f35
--- /dev/null
+++ b/chirpui/__init__.py
@@ -0,0 +1,14 @@
+# 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/>.
diff --git a/chirpui/bankedit.py b/chirpui/bankedit.py
new file mode 100644
index 0000000..23c05da
--- /dev/null
+++ b/chirpui/bankedit.py
@@ -0,0 +1,392 @@
+# 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 gtk
+import gobject
+import time
+
+from gobject import TYPE_INT, TYPE_STRING, TYPE_BOOLEAN
+
+from chirp import chirp_common
+from chirpui import common, miscwidgets
+
+class BankNamesJob(common.RadioJob):
+    def __init__(self, bm, editor, cb):
+        common.RadioJob.__init__(self, cb, None)
+        self.__bm = bm
+        self.__editor = editor
+
+    def execute(self, radio):
+        self.__editor.banks = []
+
+        banks = self.__bm.get_banks()
+        for bank in banks:
+            self.__editor.banks.append((bank, bank.get_name()))
+
+        gobject.idle_add(self.cb, *self.cb_args)
+
+class BankNameEditor(common.Editor):
+    def refresh(self):
+        def got_banks():
+            self._keys = []
+            for bank, name in self.banks:
+                self._keys.append(bank.get_index())
+                self.listw.set_item(bank.get_index(),
+                                    bank.get_index(),
+                                    name)
+
+            self.listw.connect("item-set", self.bank_changed)
+
+        job = BankNamesJob(self._bm, self, got_banks)
+        job.set_desc(_("Retrieving bank information"))
+        self.rthread.submit(job)
+
+    def get_bank_list(self):
+        banks = []
+        keys = self.listw.get_keys()
+        for key in keys:
+            banks.append(self.listw.get_item(key)[2])
+
+        return banks
+    
+    def bank_changed(self, listw, key):
+        def cb(*args):
+            self.emit("changed")
+
+        name = self.listw.get_item(key)[2]
+        bank, oldname = self.banks[self._keys.index(key)]
+
+        def trigger_changed(*args):
+            self.emit("changed")
+
+        job = common.RadioJob(trigger_changed, "set_name", name)
+        job.set_target(bank)
+        job.set_desc(_("Setting name on bank"))
+        self.rthread.submit(job)
+
+        return True
+
+    def __init__(self, rthread):
+        common.Editor.__init__(self)
+        self.rthread = rthread
+        self._bm = rthread.radio.get_bank_model()
+
+        types = [(gobject.TYPE_STRING, "key"),
+                 (gobject.TYPE_STRING, _("Bank")),
+                 (gobject.TYPE_STRING, _("Name"))]
+
+        self.listw = miscwidgets.KeyedListWidget(types)
+        self.listw.set_editable(1, True)
+        self.listw.set_sort_column(0, 1)
+        self.listw.set_sort_column(1, -1)
+        self.listw.show()
+
+        self.banks = []
+
+        sw = gtk.ScrolledWindow()
+        sw.set_policy(gtk.POLICY_AUTOMATIC, gtk.POLICY_AUTOMATIC)
+        sw.add_with_viewport(self.listw)
+
+        self.root = sw
+        self._loaded = False
+
+    def focus(self):
+        if self._loaded:
+            return
+
+        self.refresh()
+        self._loaded = True
+
+class MemoryBanksJob(common.RadioJob):
+    def __init__(self, bm, cb, number):
+        common.RadioJob.__init__(self, cb, None)
+        self.__bm = bm
+        self.__number = number
+
+    def execute(self, radio):
+        mem = radio.get_memory(self.__number)
+        if mem.empty:
+            banks = []
+            indexes = []
+        else:
+            banks = self.__bm.get_memory_banks(mem)
+            indexes = []
+            if isinstance(self.__bm, chirp_common.BankIndexInterface):
+                for bank in banks:
+                    indexes.append(self.__bm.get_memory_index(mem, bank))
+        self.cb(mem, banks, indexes, *self.cb_args)
+            
+class BankMembershipEditor(common.Editor):
+    def _number_to_path(self, number):
+        return (number - self._rf.memory_bounds[0],)
+
+    def _get_next_bank_index(self, bank):
+        # NB: Only works for one-to-one bank models right now!
+        iter = self._store.get_iter_first()
+        indexes = []
+        ncols = len(self._cols) + len(self.banks)
+        while iter:
+            vals = self._store.get(iter, *tuple([n for n in range(0, ncols)]))
+            loc = vals[self.C_LOC]
+            index = vals[self.C_INDEX]
+            banks = vals[self.C_BANKS:]
+            if True in banks and banks.index(True) == bank:
+                indexes.append(index)
+            iter = self._store.iter_next(iter)
+
+        index_bounds = self._bm.get_index_bounds()
+        num_indexes = index_bounds[1] - index_bounds[0]
+        indexes.sort()
+        for i in range(0, num_indexes):
+            if i not in indexes:
+                return i + index_bounds[0] # In case not zero-origin index
+
+        return 0 # If the bank is full, just wrap around!
+
+    def _toggled_cb(self, rend, path, colnum):
+        try:
+            if not rend.get_sensitive():
+                return
+        except AttributeError:
+            # support PyGTK < 2.22
+            iter = self._store.get_iter(path)
+            if not self._store.get(iter, self.C_FILLED)[0]:
+                return
+
+        # The bank index is the column number, minus the 3 label columns
+        bank, name = self.banks[colnum - len(self._cols)]
+        loc, = self._store.get(self._store.get_iter(path), self.C_LOC)
+
+        if rend.get_active():
+            # Changing from True to False
+            fn = "remove_memory_from_bank"
+            index = None
+        else:
+            # Changing from False to True
+            fn = "add_memory_to_bank"
+            if self._rf.has_bank_index:
+                index = self._get_next_bank_index(colnum - len(self._cols))
+            else:
+                index = None
+
+        def do_refresh_memory(*args):
+            # Step 2: Update our notion of the memory's bank information
+            self.refresh_memory(loc)
+
+        def do_bank_index(result, memory):
+            if isinstance(result, Exception):
+                common.show_error("Failed to add {mem} to bank: {err}"
+                                  .format(mem=memory.number,
+                                          err=str(result)),
+                                  parent=self.editorset.parent_window)
+                return
+            self.emit("changed")
+            # Step 3: Set the memory's bank index (maybe)
+            if not self._rf.has_bank_index or index is None:
+                return do_refresh_memory()
+
+            job = common.RadioJob(do_refresh_memory,
+                                  "set_memory_index", memory, bank, index)
+            job.set_target(self._bm)
+            job.set_desc(_("Updating bank index "
+                           "for memory {num}").format(num=memory.number))
+            self.rthread.submit(job)
+
+        def do_bank_adjustment(memory):
+            # Step 1: Do the bank add/remove
+            job = common.RadioJob(do_bank_index, fn, memory, bank)
+            job.set_target(self._bm)
+            job.set_cb_args(memory)
+            job.set_desc(_("Updating bank information "
+                           "for memory {num}").format(num=memory.number))
+            self.rthread.submit(job)
+
+        # Step 0: Fetch the memory
+        job = common.RadioJob(do_bank_adjustment, "get_memory", loc)
+        job.set_desc(_("Getting memory {num}").format(num=loc))
+        self.rthread.submit(job)
+
+    def _index_edited_cb(self, rend, path, new):
+        loc, = self._store.get(self._store.get_iter(path), self.C_LOC)
+        
+        def refresh_memory(*args):
+            self.refresh_memory(loc)
+
+        def set_index(banks, memory):
+            self.emit("changed")
+            # Step 2: Set the index
+            job = common.RadioJob(refresh_memory, "set_memory_index",
+                                  memory, banks[0], int(new))
+            job.set_target(self._bm)
+            job.set_desc(_("Setting index "
+                           "for memory {num}").format(num=memory.number))
+            self.rthread.submit(job)
+
+        def get_bank(memory):
+            # Step 1: Get the first/only bank
+            job = common.RadioJob(set_index, "get_memory_banks", memory)
+            job.set_cb_args(memory)
+            job.set_target(self._bm)
+            job.set_desc(_("Getting bank for "
+                           "memory {num}").format(num=memory.number))
+            self.rthread.submit(job)
+
+        # Step 0: Get the memory
+        job = common.RadioJob(get_bank, "get_memory", loc)
+        job.set_desc(_("Getting memory {num}").format(num=loc))
+        self.rthread.submit(job)
+            
+    def __init__(self, rthread, editorset):
+        common.Editor.__init__(self)
+        self.rthread = rthread
+        self.editorset = editorset
+        self._rf = rthread.radio.get_features()
+        self._bm = rthread.radio.get_bank_model()
+
+        self._view_cols = [
+            (_("Loc"),       TYPE_INT,     gtk.CellRendererText, ),
+            (_("Frequency"), TYPE_STRING,  gtk.CellRendererText, ),
+            (_("Name"),      TYPE_STRING,  gtk.CellRendererText, ),
+            (_("Index"),     TYPE_INT,     gtk.CellRendererText, ),
+            ]
+
+        self._cols = [
+            ("_filled",      TYPE_BOOLEAN, None,                 ),
+            ] + self._view_cols
+
+        self.C_FILLED = 0
+        self.C_LOC    = 1
+        self.C_FREQ   = 2
+        self.C_NAME   = 3
+        self.C_INDEX  = 4
+        self.C_BANKS  = 5 # and beyond
+        
+        cols = list(self._cols)
+
+        self._index_cache = []
+
+        for i in range(0, self._bm.get_num_banks()):
+            label = "Bank %i" % (i+1)
+            cols.append((label, TYPE_BOOLEAN, gtk.CellRendererToggle))
+
+        self._store = gtk.ListStore(*tuple([y for x,y,z in cols]))
+        self._view = gtk.TreeView(self._store)
+
+        colnum = 0
+        for label, dtype, rtype in cols:
+            if not rtype:
+                colnum += 1
+                continue
+            rend = rtype()
+            if dtype == TYPE_BOOLEAN:
+                rend.set_property("activatable", True)
+                rend.connect("toggled", self._toggled_cb, colnum)
+                col = gtk.TreeViewColumn(label, rend, active=colnum,
+                                         sensitive=self.C_FILLED)
+            else:
+                col = gtk.TreeViewColumn(label, rend, text=colnum,
+                                         sensitive=self.C_FILLED)
+
+            self._view.append_column(col)
+            col.set_resizable(True)
+            if colnum == self.C_NAME:
+                col.set_visible(self._rf.has_name)
+            elif colnum == self.C_INDEX:
+                rend.set_property("editable", True)
+                rend.connect("edited", self._index_edited_cb)
+                col.set_visible(self._rf.has_bank_index)
+            colnum += 1
+
+        # A non-rendered column to absorb extra space in the row
+        self._view.append_column(gtk.TreeViewColumn())
+
+        sw = gtk.ScrolledWindow()
+        sw.set_policy(gtk.POLICY_AUTOMATIC, gtk.POLICY_AUTOMATIC)
+        sw.add(self._view)
+        self._view.show()
+
+        for i in range(*self._rf.memory_bounds):
+            iter = self._store.append()
+            self._store.set(iter,
+                            self.C_FILLED, False,
+                            self.C_LOC, i,
+                            self.C_FREQ, 0,
+                            self.C_NAME, "",
+                            self.C_INDEX, 0)
+
+        self.root = sw
+        self._loaded = False
+
+    def refresh_memory(self, number):
+        def got_mem(memory, banks, indexes):
+            iter = self._store.get_iter(self._number_to_path(memory.number))
+            row = [self.C_FILLED, not memory.empty,
+                   self.C_LOC, memory.number,
+                   self.C_FREQ, chirp_common.format_freq(memory.freq),
+                   self.C_NAME, memory.name,
+                   # Hack for only one index right now
+                   self.C_INDEX, indexes and indexes[0] or 0,
+                   ]
+            for i in range(0, len(self.banks)):
+                row.append(i + len(self._cols))
+                row.append(self.banks[i][0] in banks)
+                
+            self._store.set(iter, *tuple(row))
+            if memory.number == self._rf.memory_bounds[1] - 1:
+                print "Got all bank info in %s" % (time.time() - self._start)
+
+        job = MemoryBanksJob(self._bm, got_mem, number)
+        job.set_desc(_("Getting bank information "
+                       "for memory {num}").format(num=number))
+        self.rthread.submit(job)
+
+    def refresh_all_memories(self):
+        for i in range(*self._rf.memory_bounds):
+            self.refresh_memory(i)
+
+    def refresh_banks(self, and_memories=False):
+        def got_banks():
+            for i in range(len(self._cols) - len(self._view_cols) - 1,
+                           len(self.banks)):
+                col = self._view.get_column(i + len(self._view_cols))
+                bank, name = self.banks[i]
+                if name:
+                    col.set_title(name)
+                else:
+                    col.set_title("(%s)" % i)
+            if and_memories:
+                self.refresh_all_memories()
+
+        job = BankNamesJob(self._bm, self, got_banks)
+        job.set_desc(_("Getting bank information"))
+        self.rthread.submit(job)
+
+    def focus(self):
+        common.Editor.focus(self)
+        if self._loaded:
+            return
+
+        self._start = time.time()
+        self.refresh_banks(True)
+
+        self._loaded = True
+
+    def memories_changed(self):
+        self._loaded = False
+        if self.is_focused():
+            self.refresh_all_memories()
+
+    def banks_changed(self):
+        self.refresh_banks()
diff --git a/chirpui/clone.py b/chirpui/clone.py
new file mode 100644
index 0000000..70db9ac
--- /dev/null
+++ b/chirpui/clone.py
@@ -0,0 +1,246 @@
+# 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 threading
+import os
+
+import gtk
+import gobject
+
+from chirp import platform, directory, detect, chirp_common
+from chirpui import miscwidgets, cloneprog, inputdialog, common, config
+
+AUTO_DETECT_STRING = "Auto Detect (Icom Only)"
+
+class CloneSettings:
+    def __init__(self):
+        self.port = None
+        self.radio_class = None
+
+    def __str__(self):
+        s = ""
+        if self.radio_class:
+            return _("{vendor} {model} on {port}").format(\
+                vendor=self.radio_class.VENDOR,
+                model=self.radio_class.MODEL,
+                port=self.port)
+
+class CloneSettingsDialog(gtk.Dialog):
+    def __make_field(self, label, widget):
+        l = gtk.Label(label)
+        self.__table.attach(l, 0, 1, self.__row, self.__row+1)
+        self.__table.attach(widget, 1, 2, self.__row, self.__row+1)
+        self.__row += 1
+
+        l.show()
+        widget.show()
+
+    def __make_port(self, port):
+        conf = config.get("state")
+
+        ports = platform.get_platform().list_serial_ports()
+        if not port:
+            if conf.get("last_port"):
+                port = conf.get("last_port")
+            elif ports:
+                port = ports[0]
+            if not port in ports:
+                ports.append(port)
+
+        return miscwidgets.make_choice(sorted(ports), True, port)
+
+    def __make_model(self):
+        return miscwidgets.make_choice([], False)
+
+    def __make_vendor(self, model):
+        vendors = {}
+        for rclass in sorted(directory.DRV_TO_RADIO.values()):
+            if not issubclass(rclass, chirp_common.CloneModeRadio) and \
+                    not issubclass(rclass, chirp_common.LiveRadio):
+                continue
+
+            if not vendors.has_key(rclass.VENDOR):
+                vendors[rclass.VENDOR] = []
+
+            vendors[rclass.VENDOR].append(rclass)
+
+        self.__vendors = vendors
+
+        conf = config.get("state")
+        if not conf.get("last_vendor"):
+            conf.set("last_vendor", sorted(vendors.keys())[0])
+
+        last_vendor = conf.get("last_vendor")
+        if last_vendor not in vendors.keys():
+            last_vendor = vendors.keys()[0]
+
+        v = miscwidgets.make_choice(sorted(vendors.keys()), False, last_vendor)
+
+        def _changed(box, vendors, model):
+            models = vendors[box.get_active_text()]
+
+            added_models = []
+
+            model.get_model().clear()
+            for rclass in sorted(models, key=lambda c: c.__name__):
+                if rclass.MODEL not in added_models:
+                    model.append_text(rclass.MODEL)
+                    added_models.append(rclass.MODEL)
+
+            if box.get_active_text() in detect.DETECT_FUNCTIONS:
+                model.insert_text(0, _("Detect"))
+                added_models.insert(0, _("Detect"))
+
+            model_names = [x.MODEL for x in models]
+            if conf.get("last_model") in model_names:
+                model.set_active(added_models.index(conf.get("last_model")))
+            else:
+                model.set_active(0)
+
+        v.connect("changed", _changed, vendors, model)
+        _changed(v, vendors, model)
+
+        return v
+
+    def __make_ui(self, settings):
+        self.__table = gtk.Table(3, 2)
+        self.__table.set_row_spacings(3)
+        self.__table.set_col_spacings(10)
+        self.__row = 0
+
+        self.__port = self.__make_port(settings and settings.port or None)
+        self.__modl = self.__make_model()
+        self.__vend = self.__make_vendor(self.__modl)
+
+        self.__make_field(_("Port"), self.__port)
+        self.__make_field(_("Vendor"), self.__vend)
+        self.__make_field(_("Model"), self.__modl)
+
+        if settings and settings.radio_class:
+            common.combo_select(self.__vend, settings.radio_class.VENDOR)
+            self.__modl.get_model().clear()
+            self.__modl.append_text(settings.radio_class.MODEL)
+            common.combo_select(self.__modl, settings.radio_class.MODEL)
+            self.__vend.set_sensitive(False)
+            self.__modl.set_sensitive(False)
+
+        self.__table.show()
+        self.vbox.pack_start(self.__table, 1, 1, 1)
+
+    def __init__(self, settings=None, parent=None, title=_("Radio")):
+        buttons = (gtk.STOCK_CANCEL, gtk.RESPONSE_CANCEL,
+                   gtk.STOCK_OK, gtk.RESPONSE_OK)
+        gtk.Dialog.__init__(self, title,
+                            parent=parent,
+                            flags=gtk.DIALOG_MODAL)
+        self.__make_ui(settings)
+        self.__cancel_button = self.add_button(gtk.STOCK_CANCEL,
+                                               gtk.RESPONSE_CANCEL)
+        self.__okay_button = self.add_button(gtk.STOCK_OK,
+                                             gtk.RESPONSE_OK)
+        self.__okay_button.grab_default()
+        self.__okay_button.grab_focus()
+
+    def run(self):
+        r = gtk.Dialog.run(self)
+        if r != gtk.RESPONSE_OK:
+            return None
+
+        vendor = self.__vend.get_active_text()
+        model = self.__modl.get_active_text()
+
+        cs = CloneSettings()
+        cs.port = self.__port.get_active_text()
+        if model == _("Detect"):
+            try:
+                cs.radio_class = detect.DETECT_FUNCTIONS[vendor](cs.port)
+                if not cs.radio_class:
+                    raise Exception(_("Unable to detect radio on {port}").format(port=cs.port))
+            except Exception, e:
+                d = inputdialog.ExceptionDialog(e)
+                d.run()
+                d.destroy()
+                return None
+        else:
+            for rclass in directory.DRV_TO_RADIO.values():
+                if rclass.MODEL == model:
+                    cs.radio_class = rclass
+                    break
+            if not cs.radio_class:
+                common.show_error(_("Internal error: Unable to upload to {model}").format(model=model))
+                print self.__vendors
+                return None
+
+        conf = config.get("state")
+        conf.set("last_port", cs.port)
+        conf.set("last_vendor", cs.radio_class.VENDOR)
+        conf.set("last_model", model)
+
+        return cs
+
+class CloneCancelledException(Exception):
+    pass
+
+class CloneThread(threading.Thread):
+    def __status(self, status):
+        gobject.idle_add(self.__progw.status, status)
+
+    def __init__(self, radio, direction, cb=None, parent=None):
+        threading.Thread.__init__(self)
+
+        self.__radio = radio
+        self.__out = direction == "out"
+        self.__cback = cb
+        self.__cancelled = False
+
+        self.__progw = cloneprog.CloneProg(parent=parent, cancel=self.cancel)
+
+    def cancel(self):
+        self.__radio.pipe.close()
+        self.__cancelled = True
+
+    def run(self):
+        print "Clone thread started"
+
+        gobject.idle_add(self.__progw.show)
+
+        self.__radio.status_fn = self.__status
+        
+        try:
+            if self.__out:
+                self.__radio.sync_out()
+            else:
+                self.__radio.sync_in()
+
+            emsg = None
+        except Exception, e:
+            common.log_exception()
+            print _("Clone failed: {error}").format(error=e)
+            emsg = e
+
+        gobject.idle_add(self.__progw.hide)
+
+        # NB: Compulsory close of the radio's serial connection
+        self.__radio.pipe.close()
+
+        print "Clone thread ended"
+
+        if self.__cback and not self.__cancelled:
+            gobject.idle_add(self.__cback, self.__radio, emsg)
+
+if __name__ == "__main__":
+    d = CloneSettingsDialog("/dev/ttyUSB0")
+    r = d.run()
+    print r
diff --git a/chirpui/cloneprog.py b/chirpui/cloneprog.py
new file mode 100644
index 0000000..7427bd2
--- /dev/null
+++ b/chirpui/cloneprog.py
@@ -0,0 +1,65 @@
+# 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 gtk
+
+class CloneProg(gtk.Window):
+    def __init__(self, **args):
+        if args.has_key("parent"):
+            parent = args["parent"]
+            del args["parent"]
+        else:
+            parent = None
+
+        if args.has_key("cancel"):
+            cancel = args["cancel"]
+            del args["cancel"]
+        else:
+            cancel = None
+
+        gtk.Window.__init__(self, **args)
+
+        self.set_transient_for(parent)
+        self.set_modal(True)
+        self.set_type_hint(gtk.gdk.WINDOW_TYPE_HINT_DIALOG)
+        self.set_position(gtk.WIN_POS_CENTER_ON_PARENT)	
+
+        vbox = gtk.VBox(False, 2)
+        vbox.show()
+        self.add(vbox)
+
+        self.set_title(_("Clone Progress"))
+        self.set_resizable(False)
+
+        self.infolabel = gtk.Label(_("Cloning"))
+        self.infolabel.show()
+        vbox.pack_start(self.infolabel, 1, 1, 1)
+
+        self.progbar = gtk.ProgressBar()
+        self.progbar.set_fraction(0.0)
+        self.progbar.show()
+        vbox.pack_start(self.progbar, 0, 0, 0)
+
+        cancel_b = gtk.Button(_("Cancel"))
+        cancel_b.connect("clicked", lambda b: cancel())
+        cancel_b.show()
+        vbox.pack_start(cancel_b, 0, 0, 0)
+
+    def status(self, _status):
+        self.infolabel.set_text(_status.msg)
+
+        if _status.cur > _status.max:
+            _status.cur = _status.max
+        self.progbar.set_fraction(_status.cur / float(_status.max))
diff --git a/chirpui/common.py b/chirpui/common.py
new file mode 100644
index 0000000..adacaec
--- /dev/null
+++ b/chirpui/common.py
@@ -0,0 +1,391 @@
+# 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 gtk
+import gobject
+import pango
+
+import threading
+import time
+import os
+import traceback
+
+from chirp import errors
+from chirpui import reporting
+
+class Editor(gobject.GObject):
+    __gsignals__ = {
+        'changed' : (gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, ()),
+        'usermsg' : (gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE,
+                     (gobject.TYPE_STRING,)),
+        }
+
+    root = None
+
+    def __init__(self):
+        gobject.GObject.__init__(self)
+        self._focused = False
+
+    def is_focused(self):
+        return self._focused
+
+    def focus(self):
+        self._focused = True
+
+    def unfocus(self):
+        self._focused = False
+
+    def copy_selection(self, cut=False):
+        pass
+
+    def paste_selection(self):
+        pass
+
+    def hotkey(self, action):
+        pass
+
+gobject.type_register(Editor)
+
+def DBG(*args):
+    if False:
+        print " ".join(args)
+
+VERBOSE = False
+
+class RadioJob:
+    def __init__(self, cb, func, *args, **kwargs):
+        self.cb = cb
+        self.cb_args = ()
+        self.func = func
+        self.args = args
+        self.kwargs = kwargs
+        self.desc = "Working"
+        self.target = None
+        self.tb = traceback.format_stack()
+
+    def __str__(self):
+        return "RadioJob(%s,%s,%s)" % (self.func, self.args, self.kwargs)
+
+    def set_desc(self, desc):
+        self.desc = desc
+
+    def set_cb_args(self, *args):
+        self.cb_args = args
+
+    def set_target(self, target):
+        self.target = target
+
+    def _execute(self, target, func):
+        try:
+            DBG("Running %s (%s %s)" % (self.func,
+                                        str(self.args),
+                                        str(self.kwargs)))
+            if VERBOSE:
+                print self.desc
+            result = func(*self.args, **self.kwargs)
+        except errors.InvalidMemoryLocation, e:
+            result = e
+        except Exception, e:
+            print "Exception running RadioJob: %s" % e
+            log_exception()
+            print "Job Args:   %s" % str(self.args)
+            print "Job KWArgs: %s" % str(self.kwargs)
+            print "Job Called from:%s%s" % (os.linesep, "".join(self.tb[:-1]))
+            result = e
+
+        if self.cb:
+            gobject.idle_add(self.cb, result, *self.cb_args)
+
+    def execute(self, radio):
+        if not self.target:
+            self.target = radio
+
+        try:
+            func = getattr(self.target, self.func)
+        except AttributeError, e:
+            print "No such radio function `%s' in %s" % (self.func,
+                                                         self.target)
+            return
+
+        self._execute(self.target, func)
+
+class RadioThread(threading.Thread, gobject.GObject):
+    __gsignals__ = {
+        "status" : (gobject.SIGNAL_RUN_LAST,
+                    gobject.TYPE_NONE,
+                    (gobject.TYPE_STRING,)),
+        }
+
+    def __init__(self, radio):
+        threading.Thread.__init__(self)
+        gobject.GObject.__init__(self)
+        self.__queue = {}
+        self.__counter = threading.Semaphore(0)
+        self.__enabled = True
+        self.__lock = threading.Lock()
+        self.__runlock = threading.Lock()
+        self.radio = radio
+
+    def _qlock(self):
+        self.__lock.acquire()
+
+    def _qunlock(self):
+        self.__lock.release()
+
+    def _qsubmit(self, job, priority):
+        if not self.__queue.has_key(priority):
+            self.__queue[priority] = []
+
+        self.__queue[priority].append(job)
+        self.__counter.release()
+
+    def _queue_clear_below(self, priority):
+        for i in range(0, priority):
+            if self.__queue.has_key(i) and len(self.__queue[i]) != 0:
+                return False
+
+        return True
+
+    def _qlock_when_idle(self, priority=10):
+        while True:
+            DBG("Attempting queue lock (%i)" % len(self.__queue))
+            self._qlock()
+            if self._queue_clear_below(priority):
+                return
+            self._qunlock()
+            time.sleep(0.1)
+
+    # This is the external lock, which stops any threads from running
+    # so that the radio can be operated synchronously
+    def lock(self):
+        self.__runlock.acquire()
+
+    def unlock(self):
+        self.__runlock.release()
+
+    def submit(self, job, priority=0):
+        self._qlock()
+        self._qsubmit(job, priority)
+        self._qunlock()
+
+    def flush(self, priority=None):
+        self._qlock()
+
+        if priority is None:
+            for i in self.__queue.keys():
+                self.__queue[i] = []
+        else:
+            self.__queue[priority] = []
+
+        self._qunlock()
+
+    def stop(self):
+        self.flush()
+        self.__counter.release()
+        self.__enabled = False
+    
+    def status(self, msg):
+        jobs = 0
+        for i in dict(self.__queue):
+                jobs += len(self.__queue[i])
+        gobject.idle_add(self.emit, "status", "[%i] %s" % (jobs, msg))
+            
+    def _queue_pop(self, priority):
+        try:
+            return self.__queue[priority].pop(0)
+        except IndexError:
+            return None
+
+    def run(self):
+        last_job_desc = "idle"
+        while self.__enabled:
+            DBG("Waiting for a job")
+            if last_job_desc:
+                self.status(_("Completed") + " " + last_job_desc + \
+                                " (" + _("idle") + ")")
+            self.__counter.acquire()
+
+            self._qlock()
+            for i in sorted(self.__queue.keys()):
+                job = self._queue_pop(i)
+                if job:
+                    DBG("Running job at priority %i" % i)
+                    break
+            self._qunlock()
+            
+            if job:
+                self.lock()
+                self.status(job.desc)
+                job.execute(self.radio)
+                last_job_desc = job.desc
+                self.unlock()
+   
+        print "RadioThread exiting"
+
+def log_exception():
+	import traceback
+	import sys
+
+        reporting.report_exception(traceback.format_exc(limit=30))
+
+	print "-- Exception: --"
+	traceback.print_exc(limit=30, file=sys.stdout)
+	print "------"
+
+def show_error(msg, parent=None):
+    d = gtk.MessageDialog(buttons=gtk.BUTTONS_OK, parent=parent,
+                          type=gtk.MESSAGE_ERROR)
+    d.set_property("text", msg)
+
+    if not parent:
+        d.set_position(gtk.WIN_POS_CENTER_ALWAYS)
+
+    d.run()
+    d.destroy()
+
+def ask_yesno_question(msg, parent=None):
+    d = gtk.MessageDialog(buttons=gtk.BUTTONS_YES_NO, parent=parent,
+                          type=gtk.MESSAGE_QUESTION)
+    d.set_property("text", msg)
+
+    if not parent:
+        d.set_position(gtk.WIN_POS_CENTER_ALWAYS)
+
+    r = d.run()
+    d.destroy()
+
+    return r == gtk.RESPONSE_YES
+
+def combo_select(box, value):
+    store = box.get_model()
+    iter = store.get_iter_first()
+    while iter:
+        if store.get(iter, 0)[0] == value:
+            box.set_active_iter(iter)
+            return True
+        iter = store.iter_next(iter)
+
+    return False
+
+def _add_text(d, text):
+    v = gtk.TextView()
+    v.get_buffer().set_text(text)
+    v.set_editable(False)
+    v.set_cursor_visible(False)
+    v.show()
+    sw = gtk.ScrolledWindow()
+    sw.add(v)
+    sw.show()
+    d.vbox.pack_start(sw, 1,1,1)
+    return v
+
+def show_error_text(msg, text, parent=None):
+    d = gtk.MessageDialog(buttons=gtk.BUTTONS_OK, parent=parent,
+                          type=gtk.MESSAGE_ERROR)
+    d.set_property("text", msg)
+
+    _add_text(d, text)
+    if not parent:
+        d.set_position(gtk.WIN_POS_CENTER_ALWAYS)
+
+    d.set_size_request(600, 400)
+    d.run()
+    d.destroy()
+
+def show_warning(msg, text,
+                 parent=None, buttons=None, title="Warning",
+                 can_squelch=False):
+    if buttons is None:
+        buttons = gtk.BUTTONS_OK
+    d = gtk.MessageDialog(buttons=buttons,
+                          parent=parent,
+                          type=gtk.MESSAGE_WARNING)
+    d.set_title(title)
+    d.set_property("text", msg)
+    l = gtk.Label(_("Details") + ":")
+    l.show()
+    d.vbox.pack_start(l, 0, 0, 0)
+    l = gtk.Label(_("Proceed?"))
+    l.show()
+    d.get_action_area().pack_start(l, 0, 0, 0)
+    d.get_action_area().reorder_child(l, 0)
+    textview = _add_text(d, text)
+    textview.set_wrap_mode(gtk.WRAP_WORD)
+    if not parent:
+        d.set_position(gtk.WIN_POS_CENTER_ALWAYS)
+    if can_squelch:
+        cb = gtk.CheckButton(_("Do not show this next time"))
+        cb.show()
+        d.vbox.pack_start(cb, 0, 0, 0)
+
+    d.set_size_request(600, 400)
+    r = d.run()
+    d.destroy()
+    if can_squelch:
+        return r, cb.get_active()
+    return r
+
+def simple_diff(a, b):
+    lines_a = a.split(os.linesep)
+    lines_b = b.split(os.linesep)
+
+    diff = ""
+    for i in range(0, len(lines_a)):
+        if lines_a[i] != lines_b[i]:
+            diff += "-%s%s" % (lines_a[i], os.linesep)
+            diff += "+%s%s" % (lines_b[i], os.linesep)
+        else:
+            diff += " %s%s" % (lines_a[i], os.linesep)
+    return diff
+
+# A quick hacked up tool to show a blob of text in a dialog window
+# using fixed-width fonts. It also highlights lines that start with
+# a '-' in red bold font and '+' with blue bold font.
+def show_diff_blob(title, result):
+    d = gtk.Dialog(title=title,
+                   buttons=(gtk.STOCK_OK, gtk.RESPONSE_OK))
+    b = gtk.TextBuffer()
+
+    tags = b.get_tag_table()
+    for color in ["red", "blue", "green", "grey"]:
+        tag = gtk.TextTag(color)
+        tag.set_property("foreground", color)
+        tags.add(tag)
+    tag = gtk.TextTag("bold")
+    tag.set_property("weight", pango.WEIGHT_BOLD)
+    tags.add(tag)
+
+    lines = result.split(os.linesep)
+    for line in lines:
+        if line.startswith("-"):
+            tags = ("red", "bold")
+        elif line.startswith("+"):
+            tags = ("blue", "bold")
+        else:
+            tags = ()
+        b.insert_with_tags_by_name(b.get_end_iter(), line + os.linesep, *tags)
+    v = gtk.TextView(b)
+    fontdesc = pango.FontDescription("Courier 11")
+    v.modify_font(fontdesc)
+    v.set_editable(False)
+    v.show()
+    s = gtk.ScrolledWindow()
+    s.add(v)
+    s.show()
+    d.vbox.pack_start(s, 1, 1, 1)
+    d.set_size_request(600, 400)
+    d.run()
+    d.destroy()
+
diff --git a/chirpui/config.py b/chirpui/config.py
new file mode 100644
index 0000000..d82a1db
--- /dev/null
+++ b/chirpui/config.py
@@ -0,0 +1,110 @@
+# Copyright 2011 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/>.
+
+from chirp import platform
+from ConfigParser import ConfigParser
+import os
+
+class ChirpConfig:
+    def __init__(self, basepath, name="chirp.config"):
+        self.__basepath = basepath
+        self.__name = name
+
+        self._default_section = "global"
+
+        self.__config = ConfigParser()
+
+        cfg = os.path.join(basepath, name)
+        if os.path.exists(cfg):
+            self.__config.read(cfg)
+
+    def save(self):
+        cfg = os.path.join(self.__basepath, self.__name)
+        cfg_file = file(cfg, "w")
+        self.__config.write(cfg_file)
+        cfg_file.close()
+
+    def get(self, key, section):
+        if not self.__config.has_section(section):
+            return None
+
+        if not self.__config.has_option(section, key):
+            return None
+
+        return self.__config.get(section, key)
+
+    def set(self, key, value, section):
+        if not self.__config.has_section(section):
+            self.__config.add_section(section)
+
+        self.__config.set(section, key, value)
+
+    def is_defined(self, key, section):
+        return self.__config.has_option(section, key)
+
+class ChirpConfigProxy:
+    def __init__(self, config, section="global"):
+        self._config = config
+        self._section = section
+
+    def get(self, key, section=None):
+        return self._config.get(key, section or self._section)
+
+    def set(self, key, value, section=None):
+        return self._config.set(key, value, section or self._section)
+
+    def get_int(self, key, section=None):
+        try:
+            return int(self.get(key, section))
+        except ValueError:
+            return 0
+
+    def set_int(self, key, value, section=None):
+        if not isinstance(value, int):
+            raise ValueError("Value is not an integer")
+
+        self.set(key, "%i" % value, section)
+
+    def get_float(self, key, section=None):
+        try:
+            return float(self.get(key, section))
+        except ValueError:
+            return 0
+
+    def set_float(self, key, value, section=None):
+        if not isinstance(value, float):
+            raise ValueError("Value is not an integer")
+
+        self.set(key, "%i" % value, section)
+       
+    def get_bool(self, key, section=None):
+        return self.get(key, section) == "True"
+
+    def set_bool(self, key, value, section=None):
+        self.set(key, str(bool(value)), section)
+
+    def is_defined(self, key, section=None):
+        return self._config.is_defined(key, section or self._section)
+
+_CONFIG = None
+def get(section="global"):
+    global _CONFIG
+
+    p = platform.get_platform()
+
+    if not _CONFIG:
+        _CONFIG = ChirpConfig(p.config_dir())
+
+    return ChirpConfigProxy(_CONFIG, section)
diff --git a/chirpui/dstaredit.py b/chirpui/dstaredit.py
new file mode 100644
index 0000000..942c6c5
--- /dev/null
+++ b/chirpui/dstaredit.py
@@ -0,0 +1,196 @@
+# 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 gtk
+import gobject
+
+from chirpui import common, miscwidgets
+
+WIDGETW = 80
+WIDGETH = 30
+
+class CallsignEditor(gtk.HBox):
+    __gsignals__ = {
+        "changed" : (gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, ()),
+        }
+
+    def _cs_changed(self, listw, callid):
+        if callid == 0 and self.first_fixed:
+            return False
+
+        self.emit("changed")
+
+        return True
+
+    def make_list(self, width):
+        cols = [ (gobject.TYPE_INT, ""),
+                 (gobject.TYPE_INT, ""),
+                 (gobject.TYPE_STRING, _("Callsign")),
+                 ]
+
+        self.listw = miscwidgets.KeyedListWidget(cols)
+        self.listw.show()
+
+        self.listw.set_editable(1, True)
+        self.listw.connect("item-set", self._cs_changed)
+
+        rend = self.listw.get_renderer(1)
+        rend.set_property("family", "Monospace")
+        rend.set_property("width-chars", width)
+
+        sw = gtk.ScrolledWindow()
+        sw.set_policy(gtk.POLICY_AUTOMATIC, gtk.POLICY_AUTOMATIC)
+        sw.add_with_viewport(self.listw)
+        sw.show()
+        
+        return sw
+
+    def __init__(self, first_fixed=False, width=8):
+        gtk.HBox.__init__(self, False, 2)
+
+        self.first_fixed = first_fixed
+
+        self.listw = None
+
+        self.pack_start(self.make_list(width), 1, 1, 1)
+
+    def set_callsigns(self, calls):
+        if self.first_fixed:
+            st = 1
+        else:
+            st = 0
+
+        values = []
+        i = 1
+        for call in calls[st:]:
+            self.listw.set_item(i, i, call)
+            i += 1
+
+    def get_callsigns(self):
+        calls = []
+        keys = self.listw.get_keys()
+        for key in keys:
+            id, idx, call = self.listw.get_item(key)
+            calls.append(call)
+
+        if self.first_fixed:
+            calls.insert(0, "")
+
+        return calls
+
+class DStarEditor(common.Editor):
+    def __cs_changed(self, cse):
+        job = None
+
+        print "Callsigns: %s" % cse.get_callsigns()
+        if cse == self.editor_ucall:
+            job = common.RadioJob(None,
+                                  "set_urcall_list",
+                                  cse.get_callsigns())
+            print "Set urcall"
+        elif cse == self.editor_rcall:
+            job = common.RadioJob(None,
+                                  "set_repeater_call_list",
+                                  cse.get_callsigns())
+            print "Set rcall"
+        elif cse == self.editor_mcall:
+            job = common.RadioJob(None,
+                                  "set_mycall_list",
+                                  cse.get_callsigns())
+
+        if job:
+            print "Submitting job to update call lists"
+            self.rthread.submit(job)
+
+        self.emit("changed")
+
+    def make_callsigns(self):
+        box = gtk.HBox(True, 2)
+
+        fixed = self.rthread.radio.get_features().has_implicit_calls
+
+        frame = gtk.Frame(_("Your callsign"))
+        self.editor_ucall = CallsignEditor(first_fixed=fixed)
+        self.editor_ucall.set_size_request(-1, 200)
+        self.editor_ucall.show()
+        frame.add(self.editor_ucall)
+        frame.show()
+        box.pack_start(frame, 1, 1, 0)
+
+        frame = gtk.Frame(_("Repeater callsign"))
+        self.editor_rcall = CallsignEditor(first_fixed=fixed)
+        self.editor_rcall.set_size_request(-1, 200)
+        self.editor_rcall.show()
+        frame.add(self.editor_rcall)
+        frame.show()
+        box.pack_start(frame, 1, 1, 0)
+
+        frame = gtk.Frame(_("My callsign"))
+        self.editor_mcall = CallsignEditor()
+        self.editor_mcall.set_size_request(-1, 200)
+        self.editor_mcall.show()
+        frame.add(self.editor_mcall)
+        frame.show()
+        box.pack_start(frame, 1, 1, 0)
+
+        box.show()
+        return box
+
+    def focus(self):
+        if  self.loaded:
+            return
+        self.loaded = True
+        print "Loading callsigns..."
+
+        def set_ucall(calls):
+            self.editor_ucall.set_callsigns(calls)
+            self.editor_ucall.connect("changed", self.__cs_changed)
+
+        def set_rcall(calls):
+            self.editor_rcall.set_callsigns(calls)
+            self.editor_rcall.connect("changed", self.__cs_changed)
+
+        def set_mcall(calls):
+            self.editor_mcall.set_callsigns(calls)
+            self.editor_mcall.connect("changed", self.__cs_changed)
+
+        job = common.RadioJob(set_ucall, "get_urcall_list")
+        job.set_desc(_("Downloading URCALL list"))
+        self.rthread.submit(job)
+
+        job = common.RadioJob(set_rcall, "get_repeater_call_list")
+        job.set_desc(_("Downloading RPTCALL list"))
+        self.rthread.submit(job)
+
+        job = common.RadioJob(set_mcall, "get_mycall_list")
+        job.set_desc(_("Downloading MYCALL list"))
+        self.rthread.submit(job)
+
+    def __init__(self, rthread):
+        common.Editor.__init__(self)
+        self.rthread = rthread
+
+        self.loaded = False
+
+        self.editor_ucall = self.editor_rcall = None
+
+        vbox = gtk.VBox(False, 2)
+        vbox.pack_start(self.make_callsigns(), 0, 0, 0)        
+
+        tmp = gtk.Label("")
+        tmp.show()
+        vbox.pack_start(tmp, 1, 1, 1)
+
+        self.root = vbox
diff --git a/chirpui/editorset.py b/chirpui/editorset.py
new file mode 100644
index 0000000..1f578a0
--- /dev/null
+++ b/chirpui/editorset.py
@@ -0,0 +1,373 @@
+# 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 os
+import gtk
+import gobject
+
+from chirp import chirp_common, directory, generic_csv, generic_xml
+from chirpui import memedit, dstaredit, bankedit, common, importdialog
+from chirpui import inputdialog, reporting, settingsedit
+
+class EditorSet(gtk.VBox):
+    __gsignals__ = {
+        "want-close" : (gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, ()),
+        "status" : (gobject.SIGNAL_RUN_LAST,
+                    gobject.TYPE_NONE,
+                    (gobject.TYPE_STRING,)),
+        "usermsg": (gobject.SIGNAL_RUN_LAST,
+                   gobject.TYPE_NONE,
+                   (gobject.TYPE_STRING,)),
+        "editor-selected" : (gobject.SIGNAL_RUN_LAST,
+                             gobject.TYPE_NONE,
+                             (gobject.TYPE_STRING,)),
+        }
+
+    def __init__(self, source, parent_window=None, filename=None, tempname=None):
+        gtk.VBox.__init__(self, True, 0)
+
+        self.parent_window = parent_window
+
+        if isinstance(source, str):
+            self.filename = source
+            self.radio = directory.get_radio_by_image(self.filename)
+        elif isinstance(source, chirp_common.Radio):
+            self.radio = source
+            self.filename = filename or tempname or source.VARIANT
+        else:
+            raise Exception("Unknown source type")
+
+        self.rthread = common.RadioThread(self.radio)
+        self.rthread.setDaemon(True)
+        self.rthread.start()
+
+        self.rthread.connect("status", lambda e, m: self.emit("status", m))
+
+        self.tabs = gtk.Notebook()
+        self.tabs.connect("switch-page", self.tab_selected)
+        self.tabs.set_tab_pos(gtk.POS_LEFT)
+
+        self.editors = {
+            "memedit"      : None,
+            "dstar"        : None,
+            "bank_names"   : None,
+            "bank_members" : None,
+            "settings"     : None,
+            }
+
+        if isinstance(self.radio, chirp_common.IcomDstarSupport):
+            self.editors["memedit"] = memedit.DstarMemoryEditor(self.rthread)
+            self.editors["dstar"] = dstaredit.DStarEditor(self.rthread)
+        else:
+            self.editors["memedit"] = memedit.MemoryEditor(self.rthread)
+
+        self.editors["memedit"].connect("usermsg",
+                                        lambda e, m: self.emit("usermsg", m))
+
+        rf = self.radio.get_features()
+
+        if rf.has_bank:
+            self.editors["bank_members"] = \
+                bankedit.BankMembershipEditor(self.rthread, self)
+        
+        if rf.has_bank_names:
+            self.editors["bank_names"] = bankedit.BankNameEditor(self.rthread)
+
+        if rf.has_settings:
+            self.editors["settings"] = settingsedit.SettingsEditor(self.rthread)
+
+        lab = gtk.Label(_("Memories"))
+        self.tabs.append_page(self.editors["memedit"].root, lab)
+        self.editors["memedit"].root.show()
+
+        if self.editors["dstar"]:
+            lab = gtk.Label(_("D-STAR"))
+            self.tabs.append_page(self.editors["dstar"].root, lab)
+            self.editors["dstar"].root.show()
+            self.editors["dstar"].connect("changed", self.dstar_changed)
+
+        if self.editors["bank_names"]:
+            lab = gtk.Label(_("Bank Names"))
+            self.tabs.append_page(self.editors["bank_names"].root, lab)
+            self.editors["bank_names"].root.show()
+            self.editors["bank_names"].connect("changed", self.banks_changed)
+
+        if self.editors["bank_members"]:
+            lab = gtk.Label(_("Banks"))
+            self.tabs.append_page(self.editors["bank_members"].root, lab)
+            self.editors["bank_members"].root.show()
+            self.editors["bank_members"].connect("changed", self.banks_changed)
+
+        if self.editors["settings"]:
+            lab = gtk.Label(_("Settings"))
+            self.tabs.append_page(self.editors["settings"].root, lab)
+            self.editors["settings"].root.show()
+
+        self.pack_start(self.tabs)
+        self.tabs.show()
+
+        # pylint: disable-msg=E1101
+        self.editors["memedit"].connect("changed", self.editor_changed)
+
+        self.label = self.text_label = None
+        self.make_label()
+        self.modified = (tempname is not None)
+        if tempname:
+            self.filename = tempname
+        self.update_tab()
+
+    def make_label(self):
+        self.label = gtk.HBox(False, 0)
+
+        self.text_label = gtk.Label("")
+        self.text_label.show()
+        self.label.pack_start(self.text_label, 1, 1, 1)
+
+        button = gtk.Button("X")
+        button.set_relief(gtk.RELIEF_NONE)
+        button.connect("clicked", lambda x: self.emit("want-close"))
+        button.show()
+        self.label.pack_start(button, 0, 0, 0)
+
+        self.label.show()
+
+    def update_tab(self):
+        fn = os.path.basename(self.filename)
+        if self.modified:
+            text = "%s*" % fn
+        else:
+            text = fn
+
+        self.text_label.set_text(self.radio.get_name() + ": " + text)
+
+    def save(self, fname=None):
+        if not fname:
+            fname = self.filename
+            if not os.path.exists(self.filename):
+                return # Probably before the first "Save as"
+        else:
+            self.filename = fname
+
+        self.rthread.lock()
+        try:
+            self.radio.save(fname)
+        except:
+            self.rthread.unlock()
+            raise
+        self.rthread.unlock()
+
+        self.modified = False
+        self.update_tab()
+
+    def dstar_changed(self, *args):
+        print "D-STAR editor changed"
+        memedit = self.editors["memedit"]
+        dstared = self.editors["dstar"]
+        memedit.set_urcall_list(dstared.editor_ucall.get_callsigns())
+        memedit.set_repeater_list(dstared.editor_rcall.get_callsigns())
+        memedit.prefill()
+        if not isinstance(self.radio, chirp_common.LiveRadio):
+            self.modified = True
+            self.update_tab()
+
+    def banks_changed(self, *args):
+        print "Banks changed"
+        if self.editors["bank_members"]:
+            self.editors["bank_members"].banks_changed()
+        if not isinstance(self.radio, chirp_common.LiveRadio):
+            self.modified = True
+            self.update_tab()
+
+    def editor_changed(self, *args):
+        if not isinstance(self.radio, chirp_common.LiveRadio):
+            self.modified = True
+            self.update_tab()
+        if self.editors["bank_members"]:
+            self.editors["bank_members"].memories_changed()
+
+    def get_tab_label(self):
+        return self.label
+
+    def is_modified(self):
+        return self.modified
+
+    def _do_import_locked(self, dlgclass, src_radio, dst_rthread):
+
+        # An import/export action needs to be done in the absence of any
+        # other queued changes.  So, we make sure that nothing else is
+        # staged for the thread and lock it up.  Then we use the hidden
+        # interface to queue our own changes before opening it up to the
+        # rest of the world.
+
+        dst_rthread._qlock_when_idle(5) # Suspend job submission when idle
+
+        dialog = dlgclass(src_radio, dst_rthread.radio, self.parent_window)
+        r = dialog.run()
+        dialog.hide()
+        if r != gtk.RESPONSE_OK:
+            dst_rthread._qunlock()
+            return
+
+        count = dialog.do_import(dst_rthread)
+        print "Imported %i" % count
+        dst_rthread._qunlock()
+
+        if count > 0:
+            self.editor_changed()
+            gobject.idle_add(self.editors["memedit"].prefill)
+
+        return count
+
+    def choose_sub_device(self, radio):
+        devices = radio.get_sub_devices()
+        choices = [x.VARIANT for x in devices]
+
+        d = inputdialog.ChoiceDialog(choices)
+        d.label.set_text(_("The {vendor} {model} has multiple "
+                           "independent sub-devices").format( \
+                vendor=radio.VENDOR, model=radio.MODEL) + os.linesep + \
+                             _("Choose one to import from:"))
+        r = d.run()
+        chosen = d.choice.get_active_text()
+        d.destroy()
+        if r == gtk.RESPONSE_CANCEL:
+            raise Exception(_("Cancelled"))
+        for d in devices:
+            if d.VARIANT == chosen:
+                return d
+
+        raise Exception(_("Internal Error"))
+
+    def do_import(self, filen):
+        try:
+            src_radio = directory.get_radio_by_image(filen)
+        except Exception, e:
+            common.show_error(e)
+            return
+
+        if isinstance(src_radio, chirp_common.NetworkSourceRadio):
+            ww = importdialog.WaitWindow("Querying...", self.parent_window)
+            ww.show()
+            def status(status):
+                ww.set(float(status.cur) / float(status.max))
+            try:
+                src_radio.status_fn = status
+                src_radio.do_fetch()
+            except Exception, e:
+                common.show_error(e)
+                ww.hide()
+                return
+            ww.hide()
+
+        try:
+            if src_radio.get_features().has_sub_devices:
+                src_radio = self.choose_sub_device(src_radio)
+        except Exception, e:
+            common.show_error(e)
+            return
+
+        if len(src_radio.errors) > 0:
+            _filen = os.path.basename(filen)
+            common.show_error_text(_("There were errors while opening {file}. "
+                                     "The affected memories will not "
+                                     "be importable!").format(file=_filen),
+                                   "\r\n".join(src_radio.errors))
+
+        try:
+            count = self._do_import_locked(importdialog.ImportDialog,
+                                           src_radio,
+                                           self.rthread)
+            reporting.report_model_usage(src_radio, "importsrc", True)
+        except Exception, e:
+            common.log_exception()
+            common.show_error(_("There was an error during "
+                                "import: {error}").format(error=e))
+        
+    def do_export(self, filen):
+        try:
+            if filen.lower().endswith(".csv"):
+                dst_radio = generic_csv.CSVRadio(filen)
+            elif filen.lower().endswith(".chirp"):
+                dst_radio = generic_xml.XMLRadio(filen)
+            else:
+                raise Exception(_("Unsupported file type"))
+        except Exception, e:
+            common.log_exception()
+            common.show_error(e)
+            return
+
+        dst_rthread = common.RadioThread(dst_radio)
+        dst_rthread.setDaemon(True)
+        dst_rthread.start()
+
+        try:
+            count = self._do_import_locked(importdialog.ExportDialog,
+                                           self.rthread.radio,
+                                           dst_rthread)
+        except Exception, e:
+            common.log_exception()
+            common.show_error(_("There was an error during "
+                                "export: {error}").format(error=e),
+                              self.parent_window)
+            return
+
+        if count <= 0:
+            return
+
+        # Wait for thread queue to complete
+        dst_rthread._qlock_when_idle()
+
+        try:
+            dst_radio.save(filename=filen)
+        except Exception, e:
+            common.log_exception()
+            common.show_error(_("There was an error during "
+                                "export: {error}").format(error=e),
+                              self)
+            
+    def prime(self):
+        mem = chirp_common.Memory()
+        mem.freq = 146010000
+
+        def cb(*args):
+            gobject.idle_add(self.editors["memedit"].prefill)
+
+        job = common.RadioJob(cb, "set_memory", mem)
+        job.set_desc(_("Priming memory"))
+        self.rthread.submit(job)
+
+    def tab_selected(self, notebook, foo, pagenum):
+        widget = notebook.get_nth_page(pagenum)
+        for k,v in self.editors.items():
+            if v and v.root == widget:
+                v.focus()
+                self.emit("editor-selected", k)
+            elif v:
+                v.unfocus()
+
+    def set_read_only(self, read_only=True):
+        self.editors["memedit"].set_read_only(read_only)
+    
+    def get_read_only(self):
+        return self.editors["memedit"].get_read_only()
+
+    def prepare_close(self):
+        self.editors["memedit"].prepare_close()
+
+    def get_current_editor(self):
+        for e in self.editors.values():
+            if e and self.tabs.page_num(e.root) == self.tabs.get_current_page():
+                return e
+        raise Exception("No editor selected?")
diff --git a/chirpui/fips.py b/chirpui/fips.py
new file mode 100644
index 0000000..0094ec1
--- /dev/null
+++ b/chirpui/fips.py
@@ -0,0 +1,6606 @@
+# 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/>.
+
+
+FIPS_STATES = {
+    "Alaska"               : 2,
+    "Alabama"              : 1,
+    "Arkansas"             : 5,
+    "Arizona"              : 4,
+    "California"           : 6,
+    "Colorado"             : 8,
+    "Connecticut"          : 9,
+    "District of Columbia" : 11,
+    "Delaware"             : 10,
+    "Florida"              : 12,
+    "Georgia"              : 13,
+    "Guam"                 : 66,
+    "Hawaii"               : 15,
+    "Iowa"                 : 19,
+    "Idaho"                : 16,
+    "Illinois"             : 17,
+    "Indiana"              : 18,
+    "Kansas"               : 20,
+    "Kentucky"             : 21,
+    "Louisiana"            : 22,
+    "Massachusetts"        : 25,
+    "Maryland"             : 24,
+    "Maine"                : 23,
+    "Michigan"             : 26,
+    "Minnesota"            : 27,
+    "Missouri"             : 29,
+    "Mississippi"          : 28,
+    "Montana"              : 30,
+    "North Carolina"       : 37,
+    "North Dakota"         : 38,
+    "Nebraska"             : 31,
+    "New Hampshire"        : 33,
+    "New Jersey"           : 34,
+    "New Mexico"           : 35,
+    "Nevada"               : 32,
+    "New York"             : 36,
+    "Ohio"                 : 39,
+    "Oklahoma"             : 40,
+    "Oregon"               : 41,
+    "Pennsylvania"         : 42,
+    "Puerto Rico"          : 72,
+    "Rhode Island"         : 44,
+    "South Carolina"       : 45,
+    "South Dakota"         : 46,
+    "Tennessee"            : 47,
+    "Texas"                : 48,
+    "Utah"                 : 49,
+    "Virginia"             : 51,
+    "Virgin Islands"       : 78,
+    "Vermont"              : 50,
+    "Washington"           : 53,
+    "Wisconsin"            : 55,
+    "West Virginia"        : 54,
+    "Wyoming"              : 56,
+    "Alberta"              : "CA01",
+    "British Columbia"     : "CA02",
+    "Manitoba"             : "CA03",
+    "New Brunswick"        : "CA04",
+    "Newfoundland and Labrador": "CA05",
+    "Northwest Territories": "CA13",
+    "Nova Scotia"          : "CA07",
+    "Nunavut"              : "CA14",
+    "Ontario"              : "CA08",
+    "Prince Edward Island" : "CA09",
+    "Quebec"               : "CA10",
+    "Saskatchewan"         : "CA11",
+    "Yukon"                : "CA12",
+}
+
+FIPS_COUNTIES = {
+  1: { '--All--': '%',
+       'Autauga County, AL': '001',
+       'Baldwin County, AL': '003',
+       'Barbour County, AL': '005',
+       'Bibb County, AL': '007',
+       'Blount County, AL': '009',
+       'Bullock County, AL': '011',
+       'Butler County, AL': '013',
+       'Calhoun County, AL': '015',
+       'Chambers County, AL': '017',
+       'Cherokee County, AL': '019',
+       'Chilton County, AL': '021',
+       'Choctaw County, AL': '023',
+       'Clarke County, AL': '025',
+       'Clay County, AL': '027',
+       'Cleburne County, AL': '029',
+       'Coffee County, AL': '031',
+       'Colbert County, AL': '033',
+       'Conecuh County, AL': '035',
+       'Coosa County, AL': '037',
+       'Covington County, AL': '039',
+       'Crenshaw County, AL': '041',
+       'Cullman County, AL': '043',
+       'Dale County, AL': '045',
+       'Dallas County, AL': '047',
+       'DeKalb County, AL': '049',
+       'Elmore County, AL': '051',
+       'Escambia County, AL': '053',
+       'Etowah County, AL': '055',
+       'Fayette County, AL': '057',
+       'Franklin County, AL': '059',
+       'Geneva County, AL': '061',
+       'Greene County, AL': '063',
+       'Hale County, AL': '065',
+       'Henry County, AL': '067',
+       'Houston County, AL': '069',
+       'Jackson County, AL': '071',
+       'Jefferson County, AL': '073',
+       'Lamar County, AL': '075',
+       'Lauderdale County, AL': '077',
+       'Lawrence County, AL': '079',
+       'Lee County, AL': '081',
+       'Limestone County, AL': '083',
+       'Lowndes County, AL': '085',
+       'Macon County, AL': '087',
+       'Madison County, AL': '089',
+       'Marengo County, AL': '091',
+       'Marion County, AL': '093',
+       'Marshall County, AL': '095',
+       'Mobile County, AL': '097',
+       'Monroe County, AL': '099',
+       'Montgomery County, AL': '101',
+       'Morgan County, AL': '103',
+       'Perry County, AL': '105',
+       'Pickens County, AL': '107',
+       'Pike County, AL': '109',
+       'Randolph County, AL': '111',
+       'Russell County, AL': '113',
+       'Shelby County, AL': '117',
+       'St. Clair County, AL': '115',
+       'Sumter County, AL': '119',
+       'Talladega County, AL': '121',
+       'Tallapoosa County, AL': '123',
+       'Tuscaloosa County, AL': '125',
+       'Walker County, AL': '127',
+       'Washington County, AL': '129',
+       'Wilcox County, AL': '131',
+       'Winston County, AL': '133'},
+  2: { '--All--': '%',
+       'Aleutians East Borough, AK': '013',
+       'Aleutians West Census Area, AK': '016',
+       'Anchorage Borough/municipality, AK': '020',
+       'Bethel Census Area, AK': '050',
+       'Bristol Bay Borough, AK': '060',
+       'Denali Borough, AK': '068',
+       'Dillingham Census Area, AK': '070',
+       'Fairbanks North Star Borough, AK': '090',
+       'Haines Borough, AK': '100',
+       'Juneau Borough/city, AK': '110',
+       'Kenai Peninsula Borough, AK': '122',
+       'Ketchikan Gateway Borough, AK': '130',
+       'Kodiak Island Borough, AK': '150',
+       'Lake and Peninsula Borough, AK': '164',
+       'Matanuska-Susitna Borough, AK': '170',
+       'Nome Census Area, AK': '180',
+       'North Slope Borough, AK': '185',
+       'Northwest Arctic Borough, AK': '188',
+       'Prince of Wales-Outer Ketchikan Census Area, AK': '201',
+       'Sitka Borough/city, AK': '220',
+       'Skagway-Hoonah-Angoon Census Area, AK': '232',
+       'Southeast Fairbanks Census Area, AK': '240',
+       'Valdez-Cordova Census Area, AK': '261',
+       'Wade Hampton Census Area, AK': '270',
+       'Wrangell-Petersburg Census Area, AK': '280',
+       'Yakutat Borough, AK': '282',
+       'Yukon-Koyukuk Census Area, AK': '290'},
+  4: { '--All--': '%',
+       'Apache County, AZ': '001',
+       'Cochise County, AZ': '003',
+       'Coconino County, AZ': '005',
+       'Gila County, AZ': '007',
+       'Graham County, AZ': '009',
+       'Greenlee County, AZ': '011',
+       'La Paz County, AZ': '012',
+       'Maricopa County, AZ': '013',
+       'Mohave County, AZ': '015',
+       'Navajo County, AZ': '017',
+       'Pima County, AZ': '019',
+       'Pinal County, AZ': '021',
+       'Santa Cruz County, AZ': '023',
+       'Yavapai County, AZ': '025',
+       'Yuma County, AZ': '027'},
+  5: { '--All--': '%',
+       'Arkansas County, AR': '001',
+       'Ashley County, AR': '003',
+       'Baxter County, AR': '005',
+       'Benton County, AR': '007',
+       'Boone County, AR': '009',
+       'Bradley County, AR': '011',
+       'Calhoun County, AR': '013',
+       'Carroll County, AR': '015',
+       'Chicot County, AR': '017',
+       'Clark County, AR': '019',
+       'Clay County, AR': '021',
+       'Cleburne County, AR': '023',
+       'Cleveland County, AR': '025',
+       'Columbia County, AR': '027',
+       'Conway County, AR': '029',
+       'Craighead County, AR': '031',
+       'Crawford County, AR': '033',
+       'Crittenden County, AR': '035',
+       'Cross County, AR': '037',
+       'Dallas County, AR': '039',
+       'Desha County, AR': '041',
+       'Drew County, AR': '043',
+       'Faulkner County, AR': '045',
+       'Franklin County, AR': '047',
+       'Fulton County, AR': '049',
+       'Garland County, AR': '051',
+       'Grant County, AR': '053',
+       'Greene County, AR': '055',
+       'Hempstead County, AR': '057',
+       'Hot Spring County, AR': '059',
+       'Howard County, AR': '061',
+       'Independence County, AR': '063',
+       'Izard County, AR': '065',
+       'Jackson County, AR': '067',
+       'Jefferson County, AR': '069',
+       'Johnson County, AR': '071',
+       'Lafayette County, AR': '073',
+       'Lawrence County, AR': '075',
+       'Lee County, AR': '077',
+       'Lincoln County, AR': '079',
+       'Little River County, AR': '081',
+       'Logan County, AR': '083',
+       'Lonoke County, AR': '085',
+       'Madison County, AR': '087',
+       'Marion County, AR': '089',
+       'Miller County, AR': '091',
+       'Mississippi County, AR': '093',
+       'Monroe County, AR': '095',
+       'Montgomery County, AR': '097',
+       'Nevada County, AR': '099',
+       'Newton County, AR': '101',
+       'Ouachita County, AR': '103',
+       'Perry County, AR': '105',
+       'Phillips County, AR': '107',
+       'Pike County, AR': '109',
+       'Poinsett County, AR': '111',
+       'Polk County, AR': '113',
+       'Pope County, AR': '115',
+       'Prairie County, AR': '117',
+       'Pulaski County, AR': '119',
+       'Randolph County, AR': '121',
+       'Saline County, AR': '125',
+       'Scott County, AR': '127',
+       'Searcy County, AR': '129',
+       'Sebastian County, AR': '131',
+       'Sevier County, AR': '133',
+       'Sharp County, AR': '135',
+       'St. Francis County, AR': '123',
+       'Stone County, AR': '137',
+       'Union County, AR': '139',
+       'Van Buren County, AR': '141',
+       'Washington County, AR': '143',
+       'White County, AR': '145',
+       'Woodruff County, AR': '147',
+       'Yell County, AR': '149'},
+  6: { '--All--': '%',
+       'Alameda County, CA': '001',
+       'Alpine County, CA': '003',
+       'Amador County, CA': '005',
+       'Butte County, CA': '007',
+       'Calaveras County, CA': '009',
+       'Colusa County, CA': '011',
+       'Contra Costa County, CA': '013',
+       'Del Norte County, CA': '015',
+       'El Dorado County, CA': '017',
+       'Fresno County, CA': '019',
+       'Glenn County, CA': '021',
+       'Humboldt County, CA': '023',
+       'Imperial County, CA': '025',
+       'Inyo County, CA': '027',
+       'Kern County, CA': '029',
+       'Kings County, CA': '031',
+       'Lake County, CA': '033',
+       'Lassen County, CA': '035',
+       'Los Angeles County, CA': '037',
+       'Madera County, CA': '039',
+       'Marin County, CA': '041',
+       'Mariposa County, CA': '043',
+       'Mendocino County, CA': '045',
+       'Merced County, CA': '047',
+       'Modoc County, CA': '049',
+       'Mono County, CA': '051',
+       'Monterey County, CA': '053',
+       'Napa County, CA': '055',
+       'Nevada County, CA': '057',
+       'Orange County, CA': '059',
+       'Placer County, CA': '061',
+       'Plumas County, CA': '063',
+       'Riverside County, CA': '065',
+       'Sacramento County, CA': '067',
+       'San Benito County, CA': '069',
+       'San Bernardino County, CA': '071',
+       'San Diego County, CA': '073',
+       'San Francisco County/city, CA': '075',
+       'San Joaquin County, CA': '077',
+       'San Luis Obispo County, CA': '079',
+       'San Mateo County, CA': '081',
+       'Santa Barbara County, CA': '083',
+       'Santa Clara County, CA': '085',
+       'Santa Cruz County, CA': '087',
+       'Shasta County, CA': '089',
+       'Sierra County, CA': '091',
+       'Siskiyou County, CA': '093',
+       'Solano County, CA': '095',
+       'Sonoma County, CA': '097',
+       'Stanislaus County, CA': '099',
+       'Sutter County, CA': '101',
+       'Tehama County, CA': '103',
+       'Trinity County, CA': '105',
+       'Tulare County, CA': '107',
+       'Tuolumne County, CA': '109',
+       'Ventura County, CA': '111',
+       'Yolo County, CA': '113',
+       'Yuba County, CA': '115'},
+  8: { '--All--': '%',
+       'Adams County, CO': '001',
+       'Alamosa County, CO': '003',
+       'Arapahoe County, CO': '005',
+       'Archuleta County, CO': '007',
+       'Baca County, CO': '009',
+       'Bent County, CO': '011',
+       'Boulder County, CO': '013',
+       'Broomfield County/city, CO': '014',
+       'Chaffee County, CO': '015',
+       'Cheyenne County, CO': '017',
+       'Clear Creek County, CO': '019',
+       'Conejos County, CO': '021',
+       'Costilla County, CO': '023',
+       'Crowley County, CO': '025',
+       'Custer County, CO': '027',
+       'Delta County, CO': '029',
+       'Denver County/city, CO': '031',
+       'Dolores County, CO': '033',
+       'Douglas County, CO': '035',
+       'Eagle County, CO': '037',
+       'El Paso County, CO': '041',
+       'Elbert County, CO': '039',
+       'Fremont County, CO': '043',
+       'Garfield County, CO': '045',
+       'Gilpin County, CO': '047',
+       'Grand County, CO': '049',
+       'Gunnison County, CO': '051',
+       'Hinsdale County, CO': '053',
+       'Huerfano County, CO': '055',
+       'Jackson County, CO': '057',
+       'Jefferson County, CO': '059',
+       'Kiowa County, CO': '061',
+       'Kit Carson County, CO': '063',
+       'La Plata County, CO': '067',
+       'Lake County, CO': '065',
+       'Larimer County, CO': '069',
+       'Las Animas County, CO': '071',
+       'Lincoln County, CO': '073',
+       'Logan County, CO': '075',
+       'Mesa County, CO': '077',
+       'Mineral County, CO': '079',
+       'Moffat County, CO': '081',
+       'Montezuma County, CO': '083',
+       'Montrose County, CO': '085',
+       'Morgan County, CO': '087',
+       'Otero County, CO': '089',
+       'Ouray County, CO': '091',
+       'Park County, CO': '093',
+       'Phillips County, CO': '095',
+       'Pitkin County, CO': '097',
+       'Prowers County, CO': '099',
+       'Pueblo County, CO': '101',
+       'Rio Blanco County, CO': '103',
+       'Rio Grande County, CO': '105',
+       'Routt County, CO': '107',
+       'Saguache County, CO': '109',
+       'San Juan County, CO': '111',
+       'San Miguel County, CO': '113',
+       'Sedgwick County, CO': '115',
+       'Summit County, CO': '117',
+       'Teller County, CO': '119',
+       'Washington County, CO': '121',
+       'Weld County, CO': '123',
+       'Yuma County, CO': '125'},
+  9: { '--All--': '%',
+       'Fairfield County, CT': '001',
+       'Hartford County, CT': '003',
+       'Litchfield County, CT': '005',
+       'Middlesex County, CT': '007',
+       'New Haven County, CT': '009',
+       'New London County, CT': '011',
+       'Tolland County, CT': '013',
+       'Windham County, CT': '015'},
+  10: { '--All--': '%',
+        'Kent County, DE': '001',
+        'New Castle County, DE': '003',
+        'Sussex County, DE': '005'},
+  11: { '--All--': '%', 'District of Columbia': '001'},
+  12: { '--All--': '%',
+        'Alachua County, FL': '001',
+        'Baker County, FL': '003',
+        'Bay County, FL': '005',
+        'Bradford County, FL': '007',
+        'Brevard County, FL': '009',
+        'Broward County, FL': '011',
+        'Calhoun County, FL': '013',
+        'Charlotte County, FL': '015',
+        'Citrus County, FL': '017',
+        'Clay County, FL': '019',
+        'Collier County, FL': '021',
+        'Columbia County, FL': '023',
+        'DeSoto County, FL': '027',
+        'Dixie County, FL': '029',
+        'Duval County, FL': '031',
+        'Escambia County, FL': '033',
+        'Flagler County, FL': '035',
+        'Franklin County, FL': '037',
+        'Gadsden County, FL': '039',
+        'Gilchrist County, FL': '041',
+        'Glades County, FL': '043',
+        'Gulf County, FL': '045',
+        'Hamilton County, FL': '047',
+        'Hardee County, FL': '049',
+        'Hendry County, FL': '051',
+        'Hernando County, FL': '053',
+        'Highlands County, FL': '055',
+        'Hillsborough County, FL': '057',
+        'Holmes County, FL': '059',
+        'Indian River County, FL': '061',
+        'Jackson County, FL': '063',
+        'Jefferson County, FL': '065',
+        'Lafayette County, FL': '067',
+        'Lake County, FL': '069',
+        'Lee County, FL': '071',
+        'Leon County, FL': '073',
+        'Levy County, FL': '075',
+        'Liberty County, FL': '077',
+        'Madison County, FL': '079',
+        'Manatee County, FL': '081',
+        'Marion County, FL': '083',
+        'Martin County, FL': '085',
+        'Miami-Dade County, FL': '086',
+        'Monroe County, FL': '087',
+        'Nassau County, FL': '089',
+        'Okaloosa County, FL': '091',
+        'Okeechobee County, FL': '093',
+        'Orange County, FL': '095',
+        'Osceola County, FL': '097',
+        'Palm Beach County, FL': '099',
+        'Pasco County, FL': '101',
+        'Pinellas County, FL': '103',
+        'Polk County, FL': '105',
+        'Putnam County, FL': '107',
+        'Santa Rosa County, FL': '113',
+        'Sarasota County, FL': '115',
+        'Seminole County, FL': '117',
+        'St. Johns County, FL': '109',
+        'St. Lucie County, FL': '111',
+        'Sumter County, FL': '119',
+        'Suwannee County, FL': '121',
+        'Taylor County, FL': '123',
+        'Union County, FL': '125',
+        'Volusia County, FL': '127',
+        'Wakulla County, FL': '129',
+        'Walton County, FL': '131',
+        'Washington County, FL': '133'},
+  13: { '--All--': '%',
+        'Appling County, GA': '001',
+        'Atkinson County, GA': '003',
+        'Bacon County, GA': '005',
+        'Baker County, GA': '007',
+        'Baldwin County, GA': '009',
+        'Banks County, GA': '011',
+        'Barrow County, GA': '013',
+        'Bartow County, GA': '015',
+        'Ben Hill County, GA': '017',
+        'Berrien County, GA': '019',
+        'Bibb County, GA': '021',
+        'Bleckley County, GA': '023',
+        'Brantley County, GA': '025',
+        'Brooks County, GA': '027',
+        'Bryan County, GA': '029',
+        'Bulloch County, GA': '031',
+        'Burke County, GA': '033',
+        'Butts County, GA': '035',
+        'Calhoun County, GA': '037',
+        'Camden County, GA': '039',
+        'Candler County, GA': '043',
+        'Carroll County, GA': '045',
+        'Catoosa County, GA': '047',
+        'Charlton County, GA': '049',
+        'Chatham County, GA': '051',
+        'Chattahoochee County, GA': '053',
+        'Chattooga County, GA': '055',
+        'Cherokee County, GA': '057',
+        'Clarke County, GA': '059',
+        'Clay County, GA': '061',
+        'Clayton County, GA': '063',
+        'Clinch County, GA': '065',
+        'Cobb County, GA': '067',
+        'Coffee County, GA': '069',
+        'Colquitt County, GA': '071',
+        'Columbia County, GA': '073',
+        'Cook County, GA': '075',
+        'Coweta County, GA': '077',
+        'Crawford County, GA': '079',
+        'Crisp County, GA': '081',
+        'Dade County, GA': '083',
+        'Dawson County, GA': '085',
+        'DeKalb County, GA': '089',
+        'Decatur County, GA': '087',
+        'Dodge County, GA': '091',
+        'Dooly County, GA': '093',
+        'Dougherty County, GA': '095',
+        'Douglas County, GA': '097',
+        'Early County, GA': '099',
+        'Echols County, GA': '101',
+        'Effingham County, GA': '103',
+        'Elbert County, GA': '105',
+        'Emanuel County, GA': '107',
+        'Evans County, GA': '109',
+        'Fannin County, GA': '111',
+        'Fayette County, GA': '113',
+        'Floyd County, GA': '115',
+        'Forsyth County, GA': '117',
+        'Franklin County, GA': '119',
+        'Fulton County, GA': '121',
+        'Gilmer County, GA': '123',
+        'Glascock County, GA': '125',
+        'Glynn County, GA': '127',
+        'Gordon County, GA': '129',
+        'Grady County, GA': '131',
+        'Greene County, GA': '133',
+        'Gwinnett County, GA': '135',
+        'Habersham County, GA': '137',
+        'Hall County, GA': '139',
+        'Hancock County, GA': '141',
+        'Haralson County, GA': '143',
+        'Harris County, GA': '145',
+        'Hart County, GA': '147',
+        'Heard County, GA': '149',
+        'Henry County, GA': '151',
+        'Houston County, GA': '153',
+        'Irwin County, GA': '155',
+        'Jackson County, GA': '157',
+        'Jasper County, GA': '159',
+        'Jeff Davis County, GA': '161',
+        'Jefferson County, GA': '163',
+        'Jenkins County, GA': '165',
+        'Johnson County, GA': '167',
+        'Jones County, GA': '169',
+        'Lamar County, GA': '171',
+        'Lanier County, GA': '173',
+        'Laurens County, GA': '175',
+        'Lee County, GA': '177',
+        'Liberty County, GA': '179',
+        'Lincoln County, GA': '181',
+        'Long County, GA': '183',
+        'Lowndes County, GA': '185',
+        'Lumpkin County, GA': '187',
+        'Macon County, GA': '193',
+        'Madison County, GA': '195',
+        'Marion County, GA': '197',
+        'McDuffie County, GA': '189',
+        'McIntosh County, GA': '191',
+        'Meriwether County, GA': '199',
+        'Miller County, GA': '201',
+        'Mitchell County, GA': '205',
+        'Monroe County, GA': '207',
+        'Montgomery County, GA': '209',
+        'Morgan County, GA': '211',
+        'Murray County, GA': '213',
+        'Muscogee County, GA': '215',
+        'Newton County, GA': '217',
+        'Oconee County, GA': '219',
+        'Oglethorpe County, GA': '221',
+        'Paulding County, GA': '223',
+        'Peach County, GA': '225',
+        'Pickens County, GA': '227',
+        'Pierce County, GA': '229',
+        'Pike County, GA': '231',
+        'Polk County, GA': '233',
+        'Pulaski County, GA': '235',
+        'Putnam County, GA': '237',
+        'Quitman County, GA': '239',
+        'Rabun County, GA': '241',
+        'Randolph County, GA': '243',
+        'Richmond County, GA': '245',
+        'Rockdale County, GA': '247',
+        'Schley County, GA': '249',
+        'Screven County, GA': '251',
+        'Seminole County, GA': '253',
+        'Spalding County, GA': '255',
+        'Stephens County, GA': '257',
+        'Stewart County, GA': '259',
+        'Sumter County, GA': '261',
+        'Talbot County, GA': '263',
+        'Taliaferro County, GA': '265',
+        'Tattnall County, GA': '267',
+        'Taylor County, GA': '269',
+        'Telfair County, GA': '271',
+        'Terrell County, GA': '273',
+        'Thomas County, GA': '275',
+        'Tift County, GA': '277',
+        'Toombs County, GA': '279',
+        'Towns County, GA': '281',
+        'Treutlen County, GA': '283',
+        'Troup County, GA': '285',
+        'Turner County, GA': '287',
+        'Twiggs County, GA': '289',
+        'Union County, GA': '291',
+        'Upson County, GA': '293',
+        'Walker County, GA': '295',
+        'Walton County, GA': '297',
+        'Ware County, GA': '299',
+        'Warren County, GA': '301',
+        'Washington County, GA': '303',
+        'Wayne County, GA': '305',
+        'Webster County, GA': '307',
+        'Wheeler County, GA': '309',
+        'White County, GA': '311',
+        'Whitfield County, GA': '313',
+        'Wilcox County, GA': '315',
+        'Wilkes County, GA': '317',
+        'Wilkinson County, GA': '319',
+        'Worth County, GA': '321'},
+  15: { '--All--': '%',
+        'Hawaii County, HI': '001',
+        'Honolulu County/city, HI': '003',
+        'Kauai County, HI': '007',
+        'Maui County, HI': '009'},
+  16: { '--All--': '%',
+        'Ada County, ID': '001',
+        'Adams County, ID': '003',
+        'Bannock County, ID': '005',
+        'Bear Lake County, ID': '007',
+        'Benewah County, ID': '009',
+        'Bingham County, ID': '011',
+        'Blaine County, ID': '013',
+        'Boise County, ID': '015',
+        'Bonner County, ID': '017',
+        'Bonneville County, ID': '019',
+        'Boundary County, ID': '021',
+        'Butte County, ID': '023',
+        'Camas County, ID': '025',
+        'Canyon County, ID': '027',
+        'Caribou County, ID': '029',
+        'Cassia County, ID': '031',
+        'Clark County, ID': '033',
+        'Clearwater County, ID': '035',
+        'Custer County, ID': '037',
+        'Elmore County, ID': '039',
+        'Franklin County, ID': '041',
+        'Fremont County, ID': '043',
+        'Gem County, ID': '045',
+        'Gooding County, ID': '047',
+        'Idaho County, ID': '049',
+        'Jefferson County, ID': '051',
+        'Jerome County, ID': '053',
+        'Kootenai County, ID': '055',
+        'Latah County, ID': '057',
+        'Lemhi County, ID': '059',
+        'Lewis County, ID': '061',
+        'Lincoln County, ID': '063',
+        'Madison County, ID': '065',
+        'Minidoka County, ID': '067',
+        'Nez Perce County, ID': '069',
+        'Oneida County, ID': '071',
+        'Owyhee County, ID': '073',
+        'Payette County, ID': '075',
+        'Power County, ID': '077',
+        'Shoshone County, ID': '079',
+        'Teton County, ID': '081',
+        'Twin Falls County, ID': '083',
+        'Valley County, ID': '085',
+        'Washington County, ID': '087'},
+  17: { '--All--': '%',
+        'Adams County, IL': '001',
+        'Alexander County, IL': '003',
+        'Bond County, IL': '005',
+        'Boone County, IL': '007',
+        'Brown County, IL': '009',
+        'Bureau County, IL': '011',
+        'Calhoun County, IL': '013',
+        'Carroll County, IL': '015',
+        'Cass County, IL': '017',
+        'Champaign County, IL': '019',
+        'Christian County, IL': '021',
+        'Clark County, IL': '023',
+        'Clay County, IL': '025',
+        'Clinton County, IL': '027',
+        'Coles County, IL': '029',
+        'Cook County, IL': '031',
+        'Crawford County, IL': '033',
+        'Cumberland County, IL': '035',
+        'De Witt County, IL': '039',
+        'DeKalb County, IL': '037',
+        'Douglas County, IL': '041',
+        'DuPage County, IL': '043',
+        'Edgar County, IL': '045',
+        'Edwards County, IL': '047',
+        'Effingham County, IL': '049',
+        'Fayette County, IL': '051',
+        'Ford County, IL': '053',
+        'Franklin County, IL': '055',
+        'Fulton County, IL': '057',
+        'Gallatin County, IL': '059',
+        'Greene County, IL': '061',
+        'Grundy County, IL': '063',
+        'Hamilton County, IL': '065',
+        'Hancock County, IL': '067',
+        'Hardin County, IL': '069',
+        'Henderson County, IL': '071',
+        'Henry County, IL': '073',
+        'Iroquois County, IL': '075',
+        'Jackson County, IL': '077',
+        'Jasper County, IL': '079',
+        'Jefferson County, IL': '081',
+        'Jersey County, IL': '083',
+        'Jo Daviess County, IL': '085',
+        'Johnson County, IL': '087',
+        'Kane County, IL': '089',
+        'Kankakee County, IL': '091',
+        'Kendall County, IL': '093',
+        'Knox County, IL': '095',
+        'La Salle County, IL': '099',
+        'Lake County, IL': '097',
+        'Lawrence County, IL': '101',
+        'Lee County, IL': '103',
+        'Livingston County, IL': '105',
+        'Logan County, IL': '107',
+        'Macon County, IL': '115',
+        'Macoupin County, IL': '117',
+        'Madison County, IL': '119',
+        'Marion County, IL': '121',
+        'Marshall County, IL': '123',
+        'Mason County, IL': '125',
+        'Massac County, IL': '127',
+        'McDonough County, IL': '109',
+        'McHenry County, IL': '111',
+        'McLean County, IL': '113',
+        'Menard County, IL': '129',
+        'Mercer County, IL': '131',
+        'Monroe County, IL': '133',
+        'Montgomery County, IL': '135',
+        'Morgan County, IL': '137',
+        'Moultrie County, IL': '139',
+        'Ogle County, IL': '141',
+        'Peoria County, IL': '143',
+        'Perry County, IL': '145',
+        'Piatt County, IL': '147',
+        'Pike County, IL': '149',
+        'Pope County, IL': '151',
+        'Pulaski County, IL': '153',
+        'Putnam County, IL': '155',
+        'Randolph County, IL': '157',
+        'Richland County, IL': '159',
+        'Rock Island County, IL': '161',
+        'Saline County, IL': '165',
+        'Sangamon County, IL': '167',
+        'Schuyler County, IL': '169',
+        'Scott County, IL': '171',
+        'Shelby County, IL': '173',
+        'St. Clair County, IL': '163',
+        'Stark County, IL': '175',
+        'Stephenson County, IL': '177',
+        'Tazewell County, IL': '179',
+        'Union County, IL': '181',
+        'Vermilion County, IL': '183',
+        'Wabash County, IL': '185',
+        'Warren County, IL': '187',
+        'Washington County, IL': '189',
+        'Wayne County, IL': '191',
+        'White County, IL': '193',
+        'Whiteside County, IL': '195',
+        'Will County, IL': '197',
+        'Williamson County, IL': '199',
+        'Winnebago County, IL': '201',
+        'Woodford County, IL': '203'},
+  18: { '--All--': '%',
+        'Adams County, IN': '001',
+        'Allen County, IN': '003',
+        'Bartholomew County, IN': '005',
+        'Benton County, IN': '007',
+        'Blackford County, IN': '009',
+        'Boone County, IN': '011',
+        'Brown County, IN': '013',
+        'Carroll County, IN': '015',
+        'Cass County, IN': '017',
+        'Clark County, IN': '019',
+        'Clay County, IN': '021',
+        'Clinton County, IN': '023',
+        'Crawford County, IN': '025',
+        'Daviess County, IN': '027',
+        'DeKalb County, IN': '033',
+        'Dearborn County, IN': '029',
+        'Decatur County, IN': '031',
+        'Delaware County, IN': '035',
+        'Dubois County, IN': '037',
+        'Elkhart County, IN': '039',
+        'Fayette County, IN': '041',
+        'Floyd County, IN': '043',
+        'Fountain County, IN': '045',
+        'Franklin County, IN': '047',
+        'Fulton County, IN': '049',
+        'Gibson County, IN': '051',
+        'Grant County, IN': '053',
+        'Greene County, IN': '055',
+        'Hamilton County, IN': '057',
+        'Hancock County, IN': '059',
+        'Harrison County, IN': '061',
+        'Hendricks County, IN': '063',
+        'Henry County, IN': '065',
+        'Howard County, IN': '067',
+        'Huntington County, IN': '069',
+        'Jackson County, IN': '071',
+        'Jasper County, IN': '073',
+        'Jay County, IN': '075',
+        'Jefferson County, IN': '077',
+        'Jennings County, IN': '079',
+        'Johnson County, IN': '081',
+        'Knox County, IN': '083',
+        'Kosciusko County, IN': '085',
+        'LaGrange County, IN': '087',
+        'LaPorte County, IN': '091',
+        'Lake County, IN': '089',
+        'Lawrence County, IN': '093',
+        'Madison County, IN': '095',
+        'Marion County, IN': '097',
+        'Marshall County, IN': '099',
+        'Martin County, IN': '101',
+        'Miami County, IN': '103',
+        'Monroe County, IN': '105',
+        'Montgomery County, IN': '107',
+        'Morgan County, IN': '109',
+        'Newton County, IN': '111',
+        'Noble County, IN': '113',
+        'Ohio County, IN': '115',
+        'Orange County, IN': '117',
+        'Owen County, IN': '119',
+        'Parke County, IN': '121',
+        'Perry County, IN': '123',
+        'Pike County, IN': '125',
+        'Porter County, IN': '127',
+        'Posey County, IN': '129',
+        'Pulaski County, IN': '131',
+        'Putnam County, IN': '133',
+        'Randolph County, IN': '135',
+        'Ripley County, IN': '137',
+        'Rush County, IN': '139',
+        'Scott County, IN': '143',
+        'Shelby County, IN': '145',
+        'Spencer County, IN': '147',
+        'St. Joseph County, IN': '141',
+        'Starke County, IN': '149',
+        'Steuben County, IN': '151',
+        'Sullivan County, IN': '153',
+        'Switzerland County, IN': '155',
+        'Tippecanoe County, IN': '157',
+        'Tipton County, IN': '159',
+        'Union County, IN': '161',
+        'Vanderburgh County, IN': '163',
+        'Vermillion County, IN': '165',
+        'Vigo County, IN': '167',
+        'Wabash County, IN': '169',
+        'Warren County, IN': '171',
+        'Warrick County, IN': '173',
+        'Washington County, IN': '175',
+        'Wayne County, IN': '177',
+        'Wells County, IN': '179',
+        'White County, IN': '181',
+        'Whitley County, IN': '183'},
+  19: { '--All--': '%',
+        'Adair County, IA': '001',
+        'Adams County, IA': '003',
+        'Allamakee County, IA': '005',
+        'Appanoose County, IA': '007',
+        'Audubon County, IA': '009',
+        'Benton County, IA': '011',
+        'Black Hawk County, IA': '013',
+        'Boone County, IA': '015',
+        'Bremer County, IA': '017',
+        'Buchanan County, IA': '019',
+        'Buena Vista County, IA': '021',
+        'Butler County, IA': '023',
+        'Calhoun County, IA': '025',
+        'Carroll County, IA': '027',
+        'Cass County, IA': '029',
+        'Cedar County, IA': '031',
+        'Cerro Gordo County, IA': '033',
+        'Cherokee County, IA': '035',
+        'Chickasaw County, IA': '037',
+        'Clarke County, IA': '039',
+        'Clay County, IA': '041',
+        'Clayton County, IA': '043',
+        'Clinton County, IA': '045',
+        'Crawford County, IA': '047',
+        'Dallas County, IA': '049',
+        'Davis County, IA': '051',
+        'Decatur County, IA': '053',
+        'Delaware County, IA': '055',
+        'Des Moines County, IA': '057',
+        'Dickinson County, IA': '059',
+        'Dubuque County, IA': '061',
+        'Emmet County, IA': '063',
+        'Fayette County, IA': '065',
+        'Floyd County, IA': '067',
+        'Franklin County, IA': '069',
+        'Fremont County, IA': '071',
+        'Greene County, IA': '073',
+        'Grundy County, IA': '075',
+        'Guthrie County, IA': '077',
+        'Hamilton County, IA': '079',
+        'Hancock County, IA': '081',
+        'Hardin County, IA': '083',
+        'Harrison County, IA': '085',
+        'Henry County, IA': '087',
+        'Howard County, IA': '089',
+        'Humboldt County, IA': '091',
+        'Ida County, IA': '093',
+        'Iowa County, IA': '095',
+        'Jackson County, IA': '097',
+        'Jasper County, IA': '099',
+        'Jefferson County, IA': '101',
+        'Johnson County, IA': '103',
+        'Jones County, IA': '105',
+        'Keokuk County, IA': '107',
+        'Kossuth County, IA': '109',
+        'Lee County, IA': '111',
+        'Linn County, IA': '113',
+        'Louisa County, IA': '115',
+        'Lucas County, IA': '117',
+        'Lyon County, IA': '119',
+        'Madison County, IA': '121',
+        'Mahaska County, IA': '123',
+        'Marion County, IA': '125',
+        'Marshall County, IA': '127',
+        'Mills County, IA': '129',
+        'Mitchell County, IA': '131',
+        'Monona County, IA': '133',
+        'Monroe County, IA': '135',
+        'Montgomery County, IA': '137',
+        'Muscatine County, IA': '139',
+        "O'Brien County, IA": '141',
+        'Osceola County, IA': '143',
+        'Page County, IA': '145',
+        'Palo Alto County, IA': '147',
+        'Plymouth County, IA': '149',
+        'Pocahontas County, IA': '151',
+        'Polk County, IA': '153',
+        'Pottawattamie County, IA': '155',
+        'Poweshiek County, IA': '157',
+        'Ringgold County, IA': '159',
+        'Sac County, IA': '161',
+        'Scott County, IA': '163',
+        'Shelby County, IA': '165',
+        'Sioux County, IA': '167',
+        'Story County, IA': '169',
+        'Tama County, IA': '171',
+        'Taylor County, IA': '173',
+        'Union County, IA': '175',
+        'Van Buren County, IA': '177',
+        'Wapello County, IA': '179',
+        'Warren County, IA': '181',
+        'Washington County, IA': '183',
+        'Wayne County, IA': '185',
+        'Webster County, IA': '187',
+        'Winnebago County, IA': '189',
+        'Winneshiek County, IA': '191',
+        'Woodbury County, IA': '193',
+        'Worth County, IA': '195',
+        'Wright County, IA': '197'},
+  20: { '--All--': '%',
+        'Allen County, KS': '001',
+        'Anderson County, KS': '003',
+        'Atchison County, KS': '005',
+        'Barber County, KS': '007',
+        'Barton County, KS': '009',
+        'Bourbon County, KS': '011',
+        'Brown County, KS': '013',
+        'Butler County, KS': '015',
+        'Chase County, KS': '017',
+        'Chautauqua County, KS': '019',
+        'Cherokee County, KS': '021',
+        'Cheyenne County, KS': '023',
+        'Clark County, KS': '025',
+        'Clay County, KS': '027',
+        'Cloud County, KS': '029',
+        'Coffey County, KS': '031',
+        'Comanche County, KS': '033',
+        'Cowley County, KS': '035',
+        'Crawford County, KS': '037',
+        'Decatur County, KS': '039',
+        'Dickinson County, KS': '041',
+        'Doniphan County, KS': '043',
+        'Douglas County, KS': '045',
+        'Edwards County, KS': '047',
+        'Elk County, KS': '049',
+        'Ellis County, KS': '051',
+        'Ellsworth County, KS': '053',
+        'Finney County, KS': '055',
+        'Ford County, KS': '057',
+        'Franklin County, KS': '059',
+        'Geary County, KS': '061',
+        'Gove County, KS': '063',
+        'Graham County, KS': '065',
+        'Grant County, KS': '067',
+        'Gray County, KS': '069',
+        'Greeley County, KS': '071',
+        'Greenwood County, KS': '073',
+        'Hamilton County, KS': '075',
+        'Harper County, KS': '077',
+        'Harvey County, KS': '079',
+        'Haskell County, KS': '081',
+        'Hodgeman County, KS': '083',
+        'Jackson County, KS': '085',
+        'Jefferson County, KS': '087',
+        'Jewell County, KS': '089',
+        'Johnson County, KS': '091',
+        'Kearny County, KS': '093',
+        'Kingman County, KS': '095',
+        'Kiowa County, KS': '097',
+        'Labette County, KS': '099',
+        'Lane County, KS': '101',
+        'Leavenworth County, KS': '103',
+        'Lincoln County, KS': '105',
+        'Linn County, KS': '107',
+        'Logan County, KS': '109',
+        'Lyon County, KS': '111',
+        'Marion County, KS': '115',
+        'Marshall County, KS': '117',
+        'McPherson County, KS': '113',
+        'Meade County, KS': '119',
+        'Miami County, KS': '121',
+        'Mitchell County, KS': '123',
+        'Montgomery County, KS': '125',
+        'Morris County, KS': '127',
+        'Morton County, KS': '129',
+        'Nemaha County, KS': '131',
+        'Neosho County, KS': '133',
+        'Ness County, KS': '135',
+        'Norton County, KS': '137',
+        'Osage County, KS': '139',
+        'Osborne County, KS': '141',
+        'Ottawa County, KS': '143',
+        'Pawnee County, KS': '145',
+        'Phillips County, KS': '147',
+        'Pottawatomie County, KS': '149',
+        'Pratt County, KS': '151',
+        'Rawlins County, KS': '153',
+        'Reno County, KS': '155',
+        'Republic County, KS': '157',
+        'Rice County, KS': '159',
+        'Riley County, KS': '161',
+        'Rooks County, KS': '163',
+        'Rush County, KS': '165',
+        'Russell County, KS': '167',
+        'Saline County, KS': '169',
+        'Scott County, KS': '171',
+        'Sedgwick County, KS': '173',
+        'Seward County, KS': '175',
+        'Shawnee County, KS': '177',
+        'Sheridan County, KS': '179',
+        'Sherman County, KS': '181',
+        'Smith County, KS': '183',
+        'Stafford County, KS': '185',
+        'Stanton County, KS': '187',
+        'Stevens County, KS': '189',
+        'Sumner County, KS': '191',
+        'Thomas County, KS': '193',
+        'Trego County, KS': '195',
+        'Wabaunsee County, KS': '197',
+        'Wallace County, KS': '199',
+        'Washington County, KS': '201',
+        'Wichita County, KS': '203',
+        'Wilson County, KS': '205',
+        'Woodson County, KS': '207',
+        'Wyandotte County, KS': '209'},
+  21: { '--All--': '%',
+        'Adair County, KY': '001',
+        'Allen County, KY': '003',
+        'Anderson County, KY': '005',
+        'Ballard County, KY': '007',
+        'Barren County, KY': '009',
+        'Bath County, KY': '011',
+        'Bell County, KY': '013',
+        'Boone County, KY': '015',
+        'Bourbon County, KY': '017',
+        'Boyd County, KY': '019',
+        'Boyle County, KY': '021',
+        'Bracken County, KY': '023',
+        'Breathitt County, KY': '025',
+        'Breckinridge County, KY': '027',
+        'Bullitt County, KY': '029',
+        'Butler County, KY': '031',
+        'Caldwell County, KY': '033',
+        'Calloway County, KY': '035',
+        'Campbell County, KY': '037',
+        'Carlisle County, KY': '039',
+        'Carroll County, KY': '041',
+        'Carter County, KY': '043',
+        'Casey County, KY': '045',
+        'Christian County, KY': '047',
+        'Clark County, KY': '049',
+        'Clay County, KY': '051',
+        'Clinton County, KY': '053',
+        'Crittenden County, KY': '055',
+        'Cumberland County, KY': '057',
+        'Daviess County, KY': '059',
+        'Edmonson County, KY': '061',
+        'Elliott County, KY': '063',
+        'Estill County, KY': '065',
+        'Fayette County, KY': '067',
+        'Fleming County, KY': '069',
+        'Floyd County, KY': '071',
+        'Franklin County, KY': '073',
+        'Fulton County, KY': '075',
+        'Gallatin County, KY': '077',
+        'Garrard County, KY': '079',
+        'Grant County, KY': '081',
+        'Graves County, KY': '083',
+        'Grayson County, KY': '085',
+        'Green County, KY': '087',
+        'Greenup County, KY': '089',
+        'Hancock County, KY': '091',
+        'Hardin County, KY': '093',
+        'Harlan County, KY': '095',
+        'Harrison County, KY': '097',
+        'Hart County, KY': '099',
+        'Henderson County, KY': '101',
+        'Henry County, KY': '103',
+        'Hickman County, KY': '105',
+        'Hopkins County, KY': '107',
+        'Jackson County, KY': '109',
+        'Jefferson County, KY': '111',
+        'Jessamine County, KY': '113',
+        'Johnson County, KY': '115',
+        'Kenton County, KY': '117',
+        'Knott County, KY': '119',
+        'Knox County, KY': '121',
+        'Larue County, KY': '123',
+        'Laurel County, KY': '125',
+        'Lawrence County, KY': '127',
+        'Lee County, KY': '129',
+        'Leslie County, KY': '131',
+        'Letcher County, KY': '133',
+        'Lewis County, KY': '135',
+        'Lincoln County, KY': '137',
+        'Livingston County, KY': '139',
+        'Logan County, KY': '141',
+        'Lyon County, KY': '143',
+        'Madison County, KY': '151',
+        'Magoffin County, KY': '153',
+        'Marion County, KY': '155',
+        'Marshall County, KY': '157',
+        'Martin County, KY': '159',
+        'Mason County, KY': '161',
+        'McCracken County, KY': '145',
+        'McCreary County, KY': '147',
+        'McLean County, KY': '149',
+        'Meade County, KY': '163',
+        'Menifee County, KY': '165',
+        'Mercer County, KY': '167',
+        'Metcalfe County, KY': '169',
+        'Monroe County, KY': '171',
+        'Montgomery County, KY': '173',
+        'Morgan County, KY': '175',
+        'Muhlenberg County, KY': '177',
+        'Nelson County, KY': '179',
+        'Nicholas County, KY': '181',
+        'Ohio County, KY': '183',
+        'Oldham County, KY': '185',
+        'Owen County, KY': '187',
+        'Owsley County, KY': '189',
+        'Pendleton County, KY': '191',
+        'Perry County, KY': '193',
+        'Pike County, KY': '195',
+        'Powell County, KY': '197',
+        'Pulaski County, KY': '199',
+        'Robertson County, KY': '201',
+        'Rockcastle County, KY': '203',
+        'Rowan County, KY': '205',
+        'Russell County, KY': '207',
+        'Scott County, KY': '209',
+        'Shelby County, KY': '211',
+        'Simpson County, KY': '213',
+        'Spencer County, KY': '215',
+        'Taylor County, KY': '217',
+        'Todd County, KY': '219',
+        'Trigg County, KY': '221',
+        'Trimble County, KY': '223',
+        'Union County, KY': '225',
+        'Warren County, KY': '227',
+        'Washington County, KY': '229',
+        'Wayne County, KY': '231',
+        'Webster County, KY': '233',
+        'Whitley County, KY': '235',
+        'Wolfe County, KY': '237',
+        'Woodford County, KY': '239'},
+  22: { '--All--': '%',
+        'Acadia Parish, LA': '001',
+        'Allen Parish, LA': '003',
+        'Ascension Parish, LA': '005',
+        'Assumption Parish, LA': '007',
+        'Avoyelles Parish, LA': '009',
+        'Beauregard Parish, LA': '011',
+        'Bienville Parish, LA': '013',
+        'Bossier Parish, LA': '015',
+        'Caddo Parish, LA': '017',
+        'Calcasieu Parish, LA': '019',
+        'Caldwell Parish, LA': '021',
+        'Cameron Parish, LA': '023',
+        'Catahoula Parish, LA': '025',
+        'Claiborne Parish, LA': '027',
+        'Concordia Parish, LA': '029',
+        'De Soto Parish, LA': '031',
+        'East Baton Rouge Parish, LA': '033',
+        'East Carroll Parish, LA': '035',
+        'East Feliciana Parish, LA': '037',
+        'Evangeline Parish, LA': '039',
+        'Franklin Parish, LA': '041',
+        'Grant Parish, LA': '043',
+        'Iberia Parish, LA': '045',
+        'Iberville Parish, LA': '047',
+        'Jackson Parish, LA': '049',
+        'Jefferson Davis Parish, LA': '053',
+        'Jefferson Parish, LA': '051',
+        'La Salle Parish, LA': '059',
+        'Lafayette Parish, LA': '055',
+        'Lafourche Parish, LA': '057',
+        'Lincoln Parish, LA': '061',
+        'Livingston Parish, LA': '063',
+        'Madison Parish, LA': '065',
+        'Morehouse Parish, LA': '067',
+        'Natchitoches Parish, LA': '069',
+        'Orleans Parish, LA': '071',
+        'Ouachita Parish, LA': '073',
+        'Plaquemines Parish, LA': '075',
+        'Pointe Coupee Parish, LA': '077',
+        'Rapides Parish, LA': '079',
+        'Red River Parish, LA': '081',
+        'Richland Parish, LA': '083',
+        'Sabine Parish, LA': '085',
+        'St. Bernard Parish, LA': '087',
+        'St. Charles Parish, LA': '089',
+        'St. Helena Parish, LA': '091',
+        'St. James Parish, LA': '093',
+        'St. John the Baptist Parish, LA': '095',
+        'St. Landry Parish, LA': '097',
+        'St. Martin Parish, LA': '099',
+        'St. Mary Parish, LA': '101',
+        'St. Tammany Parish, LA': '103',
+        'Tangipahoa Parish, LA': '105',
+        'Tensas Parish, LA': '107',
+        'Terrebonne Parish, LA': '109',
+        'Union Parish, LA': '111',
+        'Vermilion Parish, LA': '113',
+        'Vernon Parish, LA': '115',
+        'Washington Parish, LA': '117',
+        'Webster Parish, LA': '119',
+        'West Baton Rouge Parish, LA': '121',
+        'West Carroll Parish, LA': '123',
+        'West Feliciana Parish, LA': '125',
+        'Winn Parish, LA': '127'},
+  23: { '--All--': '%',
+        'Androscoggin County, ME': '001',
+        'Aroostook County, ME': '003',
+        'Cumberland County, ME': '005',
+        'Franklin County, ME': '007',
+        'Hancock County, ME': '009',
+        'Kennebec County, ME': '011',
+        'Knox County, ME': '013',
+        'Lincoln County, ME': '015',
+        'Oxford County, ME': '017',
+        'Penobscot County, ME': '019',
+        'Piscataquis County, ME': '021',
+        'Sagadahoc County, ME': '023',
+        'Somerset County, ME': '025',
+        'Waldo County, ME': '027',
+        'Washington County, ME': '029',
+        'York County, ME': '031'},
+  24: { '--All--': '%',
+        'Allegany County, MD': '001',
+        'Anne Arundel County, MD': '003',
+        'Baltimore County, MD': '005',
+        'Baltimore city, MD': '510',
+        'Calvert County, MD': '009',
+        'Caroline County, MD': '011',
+        'Carroll County, MD': '013',
+        'Cecil County, MD': '015',
+        'Charles County, MD': '017',
+        'Dorchester County, MD': '019',
+        'Frederick County, MD': '021',
+        'Garrett County, MD': '023',
+        'Harford County, MD': '025',
+        'Howard County, MD': '027',
+        'Kent County, MD': '029',
+        'Montgomery County, MD': '031',
+        "Prince George's County, MD": '033',
+        "Queen Anne's County, MD": '035',
+        'Somerset County, MD': '039',
+        "St. Mary's County, MD": '037',
+        'Talbot County, MD': '041',
+        'Washington County, MD': '043',
+        'Wicomico County, MD': '045',
+        'Worcester County, MD': '047'},
+  25: { '--All--': '%',
+        'Barnstable County, MA': '001',
+        'Berkshire County, MA': '003',
+        'Bristol County, MA': '005',
+        'Dukes County, MA': '007',
+        'Essex County, MA': '009',
+        'Franklin County, MA': '011',
+        'Hampden County, MA': '013',
+        'Hampshire County, MA': '015',
+        'Middlesex County, MA': '017',
+        'Nantucket County/town, MA': '019',
+        'Norfolk County, MA': '021',
+        'Plymouth County, MA': '023',
+        'Suffolk County, MA': '025',
+        'Worcester County, MA': '027'},
+  26: { '--All--': '%',
+        'Alcona County, MI': '001',
+        'Alger County, MI': '003',
+        'Allegan County, MI': '005',
+        'Alpena County, MI': '007',
+        'Antrim County, MI': '009',
+        'Arenac County, MI': '011',
+        'Baraga County, MI': '013',
+        'Barry County, MI': '015',
+        'Bay County, MI': '017',
+        'Benzie County, MI': '019',
+        'Berrien County, MI': '021',
+        'Branch County, MI': '023',
+        'Calhoun County, MI': '025',
+        'Cass County, MI': '027',
+        'Charlevoix County, MI': '029',
+        'Cheboygan County, MI': '031',
+        'Chippewa County, MI': '033',
+        'Clare County, MI': '035',
+        'Clinton County, MI': '037',
+        'Crawford County, MI': '039',
+        'Delta County, MI': '041',
+        'Dickinson County, MI': '043',
+        'Eaton County, MI': '045',
+        'Emmet County, MI': '047',
+        'Genesee County, MI': '049',
+        'Gladwin County, MI': '051',
+        'Gogebic County, MI': '053',
+        'Grand Traverse County, MI': '055',
+        'Gratiot County, MI': '057',
+        'Hillsdale County, MI': '059',
+        'Houghton County, MI': '061',
+        'Huron County, MI': '063',
+        'Ingham County, MI': '065',
+        'Ionia County, MI': '067',
+        'Iosco County, MI': '069',
+        'Iron County, MI': '071',
+        'Isabella County, MI': '073',
+        'Jackson County, MI': '075',
+        'Kalamazoo County, MI': '077',
+        'Kalkaska County, MI': '079',
+        'Kent County, MI': '081',
+        'Keweenaw County, MI': '083',
+        'Lake County, MI': '085',
+        'Lapeer County, MI': '087',
+        'Leelanau County, MI': '089',
+        'Lenawee County, MI': '091',
+        'Livingston County, MI': '093',
+        'Luce County, MI': '095',
+        'Mackinac County, MI': '097',
+        'Macomb County, MI': '099',
+        'Manistee County, MI': '101',
+        'Marquette County, MI': '103',
+        'Mason County, MI': '105',
+        'Mecosta County, MI': '107',
+        'Menominee County, MI': '109',
+        'Midland County, MI': '111',
+        'Missaukee County, MI': '113',
+        'Monroe County, MI': '115',
+        'Montcalm County, MI': '117',
+        'Montmorency County, MI': '119',
+        'Muskegon County, MI': '121',
+        'Newaygo County, MI': '123',
+        'Oakland County, MI': '125',
+        'Oceana County, MI': '127',
+        'Ogemaw County, MI': '129',
+        'Ontonagon County, MI': '131',
+        'Osceola County, MI': '133',
+        'Oscoda County, MI': '135',
+        'Otsego County, MI': '137',
+        'Ottawa County, MI': '139',
+        'Presque Isle County, MI': '141',
+        'Roscommon County, MI': '143',
+        'Saginaw County, MI': '145',
+        'Sanilac County, MI': '151',
+        'Schoolcraft County, MI': '153',
+        'Shiawassee County, MI': '155',
+        'St. Clair County, MI': '147',
+        'St. Joseph County, MI': '149',
+        'Tuscola County, MI': '157',
+        'Van Buren County, MI': '159',
+        'Washtenaw County, MI': '161',
+        'Wayne County, MI': '163',
+        'Wexford County, MI': '165'},
+  27: { '--All--': '%',
+        'Aitkin County, MN': '001',
+        'Anoka County, MN': '003',
+        'Becker County, MN': '005',
+        'Beltrami County, MN': '007',
+        'Benton County, MN': '009',
+        'Big Stone County, MN': '011',
+        'Blue Earth County, MN': '013',
+        'Brown County, MN': '015',
+        'Carlton County, MN': '017',
+        'Carver County, MN': '019',
+        'Cass County, MN': '021',
+        'Chippewa County, MN': '023',
+        'Chisago County, MN': '025',
+        'Clay County, MN': '027',
+        'Clearwater County, MN': '029',
+        'Cook County, MN': '031',
+        'Cottonwood County, MN': '033',
+        'Crow Wing County, MN': '035',
+        'Dakota County, MN': '037',
+        'Dodge County, MN': '039',
+        'Douglas County, MN': '041',
+        'Faribault County, MN': '043',
+        'Fillmore County, MN': '045',
+        'Freeborn County, MN': '047',
+        'Goodhue County, MN': '049',
+        'Grant County, MN': '051',
+        'Hennepin County, MN': '053',
+        'Houston County, MN': '055',
+        'Hubbard County, MN': '057',
+        'Isanti County, MN': '059',
+        'Itasca County, MN': '061',
+        'Jackson County, MN': '063',
+        'Kanabec County, MN': '065',
+        'Kandiyohi County, MN': '067',
+        'Kittson County, MN': '069',
+        'Koochiching County, MN': '071',
+        'Lac qui Parle County, MN': '073',
+        'Lake County, MN': '075',
+        'Lake of the Woods County, MN': '077',
+        'Le Sueur County, MN': '079',
+        'Lincoln County, MN': '081',
+        'Lyon County, MN': '083',
+        'Mahnomen County, MN': '087',
+        'Marshall County, MN': '089',
+        'Martin County, MN': '091',
+        'McLeod County, MN': '085',
+        'Meeker County, MN': '093',
+        'Mille Lacs County, MN': '095',
+        'Morrison County, MN': '097',
+        'Mower County, MN': '099',
+        'Murray County, MN': '101',
+        'Nicollet County, MN': '103',
+        'Nobles County, MN': '105',
+        'Norman County, MN': '107',
+        'Olmsted County, MN': '109',
+        'Otter Tail County, MN': '111',
+        'Pennington County, MN': '113',
+        'Pine County, MN': '115',
+        'Pipestone County, MN': '117',
+        'Polk County, MN': '119',
+        'Pope County, MN': '121',
+        'Ramsey County, MN': '123',
+        'Red Lake County, MN': '125',
+        'Redwood County, MN': '127',
+        'Renville County, MN': '129',
+        'Rice County, MN': '131',
+        'Rock County, MN': '133',
+        'Roseau County, MN': '135',
+        'Scott County, MN': '139',
+        'Sherburne County, MN': '141',
+        'Sibley County, MN': '143',
+        'St. Louis County, MN': '137',
+        'Stearns County, MN': '145',
+        'Steele County, MN': '147',
+        'Stevens County, MN': '149',
+        'Swift County, MN': '151',
+        'Todd County, MN': '153',
+        'Traverse County, MN': '155',
+        'Wabasha County, MN': '157',
+        'Wadena County, MN': '159',
+        'Waseca County, MN': '161',
+        'Washington County, MN': '163',
+        'Watonwan County, MN': '165',
+        'Wilkin County, MN': '167',
+        'Winona County, MN': '169',
+        'Wright County, MN': '171',
+        'Yellow Medicine County, MN': '173'},
+  28: { '--All--': '%',
+        'Adams County, MS': '001',
+        'Alcorn County, MS': '003',
+        'Amite County, MS': '005',
+        'Attala County, MS': '007',
+        'Benton County, MS': '009',
+        'Bolivar County, MS': '011',
+        'Calhoun County, MS': '013',
+        'Carroll County, MS': '015',
+        'Chickasaw County, MS': '017',
+        'Choctaw County, MS': '019',
+        'Claiborne County, MS': '021',
+        'Clarke County, MS': '023',
+        'Clay County, MS': '025',
+        'Coahoma County, MS': '027',
+        'Copiah County, MS': '029',
+        'Covington County, MS': '031',
+        'DeSoto County, MS': '033',
+        'Forrest County, MS': '035',
+        'Franklin County, MS': '037',
+        'George County, MS': '039',
+        'Greene County, MS': '041',
+        'Grenada County, MS': '043',
+        'Hancock County, MS': '045',
+        'Harrison County, MS': '047',
+        'Hinds County, MS': '049',
+        'Holmes County, MS': '051',
+        'Humphreys County, MS': '053',
+        'Issaquena County, MS': '055',
+        'Itawamba County, MS': '057',
+        'Jackson County, MS': '059',
+        'Jasper County, MS': '061',
+        'Jefferson County, MS': '063',
+        'Jefferson Davis County, MS': '065',
+        'Jones County, MS': '067',
+        'Kemper County, MS': '069',
+        'Lafayette County, MS': '071',
+        'Lamar County, MS': '073',
+        'Lauderdale County, MS': '075',
+        'Lawrence County, MS': '077',
+        'Leake County, MS': '079',
+        'Lee County, MS': '081',
+        'Leflore County, MS': '083',
+        'Lincoln County, MS': '085',
+        'Lowndes County, MS': '087',
+        'Madison County, MS': '089',
+        'Marion County, MS': '091',
+        'Marshall County, MS': '093',
+        'Monroe County, MS': '095',
+        'Montgomery County, MS': '097',
+        'Neshoba County, MS': '099',
+        'Newton County, MS': '101',
+        'Noxubee County, MS': '103',
+        'Oktibbeha County, MS': '105',
+        'Panola County, MS': '107',
+        'Pearl River County, MS': '109',
+        'Perry County, MS': '111',
+        'Pike County, MS': '113',
+        'Pontotoc County, MS': '115',
+        'Prentiss County, MS': '117',
+        'Quitman County, MS': '119',
+        'Rankin County, MS': '121',
+        'Scott County, MS': '123',
+        'Sharkey County, MS': '125',
+        'Simpson County, MS': '127',
+        'Smith County, MS': '129',
+        'Stone County, MS': '131',
+        'Sunflower County, MS': '133',
+        'Tallahatchie County, MS': '135',
+        'Tate County, MS': '137',
+        'Tippah County, MS': '139',
+        'Tishomingo County, MS': '141',
+        'Tunica County, MS': '143',
+        'Union County, MS': '145',
+        'Walthall County, MS': '147',
+        'Warren County, MS': '149',
+        'Washington County, MS': '151',
+        'Wayne County, MS': '153',
+        'Webster County, MS': '155',
+        'Wilkinson County, MS': '157',
+        'Winston County, MS': '159',
+        'Yalobusha County, MS': '161',
+        'Yazoo County, MS': '163'},
+  29: { '--All--': '%',
+        'Adair County, MO': '001',
+        'Andrew County, MO': '003',
+        'Atchison County, MO': '005',
+        'Audrain County, MO': '007',
+        'Barry County, MO': '009',
+        'Barton County, MO': '011',
+        'Bates County, MO': '013',
+        'Benton County, MO': '015',
+        'Bollinger County, MO': '017',
+        'Boone County, MO': '019',
+        'Buchanan County, MO': '021',
+        'Butler County, MO': '023',
+        'Caldwell County, MO': '025',
+        'Callaway County, MO': '027',
+        'Camden County, MO': '029',
+        'Cape Girardeau County, MO': '031',
+        'Carroll County, MO': '033',
+        'Carter County, MO': '035',
+        'Cass County, MO': '037',
+        'Cedar County, MO': '039',
+        'Chariton County, MO': '041',
+        'Christian County, MO': '043',
+        'Clark County, MO': '045',
+        'Clay County, MO': '047',
+        'Clinton County, MO': '049',
+        'Cole County, MO': '051',
+        'Cooper County, MO': '053',
+        'Crawford County, MO': '055',
+        'Dade County, MO': '057',
+        'Dallas County, MO': '059',
+        'Daviess County, MO': '061',
+        'DeKalb County, MO': '063',
+        'Dent County, MO': '065',
+        'Douglas County, MO': '067',
+        'Dunklin County, MO': '069',
+        'Franklin County, MO': '071',
+        'Gasconade County, MO': '073',
+        'Gentry County, MO': '075',
+        'Greene County, MO': '077',
+        'Grundy County, MO': '079',
+        'Harrison County, MO': '081',
+        'Henry County, MO': '083',
+        'Hickory County, MO': '085',
+        'Holt County, MO': '087',
+        'Howard County, MO': '089',
+        'Howell County, MO': '091',
+        'Iron County, MO': '093',
+        'Jackson County, MO': '095',
+        'Jasper County, MO': '097',
+        'Jefferson County, MO': '099',
+        'Johnson County, MO': '101',
+        'Knox County, MO': '103',
+        'Laclede County, MO': '105',
+        'Lafayette County, MO': '107',
+        'Lawrence County, MO': '109',
+        'Lewis County, MO': '111',
+        'Lincoln County, MO': '113',
+        'Linn County, MO': '115',
+        'Livingston County, MO': '117',
+        'Macon County, MO': '121',
+        'Madison County, MO': '123',
+        'Maries County, MO': '125',
+        'Marion County, MO': '127',
+        'McDonald County, MO': '119',
+        'Mercer County, MO': '129',
+        'Miller County, MO': '131',
+        'Mississippi County, MO': '133',
+        'Moniteau County, MO': '135',
+        'Monroe County, MO': '137',
+        'Montgomery County, MO': '139',
+        'Morgan County, MO': '141',
+        'New Madrid County, MO': '143',
+        'Newton County, MO': '145',
+        'Nodaway County, MO': '147',
+        'Oregon County, MO': '149',
+        'Osage County, MO': '151',
+        'Ozark County, MO': '153',
+        'Pemiscot County, MO': '155',
+        'Perry County, MO': '157',
+        'Pettis County, MO': '159',
+        'Phelps County, MO': '161',
+        'Pike County, MO': '163',
+        'Platte County, MO': '165',
+        'Polk County, MO': '167',
+        'Pulaski County, MO': '169',
+        'Putnam County, MO': '171',
+        'Ralls County, MO': '173',
+        'Randolph County, MO': '175',
+        'Ray County, MO': '177',
+        'Reynolds County, MO': '179',
+        'Ripley County, MO': '181',
+        'Saline County, MO': '195',
+        'Schuyler County, MO': '197',
+        'Scotland County, MO': '199',
+        'Scott County, MO': '201',
+        'Shannon County, MO': '203',
+        'Shelby County, MO': '205',
+        'St. Charles County, MO': '183',
+        'St. Clair County, MO': '185',
+        'St. Francois County, MO': '187',
+        'St. Louis County, MO': '189',
+        'St. Louis city, MO': '510',
+        'Ste. Genevieve County, MO': '186',
+        'Stoddard County, MO': '207',
+        'Stone County, MO': '209',
+        'Sullivan County, MO': '211',
+        'Taney County, MO': '213',
+        'Texas County, MO': '215',
+        'Vernon County, MO': '217',
+        'Warren County, MO': '219',
+        'Washington County, MO': '221',
+        'Wayne County, MO': '223',
+        'Webster County, MO': '225',
+        'Worth County, MO': '227',
+        'Wright County, MO': '229'},
+  30: { '--All--': '%',
+        'Beaverhead County, MT': '001',
+        'Big Horn County, MT': '003',
+        'Blaine County, MT': '005',
+        'Broadwater County, MT': '007',
+        'Carbon County, MT': '009',
+        'Carter County, MT': '011',
+        'Cascade County, MT': '013',
+        'Chouteau County, MT': '015',
+        'Custer County, MT': '017',
+        'Daniels County, MT': '019',
+        'Dawson County, MT': '021',
+        'Deer Lodge County, MT': '023',
+        'Fallon County, MT': '025',
+        'Fergus County, MT': '027',
+        'Flathead County, MT': '029',
+        'Gallatin County, MT': '031',
+        'Garfield County, MT': '033',
+        'Glacier County, MT': '035',
+        'Golden Valley County, MT': '037',
+        'Granite County, MT': '039',
+        'Hill County, MT': '041',
+        'Jefferson County, MT': '043',
+        'Judith Basin County, MT': '045',
+        'Lake County, MT': '047',
+        'Lewis and Clark County, MT': '049',
+        'Liberty County, MT': '051',
+        'Lincoln County, MT': '053',
+        'Madison County, MT': '057',
+        'McCone County, MT': '055',
+        'Meagher County, MT': '059',
+        'Mineral County, MT': '061',
+        'Missoula County, MT': '063',
+        'Musselshell County, MT': '065',
+        'Park County, MT': '067',
+        'Petroleum County, MT': '069',
+        'Phillips County, MT': '071',
+        'Pondera County, MT': '073',
+        'Powder River County, MT': '075',
+        'Powell County, MT': '077',
+        'Prairie County, MT': '079',
+        'Ravalli County, MT': '081',
+        'Richland County, MT': '083',
+        'Roosevelt County, MT': '085',
+        'Rosebud County, MT': '087',
+        'Sanders County, MT': '089',
+        'Sheridan County, MT': '091',
+        'Silver Bow County, MT': '093',
+        'Stillwater County, MT': '095',
+        'Sweet Grass County, MT': '097',
+        'Teton County, MT': '099',
+        'Toole County, MT': '101',
+        'Treasure County, MT': '103',
+        'Valley County, MT': '105',
+        'Wheatland County, MT': '107',
+        'Wibaux County, MT': '109',
+        'Yellowstone County, MT': '111'},
+  31: { '--All--': '%',
+        'Adams County, NE': '001',
+        'Antelope County, NE': '003',
+        'Arthur County, NE': '005',
+        'Banner County, NE': '007',
+        'Blaine County, NE': '009',
+        'Boone County, NE': '011',
+        'Box Butte County, NE': '013',
+        'Boyd County, NE': '015',
+        'Brown County, NE': '017',
+        'Buffalo County, NE': '019',
+        'Burt County, NE': '021',
+        'Butler County, NE': '023',
+        'Cass County, NE': '025',
+        'Cedar County, NE': '027',
+        'Chase County, NE': '029',
+        'Cherry County, NE': '031',
+        'Cheyenne County, NE': '033',
+        'Clay County, NE': '035',
+        'Colfax County, NE': '037',
+        'Cuming County, NE': '039',
+        'Custer County, NE': '041',
+        'Dakota County, NE': '043',
+        'Dawes County, NE': '045',
+        'Dawson County, NE': '047',
+        'Deuel County, NE': '049',
+        'Dixon County, NE': '051',
+        'Dodge County, NE': '053',
+        'Douglas County, NE': '055',
+        'Dundy County, NE': '057',
+        'Fillmore County, NE': '059',
+        'Franklin County, NE': '061',
+        'Frontier County, NE': '063',
+        'Furnas County, NE': '065',
+        'Gage County, NE': '067',
+        'Garden County, NE': '069',
+        'Garfield County, NE': '071',
+        'Gosper County, NE': '073',
+        'Grant County, NE': '075',
+        'Greeley County, NE': '077',
+        'Hall County, NE': '079',
+        'Hamilton County, NE': '081',
+        'Harlan County, NE': '083',
+        'Hayes County, NE': '085',
+        'Hitchcock County, NE': '087',
+        'Holt County, NE': '089',
+        'Hooker County, NE': '091',
+        'Howard County, NE': '093',
+        'Jefferson County, NE': '095',
+        'Johnson County, NE': '097',
+        'Kearney County, NE': '099',
+        'Keith County, NE': '101',
+        'Keya Paha County, NE': '103',
+        'Kimball County, NE': '105',
+        'Knox County, NE': '107',
+        'Lancaster County, NE': '109',
+        'Lincoln County, NE': '111',
+        'Logan County, NE': '113',
+        'Loup County, NE': '115',
+        'Madison County, NE': '119',
+        'McPherson County, NE': '117',
+        'Merrick County, NE': '121',
+        'Morrill County, NE': '123',
+        'Nance County, NE': '125',
+        'Nemaha County, NE': '127',
+        'Nuckolls County, NE': '129',
+        'Otoe County, NE': '131',
+        'Pawnee County, NE': '133',
+        'Perkins County, NE': '135',
+        'Phelps County, NE': '137',
+        'Pierce County, NE': '139',
+        'Platte County, NE': '141',
+        'Polk County, NE': '143',
+        'Red Willow County, NE': '145',
+        'Richardson County, NE': '147',
+        'Rock County, NE': '149',
+        'Saline County, NE': '151',
+        'Sarpy County, NE': '153',
+        'Saunders County, NE': '155',
+        'Scotts Bluff County, NE': '157',
+        'Seward County, NE': '159',
+        'Sheridan County, NE': '161',
+        'Sherman County, NE': '163',
+        'Sioux County, NE': '165',
+        'Stanton County, NE': '167',
+        'Thayer County, NE': '169',
+        'Thomas County, NE': '171',
+        'Thurston County, NE': '173',
+        'Valley County, NE': '175',
+        'Washington County, NE': '177',
+        'Wayne County, NE': '179',
+        'Webster County, NE': '181',
+        'Wheeler County, NE': '183',
+        'York County, NE': '185'},
+  32: { '--All--': '%',
+        'Carson City, NV': '510',
+        'Churchill County, NV': '001',
+        'Clark County, NV': '003',
+        'Douglas County, NV': '005',
+        'Elko County, NV': '007',
+        'Esmeralda County, NV': '009',
+        'Eureka County, NV': '011',
+        'Humboldt County, NV': '013',
+        'Lander County, NV': '015',
+        'Lincoln County, NV': '017',
+        'Lyon County, NV': '019',
+        'Mineral County, NV': '021',
+        'Nye County, NV': '023',
+        'Pershing County, NV': '027',
+        'Storey County, NV': '029',
+        'Washoe County, NV': '031',
+        'White Pine County, NV': '033'},
+  33: { '--All--': '%',
+        'Belknap County, NH': '001',
+        'Carroll County, NH': '003',
+        'Cheshire County, NH': '005',
+        'Coos County, NH': '007',
+        'Grafton County, NH': '009',
+        'Hillsborough County, NH': '011',
+        'Merrimack County, NH': '013',
+        'Rockingham County, NH': '015',
+        'Strafford County, NH': '017',
+        'Sullivan County, NH': '019'},
+  34: { '--All--': '%',
+        'Atlantic County, NJ': '001',
+        'Bergen County, NJ': '003',
+        'Burlington County, NJ': '005',
+        'Camden County, NJ': '007',
+        'Cape May County, NJ': '009',
+        'Cumberland County, NJ': '011',
+        'Essex County, NJ': '013',
+        'Gloucester County, NJ': '015',
+        'Hudson County, NJ': '017',
+        'Hunterdon County, NJ': '019',
+        'Mercer County, NJ': '021',
+        'Middlesex County, NJ': '023',
+        'Monmouth County, NJ': '025',
+        'Morris County, NJ': '027',
+        'Ocean County, NJ': '029',
+        'Passaic County, NJ': '031',
+        'Salem County, NJ': '033',
+        'Somerset County, NJ': '035',
+        'Sussex County, NJ': '037',
+        'Union County, NJ': '039',
+        'Warren County, NJ': '041'},
+  35: { '--All--': '%',
+        'Bernalillo County, NM': '001',
+        'Catron County, NM': '003',
+        'Chaves County, NM': '005',
+        'Cibola County, NM': '006',
+        'Colfax County, NM': '007',
+        'Curry County, NM': '009',
+        'DeBaca County, NM': '011',
+        'Dona Ana County, NM': '013',
+        'Eddy County, NM': '015',
+        'Grant County, NM': '017',
+        'Guadalupe County, NM': '019',
+        'Harding County, NM': '021',
+        'Hidalgo County, NM': '023',
+        'Lea County, NM': '025',
+        'Lincoln County, NM': '027',
+        'Los Alamos County, NM': '028',
+        'Luna County, NM': '029',
+        'McKinley County, NM': '031',
+        'Mora County, NM': '033',
+        'Otero County, NM': '035',
+        'Quay County, NM': '037',
+        'Rio Arriba County, NM': '039',
+        'Roosevelt County, NM': '041',
+        'San Juan County, NM': '045',
+        'San Miguel County, NM': '047',
+        'Sandoval County, NM': '043',
+        'Santa Fe County, NM': '049',
+        'Sierra County, NM': '051',
+        'Socorro County, NM': '053',
+        'Taos County, NM': '055',
+        'Torrance County, NM': '057',
+        'Union County, NM': '059',
+        'Valencia County, NM': '061'},
+  36: { '--All--': '%',
+        'Albany County, NY': '001',
+        'Allegany County, NY': '003',
+        'Bronx County, NY': '005',
+        'Broome County, NY': '007',
+        'Cattaraugus County, NY': '009',
+        'Cayuga County, NY': '011',
+        'Chautauqua County, NY': '013',
+        'Chemung County, NY': '015',
+        'Chenango County, NY': '017',
+        'Clinton County, NY': '019',
+        'Columbia County, NY': '021',
+        'Cortland County, NY': '023',
+        'Delaware County, NY': '025',
+        'Dutchess County, NY': '027',
+        'Erie County, NY': '029',
+        'Essex County, NY': '031',
+        'Franklin County, NY': '033',
+        'Fulton County, NY': '035',
+        'Genesee County, NY': '037',
+        'Greene County, NY': '039',
+        'Hamilton County, NY': '041',
+        'Herkimer County, NY': '043',
+        'Jefferson County, NY': '045',
+        'Kings County, NY': '047',
+        'Lewis County, NY': '049',
+        'Livingston County, NY': '051',
+        'Madison County, NY': '053',
+        'Monroe County, NY': '055',
+        'Montgomery County, NY': '057',
+        'Nassau County, NY': '059',
+        'New York County, NY': '061',
+        'Niagara County, NY': '063',
+        'Oneida County, NY': '065',
+        'Onondaga County, NY': '067',
+        'Ontario County, NY': '069',
+        'Orange County, NY': '071',
+        'Orleans County, NY': '073',
+        'Oswego County, NY': '075',
+        'Otsego County, NY': '077',
+        'Putnam County, NY': '079',
+        'Queens County, NY': '081',
+        'Rensselaer County, NY': '083',
+        'Richmond County, NY': '085',
+        'Rockland County, NY': '087',
+        'Saratoga County, NY': '091',
+        'Schenectady County, NY': '093',
+        'Schoharie County, NY': '095',
+        'Schuyler County, NY': '097',
+        'Seneca County, NY': '099',
+        'St. Lawrence County, NY': '089',
+        'Steuben County, NY': '101',
+        'Suffolk County, NY': '103',
+        'Sullivan County, NY': '105',
+        'Tioga County, NY': '107',
+        'Tompkins County, NY': '109',
+        'Ulster County, NY': '111',
+        'Warren County, NY': '113',
+        'Washington County, NY': '115',
+        'Wayne County, NY': '117',
+        'Westchester County, NY': '119',
+        'Wyoming County, NY': '121',
+        'Yates County, NY': '123'},
+  37: { '--All--': '%',
+        'Alamance County, NC': '001',
+        'Alexander County, NC': '003',
+        'Alleghany County, NC': '005',
+        'Anson County, NC': '007',
+        'Ashe County, NC': '009',
+        'Avery County, NC': '011',
+        'Beaufort County, NC': '013',
+        'Bertie County, NC': '015',
+        'Bladen County, NC': '017',
+        'Brunswick County, NC': '019',
+        'Buncombe County, NC': '021',
+        'Burke County, NC': '023',
+        'Cabarrus County, NC': '025',
+        'Caldwell County, NC': '027',
+        'Camden County, NC': '029',
+        'Carteret County, NC': '031',
+        'Caswell County, NC': '033',
+        'Catawba County, NC': '035',
+        'Chatham County, NC': '037',
+        'Cherokee County, NC': '039',
+        'Chowan County, NC': '041',
+        'Clay County, NC': '043',
+        'Cleveland County, NC': '045',
+        'Columbus County, NC': '047',
+        'Craven County, NC': '049',
+        'Cumberland County, NC': '051',
+        'Currituck County, NC': '053',
+        'Dare County, NC': '055',
+        'Davidson County, NC': '057',
+        'Davie County, NC': '059',
+        'Duplin County, NC': '061',
+        'Durham County, NC': '063',
+        'Edgecombe County, NC': '065',
+        'Forsyth County, NC': '067',
+        'Franklin County, NC': '069',
+        'Gaston County, NC': '071',
+        'Gates County, NC': '073',
+        'Graham County, NC': '075',
+        'Granville County, NC': '077',
+        'Greene County, NC': '079',
+        'Guilford County, NC': '081',
+        'Halifax County, NC': '083',
+        'Harnett County, NC': '085',
+        'Haywood County, NC': '087',
+        'Henderson County, NC': '089',
+        'Hertford County, NC': '091',
+        'Hoke County, NC': '093',
+        'Hyde County, NC': '095',
+        'Iredell County, NC': '097',
+        'Jackson County, NC': '099',
+        'Johnston County, NC': '101',
+        'Jones County, NC': '103',
+        'Lee County, NC': '105',
+        'Lenoir County, NC': '107',
+        'Lincoln County, NC': '109',
+        'Macon County, NC': '113',
+        'Madison County, NC': '115',
+        'Martin County, NC': '117',
+        'McDowell County, NC': '111',
+        'Mecklenburg County, NC': '119',
+        'Mitchell County, NC': '121',
+        'Montgomery County, NC': '123',
+        'Moore County, NC': '125',
+        'Nash County, NC': '127',
+        'New Hanover County, NC': '129',
+        'Northampton County, NC': '131',
+        'Onslow County, NC': '133',
+        'Orange County, NC': '135',
+        'Pamlico County, NC': '137',
+        'Pasquotank County, NC': '139',
+        'Pender County, NC': '141',
+        'Perquimans County, NC': '143',
+        'Person County, NC': '145',
+        'Pitt County, NC': '147',
+        'Polk County, NC': '149',
+        'Randolph County, NC': '151',
+        'Richmond County, NC': '153',
+        'Robeson County, NC': '155',
+        'Rockingham County, NC': '157',
+        'Rowan County, NC': '159',
+        'Rutherford County, NC': '161',
+        'Sampson County, NC': '163',
+        'Scotland County, NC': '165',
+        'Stanly County, NC': '167',
+        'Stokes County, NC': '169',
+        'Surry County, NC': '171',
+        'Swain County, NC': '173',
+        'Transylvania County, NC': '175',
+        'Tyrrell County, NC': '177',
+        'Union County, NC': '179',
+        'Vance County, NC': '181',
+        'Wake County, NC': '183',
+        'Warren County, NC': '185',
+        'Washington County, NC': '187',
+        'Watauga County, NC': '189',
+        'Wayne County, NC': '191',
+        'Wilkes County, NC': '193',
+        'Wilson County, NC': '195',
+        'Yadkin County, NC': '197',
+        'Yancey County, NC': '199'},
+  38: { '--All--': '%',
+        'Adams County, ND': '001',
+        'Barnes County, ND': '003',
+        'Benson County, ND': '005',
+        'Billings County, ND': '007',
+        'Bottineau County, ND': '009',
+        'Bowman County, ND': '011',
+        'Burke County, ND': '013',
+        'Burleigh County, ND': '015',
+        'Cass County, ND': '017',
+        'Cavalier County, ND': '019',
+        'Dickey County, ND': '021',
+        'Divide County, ND': '023',
+        'Dunn County, ND': '025',
+        'Eddy County, ND': '027',
+        'Emmons County, ND': '029',
+        'Foster County, ND': '031',
+        'Golden Valley County, ND': '033',
+        'Grand Forks County, ND': '035',
+        'Grant County, ND': '037',
+        'Griggs County, ND': '039',
+        'Hettinger County, ND': '041',
+        'Kidder County, ND': '043',
+        'LaMoure County, ND': '045',
+        'Logan County, ND': '047',
+        'McHenry County, ND': '049',
+        'McIntosh County, ND': '051',
+        'McKenzie County, ND': '053',
+        'McLean County, ND': '055',
+        'Mercer County, ND': '057',
+        'Morton County, ND': '059',
+        'Mountrail County, ND': '061',
+        'Nelson County, ND': '063',
+        'Oliver County, ND': '065',
+        'Pembina County, ND': '067',
+        'Pierce County, ND': '069',
+        'Ramsey County, ND': '071',
+        'Ransom County, ND': '073',
+        'Renville County, ND': '075',
+        'Richland County, ND': '077',
+        'Rolette County, ND': '079',
+        'Sargent County, ND': '081',
+        'Sheridan County, ND': '083',
+        'Sioux County, ND': '085',
+        'Slope County, ND': '087',
+        'Stark County, ND': '089',
+        'Steele County, ND': '091',
+        'Stutsman County, ND': '093',
+        'Towner County, ND': '095',
+        'Traill County, ND': '097',
+        'Walsh County, ND': '099',
+        'Ward County, ND': '101',
+        'Wells County, ND': '103',
+        'Williams County, ND': '105'},
+  39: { '--All--': '%',
+        'Adams County, OH': '001',
+        'Allen County, OH': '003',
+        'Ashland County, OH': '005',
+        'Ashtabula County, OH': '007',
+        'Athens County, OH': '009',
+        'Auglaize County, OH': '011',
+        'Belmont County, OH': '013',
+        'Brown County, OH': '015',
+        'Butler County, OH': '017',
+        'Carroll County, OH': '019',
+        'Champaign County, OH': '021',
+        'Clark County, OH': '023',
+        'Clermont County, OH': '025',
+        'Clinton County, OH': '027',
+        'Columbiana County, OH': '029',
+        'Coshocton County, OH': '031',
+        'Crawford County, OH': '033',
+        'Cuyahoga County, OH': '035',
+        'Darke County, OH': '037',
+        'Defiance County, OH': '039',
+        'Delaware County, OH': '041',
+        'Erie County, OH': '043',
+        'Fairfield County, OH': '045',
+        'Fayette County, OH': '047',
+        'Franklin County, OH': '049',
+        'Fulton County, OH': '051',
+        'Gallia County, OH': '053',
+        'Geauga County, OH': '055',
+        'Greene County, OH': '057',
+        'Guernsey County, OH': '059',
+        'Hamilton County, OH': '061',
+        'Hancock County, OH': '063',
+        'Hardin County, OH': '065',
+        'Harrison County, OH': '067',
+        'Henry County, OH': '069',
+        'Highland County, OH': '071',
+        'Hocking County, OH': '073',
+        'Holmes County, OH': '075',
+        'Huron County, OH': '077',
+        'Jackson County, OH': '079',
+        'Jefferson County, OH': '081',
+        'Knox County, OH': '083',
+        'Lake County, OH': '085',
+        'Lawrence County, OH': '087',
+        'Licking County, OH': '089',
+        'Logan County, OH': '091',
+        'Lorain County, OH': '093',
+        'Lucas County, OH': '095',
+        'Madison County, OH': '097',
+        'Mahoning County, OH': '099',
+        'Marion County, OH': '101',
+        'Medina County, OH': '103',
+        'Meigs County, OH': '105',
+        'Mercer County, OH': '107',
+        'Miami County, OH': '109',
+        'Monroe County, OH': '111',
+        'Montgomery County, OH': '113',
+        'Morgan County, OH': '115',
+        'Morrow County, OH': '117',
+        'Muskingum County, OH': '119',
+        'Noble County, OH': '121',
+        'Ottawa County, OH': '123',
+        'Paulding County, OH': '125',
+        'Perry County, OH': '127',
+        'Pickaway County, OH': '129',
+        'Pike County, OH': '131',
+        'Portage County, OH': '133',
+        'Preble County, OH': '135',
+        'Putnam County, OH': '137',
+        'Richland County, OH': '139',
+        'Ross County, OH': '141',
+        'Sandusky County, OH': '143',
+        'Scioto County, OH': '145',
+        'Seneca County, OH': '147',
+        'Shelby County, OH': '149',
+        'Stark County, OH': '151',
+        'Summit County, OH': '153',
+        'Trumbull County, OH': '155',
+        'Tuscarawas County, OH': '157',
+        'Union County, OH': '159',
+        'Van Wert County, OH': '161',
+        'Vinton County, OH': '163',
+        'Warren County, OH': '165',
+        'Washington County, OH': '167',
+        'Wayne County, OH': '169',
+        'Williams County, OH': '171',
+        'Wood County, OH': '173',
+        'Wyandot County, OH': '175'},
+  40: { '--All--': '%',
+        'Adair County, OK': '001',
+        'Alfalfa County, OK': '003',
+        'Atoka County, OK': '005',
+        'Beaver County, OK': '007',
+        'Beckham County, OK': '009',
+        'Blaine County, OK': '011',
+        'Bryan County, OK': '013',
+        'Caddo County, OK': '015',
+        'Canadian County, OK': '017',
+        'Carter County, OK': '019',
+        'Cherokee County, OK': '021',
+        'Choctaw County, OK': '023',
+        'Cimarron County, OK': '025',
+        'Cleveland County, OK': '027',
+        'Coal County, OK': '029',
+        'Comanche County, OK': '031',
+        'Cotton County, OK': '033',
+        'Craig County, OK': '035',
+        'Creek County, OK': '037',
+        'Custer County, OK': '039',
+        'Delaware County, OK': '041',
+        'Dewey County, OK': '043',
+        'Ellis County, OK': '045',
+        'Garfield County, OK': '047',
+        'Garvin County, OK': '049',
+        'Grady County, OK': '051',
+        'Grant County, OK': '053',
+        'Greer County, OK': '055',
+        'Harmon County, OK': '057',
+        'Harper County, OK': '059',
+        'Haskell County, OK': '061',
+        'Hughes County, OK': '063',
+        'Jackson County, OK': '065',
+        'Jefferson County, OK': '067',
+        'Johnston County, OK': '069',
+        'Kay County, OK': '071',
+        'Kingfisher County, OK': '073',
+        'Kiowa County, OK': '075',
+        'Latimer County, OK': '077',
+        'Le Flore County, OK': '079',
+        'Lincoln County, OK': '081',
+        'Logan County, OK': '083',
+        'Love County, OK': '085',
+        'Major County, OK': '093',
+        'Marshall County, OK': '095',
+        'Mayes County, OK': '097',
+        'McClain County, OK': '087',
+        'McCurtain County, OK': '089',
+        'McIntosh County, OK': '091',
+        'Murray County, OK': '099',
+        'Muskogee County, OK': '101',
+        'Noble County, OK': '103',
+        'Nowata County, OK': '105',
+        'Okfuskee County, OK': '107',
+        'Oklahoma County, OK': '109',
+        'Okmulgee County, OK': '111',
+        'Osage County, OK': '113',
+        'Ottawa County, OK': '115',
+        'Pawnee County, OK': '117',
+        'Payne County, OK': '119',
+        'Pittsburg County, OK': '121',
+        'Pontotoc County, OK': '123',
+        'Pottawatomie County, OK': '125',
+        'Pushmataha County, OK': '127',
+        'Roger Mills County, OK': '129',
+        'Rogers County, OK': '131',
+        'Seminole County, OK': '133',
+        'Sequoyah County, OK': '135',
+        'Stephens County, OK': '137',
+        'Texas County, OK': '139',
+        'Tillman County, OK': '141',
+        'Tulsa County, OK': '143',
+        'Wagoner County, OK': '145',
+        'Washington County, OK': '147',
+        'Washita County, OK': '149',
+        'Woods County, OK': '151',
+        'Woodward County, OK': '153'},
+  41: { '--All--': '%',
+        'Baker County, OR': '001',
+        'Benton County, OR': '003',
+        'Clackamas County, OR': '005',
+        'Clatsop County, OR': '007',
+        'Columbia County, OR': '009',
+        'Coos County, OR': '011',
+        'Crook County, OR': '013',
+        'Curry County, OR': '015',
+        'Deschutes County, OR': '017',
+        'Douglas County, OR': '019',
+        'Gilliam County, OR': '021',
+        'Grant County, OR': '023',
+        'Harney County, OR': '025',
+        'Hood River County, OR': '027',
+        'Jackson County, OR': '029',
+        'Jefferson County, OR': '031',
+        'Josephine County, OR': '033',
+        'Klamath County, OR': '035',
+        'Lake County, OR': '037',
+        'Lane County, OR': '039',
+        'Lincoln County, OR': '041',
+        'Linn County, OR': '043',
+        'Malheur County, OR': '045',
+        'Marion County, OR': '047',
+        'Morrow County, OR': '049',
+        'Multnomah County, OR': '051',
+        'Polk County, OR': '053',
+        'Sherman County, OR': '055',
+        'Tillamook County, OR': '057',
+        'Umatilla County, OR': '059',
+        'Union County, OR': '061',
+        'Wallowa County, OR': '063',
+        'Wasco County, OR': '065',
+        'Washington County, OR': '067',
+        'Wheeler County, OR': '069',
+        'Yamhill County, OR': '071'},
+  42: { '--All--': '%',
+        'Adams County, PA': '001',
+        'Allegheny County, PA': '003',
+        'Armstrong County, PA': '005',
+        'Beaver County, PA': '007',
+        'Bedford County, PA': '009',
+        'Berks County, PA': '011',
+        'Blair County, PA': '013',
+        'Bradford County, PA': '015',
+        'Bucks County, PA': '017',
+        'Butler County, PA': '019',
+        'Cambria County, PA': '021',
+        'Cameron County, PA': '023',
+        'Carbon County, PA': '025',
+        'Centre County, PA': '027',
+        'Chester County, PA': '029',
+        'Clarion County, PA': '031',
+        'Clearfield County, PA': '033',
+        'Clinton County, PA': '035',
+        'Columbia County, PA': '037',
+        'Crawford County, PA': '039',
+        'Cumberland County, PA': '041',
+        'Dauphin County, PA': '043',
+        'Delaware County, PA': '045',
+        'Elk County, PA': '047',
+        'Erie County, PA': '049',
+        'Fayette County, PA': '051',
+        'Forest County, PA': '053',
+        'Franklin County, PA': '055',
+        'Fulton County, PA': '057',
+        'Greene County, PA': '059',
+        'Huntingdon County, PA': '061',
+        'Indiana County, PA': '063',
+        'Jefferson County, PA': '065',
+        'Juniata County, PA': '067',
+        'Lackawanna County, PA': '069',
+        'Lancaster County, PA': '071',
+        'Lawrence County, PA': '073',
+        'Lebanon County, PA': '075',
+        'Lehigh County, PA': '077',
+        'Luzerne County, PA': '079',
+        'Lycoming County, PA': '081',
+        'McKean County, PA': '083',
+        'Mercer County, PA': '085',
+        'Mifflin County, PA': '087',
+        'Monroe County, PA': '089',
+        'Montgomery County, PA': '091',
+        'Montour County, PA': '093',
+        'Northampton County, PA': '095',
+        'Northumberland County, PA': '097',
+        'Perry County, PA': '099',
+        'Philadelphia County/city, PA': '101',
+        'Pike County, PA': '103',
+        'Potter County, PA': '105',
+        'Schuylkill County, PA': '107',
+        'Snyder County, PA': '109',
+        'Somerset County, PA': '111',
+        'Sullivan County, PA': '113',
+        'Susquehanna County, PA': '115',
+        'Tioga County, PA': '117',
+        'Union County, PA': '119',
+        'Venango County, PA': '121',
+        'Warren County, PA': '123',
+        'Washington County, PA': '125',
+        'Wayne County, PA': '127',
+        'Westmoreland County, PA': '129',
+        'Wyoming County, PA': '131',
+        'York County, PA': '133'},
+  44: { '--All--': '%',
+        'Bristol County, RI': '001',
+        'Kent County, RI': '003',
+        'Newport County, RI': '005',
+        'Providence County, RI': '007',
+        'Washington County, RI': '009'},
+  45: { '--All--': '%',
+        'Abbeville County, SC': '001',
+        'Aiken County, SC': '003',
+        'Allendale County, SC': '005',
+        'Anderson County, SC': '007',
+        'Bamberg County, SC': '009',
+        'Barnwell County, SC': '011',
+        'Beaufort County, SC': '013',
+        'Berkeley County, SC': '015',
+        'Calhoun County, SC': '017',
+        'Charleston County, SC': '019',
+        'Cherokee County, SC': '021',
+        'Chester County, SC': '023',
+        'Chesterfield County, SC': '025',
+        'Clarendon County, SC': '027',
+        'Colleton County, SC': '029',
+        'Darlington County, SC': '031',
+        'Dillon County, SC': '033',
+        'Dorchester County, SC': '035',
+        'Edgefield County, SC': '037',
+        'Fairfield County, SC': '039',
+        'Florence County, SC': '041',
+        'Georgetown County, SC': '043',
+        'Greenville County, SC': '045',
+        'Greenwood County, SC': '047',
+        'Hampton County, SC': '049',
+        'Horry County, SC': '051',
+        'Jasper County, SC': '053',
+        'Kershaw County, SC': '055',
+        'Lancaster County, SC': '057',
+        'Laurens County, SC': '059',
+        'Lee County, SC': '061',
+        'Lexington County, SC': '063',
+        'Marion County, SC': '067',
+        'Marlboro County, SC': '069',
+        'McCormick County, SC': '065',
+        'Newberry County, SC': '071',
+        'Oconee County, SC': '073',
+        'Orangeburg County, SC': '075',
+        'Pickens County, SC': '077',
+        'Richland County, SC': '079',
+        'Saluda County, SC': '081',
+        'Spartanburg County, SC': '083',
+        'Sumter County, SC': '085',
+        'Union County, SC': '087',
+        'Williamsburg County, SC': '089',
+        'York County, SC': '091'},
+  46: { '--All--': '%',
+        'Aurora County, SD': '003',
+        'Beadle County, SD': '005',
+        'Bennett County, SD': '007',
+        'Bon Homme County, SD': '009',
+        'Brookings County, SD': '011',
+        'Brown County, SD': '013',
+        'Brule County, SD': '015',
+        'Buffalo County, SD': '017',
+        'Butte County, SD': '019',
+        'Campbell County, SD': '021',
+        'Charles Mix County, SD': '023',
+        'Clark County, SD': '025',
+        'Clay County, SD': '027',
+        'Codington County, SD': '029',
+        'Corson County, SD': '031',
+        'Custer County, SD': '033',
+        'Davison County, SD': '035',
+        'Day County, SD': '037',
+        'Deuel County, SD': '039',
+        'Dewey County, SD': '041',
+        'Douglas County, SD': '043',
+        'Edmunds County, SD': '045',
+        'Fall River County, SD': '047',
+        'Faulk County, SD': '049',
+        'Grant County, SD': '051',
+        'Gregory County, SD': '053',
+        'Haakon County, SD': '055',
+        'Hamlin County, SD': '057',
+        'Hand County, SD': '059',
+        'Hanson County, SD': '061',
+        'Harding County, SD': '063',
+        'Hughes County, SD': '065',
+        'Hutchinson County, SD': '067',
+        'Hyde County, SD': '069',
+        'Jackson County, SD': '071',
+        'Jerauld County, SD': '073',
+        'Jones County, SD': '075',
+        'Kingsbury County, SD': '077',
+        'Lake County, SD': '079',
+        'Lawrence County, SD': '081',
+        'Lincoln County, SD': '083',
+        'Lyman County, SD': '085',
+        'Marshall County, SD': '091',
+        'McCook County, SD': '087',
+        'McPherson County, SD': '089',
+        'Meade County, SD': '093',
+        'Mellette County, SD': '095',
+        'Miner County, SD': '097',
+        'Minnehaha County, SD': '099',
+        'Moody County, SD': '101',
+        'Pennington County, SD': '103',
+        'Perkins County, SD': '105',
+        'Potter County, SD': '107',
+        'Roberts County, SD': '109',
+        'Sanborn County, SD': '111',
+        'Shannon County, SD': '113',
+        'Spink County, SD': '115',
+        'Stanley County, SD': '117',
+        'Sully County, SD': '119',
+        'Todd County, SD': '121',
+        'Tripp County, SD': '123',
+        'Turner County, SD': '125',
+        'Union County, SD': '127',
+        'Walworth County, SD': '129',
+        'Yankton County, SD': '135',
+        'Ziebach County, SD': '137'},
+  47: { '--All--': '%',
+        'Anderson County, TN': '001',
+        'Bedford County, TN': '003',
+        'Benton County, TN': '005',
+        'Bledsoe County, TN': '007',
+        'Blount County, TN': '009',
+        'Bradley County, TN': '011',
+        'Campbell County, TN': '013',
+        'Cannon County, TN': '015',
+        'Carroll County, TN': '017',
+        'Carter County, TN': '019',
+        'Cheatham County, TN': '021',
+        'Chester County, TN': '023',
+        'Claiborne County, TN': '025',
+        'Clay County, TN': '027',
+        'Cocke County, TN': '029',
+        'Coffee County, TN': '031',
+        'Crockett County, TN': '033',
+        'Cumberland County, TN': '035',
+        'Davidson County, TN': '037',
+        'DeKalb County, TN': '041',
+        'Decatur County, TN': '039',
+        'Dickson County, TN': '043',
+        'Dyer County, TN': '045',
+        'Fayette County, TN': '047',
+        'Fentress County, TN': '049',
+        'Franklin County, TN': '051',
+        'Gibson County, TN': '053',
+        'Giles County, TN': '055',
+        'Grainger County, TN': '057',
+        'Greene County, TN': '059',
+        'Grundy County, TN': '061',
+        'Hamblen County, TN': '063',
+        'Hamilton County, TN': '065',
+        'Hancock County, TN': '067',
+        'Hardeman County, TN': '069',
+        'Hardin County, TN': '071',
+        'Hawkins County, TN': '073',
+        'Haywood County, TN': '075',
+        'Henderson County, TN': '077',
+        'Henry County, TN': '079',
+        'Hickman County, TN': '081',
+        'Houston County, TN': '083',
+        'Humphreys County, TN': '085',
+        'Jackson County, TN': '087',
+        'Jefferson County, TN': '089',
+        'Johnson County, TN': '091',
+        'Knox County, TN': '093',
+        'Lake County, TN': '095',
+        'Lauderdale County, TN': '097',
+        'Lawrence County, TN': '099',
+        'Lewis County, TN': '101',
+        'Lincoln County, TN': '103',
+        'Loudon County, TN': '105',
+        'Macon County, TN': '111',
+        'Madison County, TN': '113',
+        'Marion County, TN': '115',
+        'Marshall County, TN': '117',
+        'Maury County, TN': '119',
+        'McMinn County, TN': '107',
+        'McNairy County, TN': '109',
+        'Meigs County, TN': '121',
+        'Monroe County, TN': '123',
+        'Montgomery County, TN': '125',
+        'Moore County, TN': '127',
+        'Morgan County, TN': '129',
+        'Obion County, TN': '131',
+        'Overton County, TN': '133',
+        'Perry County, TN': '135',
+        'Pickett County, TN': '137',
+        'Polk County, TN': '139',
+        'Putnam County, TN': '141',
+        'Rhea County, TN': '143',
+        'Roane County, TN': '145',
+        'Robertson County, TN': '147',
+        'Rutherford County, TN': '149',
+        'Scott County, TN': '151',
+        'Sequatchie County, TN': '153',
+        'Sevier County, TN': '155',
+        'Shelby County, TN': '157',
+        'Smith County, TN': '159',
+        'Stewart County, TN': '161',
+        'Sullivan County, TN': '163',
+        'Sumner County, TN': '165',
+        'Tipton County, TN': '167',
+        'Trousdale County, TN': '169',
+        'Unicoi County, TN': '171',
+        'Union County, TN': '173',
+        'Van Buren County, TN': '175',
+        'Warren County, TN': '177',
+        'Washington County, TN': '179',
+        'Wayne County, TN': '181',
+        'Weakley County, TN': '183',
+        'White County, TN': '185',
+        'Williamson County, TN': '187',
+        'Wilson County, TN': '189'},
+  48: { '--All--': '%',
+        'Anderson County, TX': '001',
+        'Andrews County, TX': '003',
+        'Angelina County, TX': '005',
+        'Aransas County, TX': '007',
+        'Archer County, TX': '009',
+        'Armstrong County, TX': '011',
+        'Atascosa County, TX': '013',
+        'Austin County, TX': '015',
+        'Bailey County, TX': '017',
+        'Bandera County, TX': '019',
+        'Bastrop County, TX': '021',
+        'Baylor County, TX': '023',
+        'Bee County, TX': '025',
+        'Bell County, TX': '027',
+        'Bexar County, TX': '029',
+        'Blanco County, TX': '031',
+        'Borden County, TX': '033',
+        'Bosque County, TX': '035',
+        'Bowie County, TX': '037',
+        'Brazoria County, TX': '039',
+        'Brazos County, TX': '041',
+        'Brewster County, TX': '043',
+        'Briscoe County, TX': '045',
+        'Brooks County, TX': '047',
+        'Brown County, TX': '049',
+        'Burleson County, TX': '051',
+        'Burnet County, TX': '053',
+        'Caldwell County, TX': '055',
+        'Calhoun County, TX': '057',
+        'Callahan County, TX': '059',
+        'Cameron County, TX': '061',
+        'Camp County, TX': '063',
+        'Carson County, TX': '065',
+        'Cass County, TX': '067',
+        'Castro County, TX': '069',
+        'Chambers County, TX': '071',
+        'Cherokee County, TX': '073',
+        'Childress County, TX': '075',
+        'Clay County, TX': '077',
+        'Cochran County, TX': '079',
+        'Coke County, TX': '081',
+        'Coleman County, TX': '083',
+        'Collin County, TX': '085',
+        'Collingsworth County, TX': '087',
+        'Colorado County, TX': '089',
+        'Comal County, TX': '091',
+        'Comanche County, TX': '093',
+        'Concho County, TX': '095',
+        'Cooke County, TX': '097',
+        'Coryell County, TX': '099',
+        'Cottle County, TX': '101',
+        'Crane County, TX': '103',
+        'Crockett County, TX': '105',
+        'Crosby County, TX': '107',
+        'Culberson County, TX': '109',
+        'Dallam County, TX': '111',
+        'Dallas County, TX': '113',
+        'Dawson County, TX': '115',
+        'DeWitt County, TX': '123',
+        'Deaf Smith County, TX': '117',
+        'Delta County, TX': '119',
+        'Denton County, TX': '121',
+        'Dickens County, TX': '125',
+        'Dimmit County, TX': '127',
+        'Donley County, TX': '129',
+        'Duval County, TX': '131',
+        'Eastland County, TX': '133',
+        'Ector County, TX': '135',
+        'Edwards County, TX': '137',
+        'El Paso County, TX': '141',
+        'Ellis County, TX': '139',
+        'Erath County, TX': '143',
+        'Falls County, TX': '145',
+        'Fannin County, TX': '147',
+        'Fayette County, TX': '149',
+        'Fisher County, TX': '151',
+        'Floyd County, TX': '153',
+        'Foard County, TX': '155',
+        'Fort Bend County, TX': '157',
+        'Franklin County, TX': '159',
+        'Freestone County, TX': '161',
+        'Frio County, TX': '163',
+        'Gaines County, TX': '165',
+        'Galveston County, TX': '167',
+        'Garza County, TX': '169',
+        'Gillespie County, TX': '171',
+        'Glasscock County, TX': '173',
+        'Goliad County, TX': '175',
+        'Gonzales County, TX': '177',
+        'Gray County, TX': '179',
+        'Grayson County, TX': '181',
+        'Gregg County, TX': '183',
+        'Grimes County, TX': '185',
+        'Guadalupe County, TX': '187',
+        'Hale County, TX': '189',
+        'Hall County, TX': '191',
+        'Hamilton County, TX': '193',
+        'Hansford County, TX': '195',
+        'Hardeman County, TX': '197',
+        'Hardin County, TX': '199',
+        'Harris County, TX': '201',
+        'Harrison County, TX': '203',
+        'Hartley County, TX': '205',
+        'Haskell County, TX': '207',
+        'Hays County, TX': '209',
+        'Hemphill County, TX': '211',
+        'Henderson County, TX': '213',
+        'Hidalgo County, TX': '215',
+        'Hill County, TX': '217',
+        'Hockley County, TX': '219',
+        'Hood County, TX': '221',
+        'Hopkins County, TX': '223',
+        'Houston County, TX': '225',
+        'Howard County, TX': '227',
+        'Hudspeth County, TX': '229',
+        'Hunt County, TX': '231',
+        'Hutchinson County, TX': '233',
+        'Irion County, TX': '235',
+        'Jack County, TX': '237',
+        'Jackson County, TX': '239',
+        'Jasper County, TX': '241',
+        'Jeff Davis County, TX': '243',
+        'Jefferson County, TX': '245',
+        'Jim Hogg County, TX': '247',
+        'Jim Wells County, TX': '249',
+        'Johnson County, TX': '251',
+        'Jones County, TX': '253',
+        'Karnes County, TX': '255',
+        'Kaufman County, TX': '257',
+        'Kendall County, TX': '259',
+        'Kenedy County, TX': '261',
+        'Kent County, TX': '263',
+        'Kerr County, TX': '265',
+        'Kimble County, TX': '267',
+        'King County, TX': '269',
+        'Kinney County, TX': '271',
+        'Kleberg County, TX': '273',
+        'Knox County, TX': '275',
+        'La Salle County, TX': '283',
+        'Lamar County, TX': '277',
+        'Lamb County, TX': '279',
+        'Lampasas County, TX': '281',
+        'Lavaca County, TX': '285',
+        'Lee County, TX': '287',
+        'Leon County, TX': '289',
+        'Liberty County, TX': '291',
+        'Limestone County, TX': '293',
+        'Lipscomb County, TX': '295',
+        'Live Oak County, TX': '297',
+        'Llano County, TX': '299',
+        'Loving County, TX': '301',
+        'Lubbock County, TX': '303',
+        'Lynn County, TX': '305',
+        'Madison County, TX': '313',
+        'Marion County, TX': '315',
+        'Martin County, TX': '317',
+        'Mason County, TX': '319',
+        'Matagorda County, TX': '321',
+        'Maverick County, TX': '323',
+        'McCulloch County, TX': '307',
+        'McLennan County, TX': '309',
+        'McMullen County, TX': '311',
+        'Medina County, TX': '325',
+        'Menard County, TX': '327',
+        'Midland County, TX': '329',
+        'Milam County, TX': '331',
+        'Mills County, TX': '333',
+        'Mitchell County, TX': '335',
+        'Montague County, TX': '337',
+        'Montgomery County, TX': '339',
+        'Moore County, TX': '341',
+        'Morris County, TX': '343',
+        'Motley County, TX': '345',
+        'Nacogdoches County, TX': '347',
+        'Navarro County, TX': '349',
+        'Newton County, TX': '351',
+        'Nolan County, TX': '353',
+        'Nueces County, TX': '355',
+        'Ochiltree County, TX': '357',
+        'Oldham County, TX': '359',
+        'Orange County, TX': '361',
+        'Palo Pinto County, TX': '363',
+        'Panola County, TX': '365',
+        'Parker County, TX': '367',
+        'Parmer County, TX': '369',
+        'Pecos County, TX': '371',
+        'Polk County, TX': '373',
+        'Potter County, TX': '375',
+        'Presidio County, TX': '377',
+        'Rains County, TX': '379',
+        'Randall County, TX': '381',
+        'Reagan County, TX': '383',
+        'Real County, TX': '385',
+        'Red River County, TX': '387',
+        'Reeves County, TX': '389',
+        'Refugio County, TX': '391',
+        'Roberts County, TX': '393',
+        'Robertson County, TX': '395',
+        'Rockwall County, TX': '397',
+        'Runnels County, TX': '399',
+        'Rusk County, TX': '401',
+        'Sabine County, TX': '403',
+        'San Augustine County, TX': '405',
+        'San Jacinto County, TX': '407',
+        'San Patricio County, TX': '409',
+        'San Saba County, TX': '411',
+        'Schleicher County, TX': '413',
+        'Scurry County, TX': '415',
+        'Shackelford County, TX': '417',
+        'Shelby County, TX': '419',
+        'Sherman County, TX': '421',
+        'Smith County, TX': '423',
+        'Somervell County, TX': '425',
+        'Starr County, TX': '427',
+        'Stephens County, TX': '429',
+        'Sterling County, TX': '431',
+        'Stonewall County, TX': '433',
+        'Sutton County, TX': '435',
+        'Swisher County, TX': '437',
+        'Tarrant County, TX': '439',
+        'Taylor County, TX': '441',
+        'Terrell County, TX': '443',
+        'Terry County, TX': '445',
+        'Throckmorton County, TX': '447',
+        'Titus County, TX': '449',
+        'Tom Green County, TX': '451',
+        'Travis County, TX': '453',
+        'Trinity County, TX': '455',
+        'Tyler County, TX': '457',
+        'Upshur County, TX': '459',
+        'Upton County, TX': '461',
+        'Uvalde County, TX': '463',
+        'Val Verde County, TX': '465',
+        'Van Zandt County, TX': '467',
+        'Victoria County, TX': '469',
+        'Walker County, TX': '471',
+        'Waller County, TX': '473',
+        'Ward County, TX': '475',
+        'Washington County, TX': '477',
+        'Webb County, TX': '479',
+        'Wharton County, TX': '481',
+        'Wheeler County, TX': '483',
+        'Wichita County, TX': '485',
+        'Wilbarger County, TX': '487',
+        'Willacy County, TX': '489',
+        'Williamson County, TX': '491',
+        'Wilson County, TX': '493',
+        'Winkler County, TX': '495',
+        'Wise County, TX': '497',
+        'Wood County, TX': '499',
+        'Yoakum County, TX': '501',
+        'Young County, TX': '503',
+        'Zapata County, TX': '505',
+        'Zavala County, TX': '507'},
+  49: { '--All--': '%',
+        'Beaver County, UT': '001',
+        'Box Elder County, UT': '003',
+        'Cache County, UT': '005',
+        'Carbon County, UT': '007',
+        'Daggett County, UT': '009',
+        'Davis County, UT': '011',
+        'Duchesne County, UT': '013',
+        'Emery County, UT': '015',
+        'Garfield County, UT': '017',
+        'Grand County, UT': '019',
+        'Iron County, UT': '021',
+        'Juab County, UT': '023',
+        'Kane County, UT': '025',
+        'Millard County, UT': '027',
+        'Morgan County, UT': '029',
+        'Piute County, UT': '031',
+        'Rich County, UT': '033',
+        'Salt Lake County, UT': '035',
+        'San Juan County, UT': '037',
+        'Sanpete County, UT': '039',
+        'Sevier County, UT': '041',
+        'Summit County, UT': '043',
+        'Tooele County, UT': '045',
+        'Uintah County, UT': '047',
+        'Utah County, UT': '049',
+        'Wasatch County, UT': '051',
+        'Washington County, UT': '053',
+        'Wayne County, UT': '055',
+        'Weber County, UT': '057'},
+  50: { '--All--': '%',
+        'Addison County, VT': '001',
+        'Bennington County, VT': '003',
+        'Caledonia County, VT': '005',
+        'Chittenden County, VT': '007',
+        'Essex County, VT': '009',
+        'Franklin County, VT': '011',
+        'Grand Isle County, VT': '013',
+        'Lamoille County, VT': '015',
+        'Orange County, VT': '017',
+        'Orleans County, VT': '019',
+        'Rutland County, VT': '021',
+        'Washington County, VT': '023',
+        'Windham County, VT': '025',
+        'Windsor County, VT': '027'},
+  51: { '--All--': '%',
+        'Accomack County, VA': '001',
+        'Albemarle County, VA': '003',
+        'Alexandria city, VA': '510',
+        'Alleghany County, VA': '005',
+        'Amelia County, VA': '007',
+        'Amherst County, VA': '009',
+        'Appomattox County, VA': '011',
+        'Arlington County, VA': '013',
+        'Augusta County, VA': '015',
+        'Bath County, VA': '017',
+        'Bedford County, VA': '019',
+        'Bedford city, VA': '515',
+        'Bland County, VA': '021',
+        'Botetourt County, VA': '023',
+        'Bristol city, VA': '520',
+        'Brunswick County, VA': '025',
+        'Buchanan County, VA': '027',
+        'Buckingham County, VA': '029',
+        'Buena Vista city, VA': '530',
+        'Campbell County, VA': '031',
+        'Caroline County, VA': '033',
+        'Carroll County, VA': '035',
+        'Charles City County, VA': '036',
+        'Charlotte County, VA': '037',
+        'Charlottesville city, VA': '540',
+        'Chesapeake city, VA': '550',
+        'Chesterfield County, VA': '041',
+        'Clarke County, VA': '043',
+        'Colonial Heights city, VA': '570',
+        'Covington city, VA': '580',
+        'Craig County, VA': '045',
+        'Culpeper County, VA': '047',
+        'Cumberland County, VA': '049',
+        'Danville city, VA': '590',
+        'Dickenson County, VA': '051',
+        'Dinwiddie County, VA': '053',
+        'Emporia city, VA': '595',
+        'Essex County, VA': '057',
+        'Fairfax County, VA': '059',
+        'Fairfax city, VA': '600',
+        'Falls Church city, VA': '610',
+        'Fauquier County, VA': '061',
+        'Floyd County, VA': '063',
+        'Fluvanna County, VA': '065',
+        'Franklin County, VA': '067',
+        'Franklin city, VA': '620',
+        'Frederick County, VA': '069',
+        'Fredericksburg city, VA': '630',
+        'Galax city, VA': '640',
+        'Giles County, VA': '071',
+        'Gloucester County, VA': '073',
+        'Goochland County, VA': '075',
+        'Grayson County, VA': '077',
+        'Greene County, VA': '079',
+        'Greensville County, VA': '081',
+        'Halifax County, VA': '083',
+        'Hampton city, VA': '650',
+        'Hanover County, VA': '085',
+        'Harrisonburg city, VA': '660',
+        'Henrico County, VA': '087',
+        'Henry County, VA': '089',
+        'Highland County, VA': '091',
+        'Hopewell city, VA': '670',
+        'Isle of Wight County, VA': '093',
+        'James City County, VA': '095',
+        'King George County, VA': '099',
+        'King William County, VA': '101',
+        'King and Queen County, VA': '097',
+        'Lancaster County, VA': '103',
+        'Lee County, VA': '105',
+        'Lexington city, VA': '678',
+        'Loudoun County, VA': '107',
+        'Louisa County, VA': '109',
+        'Lunenburg County, VA': '111',
+        'Lynchburg city, VA': '680',
+        'Madison County, VA': '113',
+        'Manassas Park city, VA': '685',
+        'Manassas city, VA': '683',
+        'Martinsville city, VA': '690',
+        'Mathews County, VA': '115',
+        'Mecklenburg County, VA': '117',
+        'Middlesex County, VA': '119',
+        'Montgomery County, VA': '121',
+        'Nelson County, VA': '125',
+        'New Kent County, VA': '127',
+        'Newport News city, VA': '700',
+        'Norfolk city, VA': '710',
+        'Northampton County, VA': '131',
+        'Northumberland County, VA': '133',
+        'Norton city, VA': '720',
+        'Nottoway County, VA': '135',
+        'Orange County, VA': '137',
+        'Page County, VA': '139',
+        'Patrick County, VA': '141',
+        'Petersburg city, VA': '730',
+        'Pittsylvania County, VA': '143',
+        'Poquoson city, VA': '735',
+        'Portsmouth city, VA': '740',
+        'Powhatan County, VA': '145',
+        'Prince Edward County, VA': '147',
+        'Prince George County, VA': '149',
+        'Prince William County, VA': '153',
+        'Pulaski County, VA': '155',
+        'Radford city, VA': '750',
+        'Rappahannock County, VA': '157',
+        'Richmond County, VA': '159',
+        'Richmond city, VA': '760',
+        'Roanoke County, VA': '161',
+        'Roanoke city, VA': '770',
+        'Rockbridge County, VA': '163',
+        'Rockingham County, VA': '165',
+        'Russell County, VA': '167',
+        'Salem city, VA': '775',
+        'Scott County, VA': '169',
+        'Shenandoah County, VA': '171',
+        'Smyth County, VA': '173',
+        'Southampton County, VA': '175',
+        'Spotsylvania County, VA': '177',
+        'Stafford County, VA': '179',
+        'Staunton city, VA': '790',
+        'Suffolk city, VA': '800',
+        'Surry County, VA': '181',
+        'Sussex County, VA': '183',
+        'Tazewell County, VA': '185',
+        'Virginia Beach city, VA': '810',
+        'Warren County, VA': '187',
+        'Washington County, VA': '191',
+        'Waynesboro city, VA': '820',
+        'Westmoreland County, VA': '193',
+        'Williamsburg city, VA': '830',
+        'Winchester city, VA': '840',
+        'Wise County, VA': '195',
+        'Wythe County, VA': '197',
+        'York County, VA': '199'},
+  53: { '--All--': '%',
+        'Adams County, WA': '001',
+        'Asotin County, WA': '003',
+        'Benton County, WA': '005',
+        'Chelan County, WA': '007',
+        'Clallam County, WA': '009',
+        'Clark County, WA': '011',
+        'Columbia County, WA': '013',
+        'Cowlitz County, WA': '015',
+        'Douglas County, WA': '017',
+        'Ferry County, WA': '019',
+        'Franklin County, WA': '021',
+        'Garfield County, WA': '023',
+        'Grant County, WA': '025',
+        'Grays Harbor County, WA': '027',
+        'Island County, WA': '029',
+        'Jefferson County, WA': '031',
+        'King County, WA': '033',
+        'Kitsap County, WA': '035',
+        'Kittitas County, WA': '037',
+        'Klickitat County, WA': '039',
+        'Lewis County, WA': '041',
+        'Lincoln County, WA': '043',
+        'Mason County, WA': '045',
+        'Okanogan County, WA': '047',
+        'Pacific County, WA': '049',
+        'Pend Oreille County, WA': '051',
+        'Pierce County, WA': '053',
+        'San Juan County, WA': '055',
+        'Skagit County, WA': '057',
+        'Skamania County, WA': '059',
+        'Snohomish County, WA': '061',
+        'Spokane County, WA': '063',
+        'Stevens County, WA': '065',
+        'Thurston County, WA': '067',
+        'Wahkiakum County, WA': '069',
+        'Walla Walla County, WA': '071',
+        'Whatcom County, WA': '073',
+        'Whitman County, WA': '075',
+        'Yakima County, WA': '077'},
+  54: { '--All--': '%',
+        'Barbour County, WV': '001',
+        'Berkeley County, WV': '003',
+        'Boone County, WV': '005',
+        'Braxton County, WV': '007',
+        'Brooke County, WV': '009',
+        'Cabell County, WV': '011',
+        'Calhoun County, WV': '013',
+        'Clay County, WV': '015',
+        'Doddridge County, WV': '017',
+        'Fayette County, WV': '019',
+        'Gilmer County, WV': '021',
+        'Grant County, WV': '023',
+        'Greenbrier County, WV': '025',
+        'Hampshire County, WV': '027',
+        'Hancock County, WV': '029',
+        'Hardy County, WV': '031',
+        'Harrison County, WV': '033',
+        'Jackson County, WV': '035',
+        'Jefferson County, WV': '037',
+        'Kanawha County, WV': '039',
+        'Lewis County, WV': '041',
+        'Lincoln County, WV': '043',
+        'Logan County, WV': '045',
+        'Marion County, WV': '049',
+        'Marshall County, WV': '051',
+        'Mason County, WV': '053',
+        'McDowell County, WV': '047',
+        'Mercer County, WV': '055',
+        'Mineral County, WV': '057',
+        'Mingo County, WV': '059',
+        'Monongalia County, WV': '061',
+        'Monroe County, WV': '063',
+        'Morgan County, WV': '065',
+        'Nicholas County, WV': '067',
+        'Ohio County, WV': '069',
+        'Pendleton County, WV': '071',
+        'Pleasants County, WV': '073',
+        'Pocahontas County, WV': '075',
+        'Preston County, WV': '077',
+        'Putnam County, WV': '079',
+        'Raleigh County, WV': '081',
+        'Randolph County, WV': '083',
+        'Ritchie County, WV': '085',
+        'Roane County, WV': '087',
+        'Summers County, WV': '089',
+        'Taylor County, WV': '091',
+        'Tucker County, WV': '093',
+        'Tyler County, WV': '095',
+        'Upshur County, WV': '097',
+        'Wayne County, WV': '099',
+        'Webster County, WV': '101',
+        'Wetzel County, WV': '103',
+        'Wirt County, WV': '105',
+        'Wood County, WV': '107',
+        'Wyoming County, WV': '109'},
+  55: { '--All--': '%',
+        'Adams County, WI': '001',
+        'Ashland County, WI': '003',
+        'Barron County, WI': '005',
+        'Bayfield County, WI': '007',
+        'Brown County, WI': '009',
+        'Buffalo County, WI': '011',
+        'Burnett County, WI': '013',
+        'Calumet County, WI': '015',
+        'Chippewa County, WI': '017',
+        'Clark County, WI': '019',
+        'Columbia County, WI': '021',
+        'Crawford County, WI': '023',
+        'Dane County, WI': '025',
+        'Dodge County, WI': '027',
+        'Door County, WI': '029',
+        'Douglas County, WI': '031',
+        'Dunn County, WI': '033',
+        'Eau Claire County, WI': '035',
+        'Florence County, WI': '037',
+        'Fond du Lac County, WI': '039',
+        'Forest County, WI': '041',
+        'Grant County, WI': '043',
+        'Green County, WI': '045',
+        'Green Lake County, WI': '047',
+        'Iowa County, WI': '049',
+        'Iron County, WI': '051',
+        'Jackson County, WI': '053',
+        'Jefferson County, WI': '055',
+        'Juneau County, WI': '057',
+        'Kenosha County, WI': '059',
+        'Kewaunee County, WI': '061',
+        'La Crosse County, WI': '063',
+        'Lafayette County, WI': '065',
+        'Langlade County, WI': '067',
+        'Lincoln County, WI': '069',
+        'Manitowoc County, WI': '071',
+        'Marathon County, WI': '073',
+        'Marinette County, WI': '075',
+        'Marquette County, WI': '077',
+        'Menominee County, WI': '078',
+        'Milwaukee County, WI': '079',
+        'Monroe County, WI': '081',
+        'Oconto County, WI': '083',
+        'Oneida County, WI': '085',
+        'Outagamie County, WI': '087',
+        'Ozaukee County, WI': '089',
+        'Pepin County, WI': '091',
+        'Pierce County, WI': '093',
+        'Polk County, WI': '095',
+        'Portage County, WI': '097',
+        'Price County, WI': '099',
+        'Racine County, WI': '101',
+        'Richland County, WI': '103',
+        'Rock County, WI': '105',
+        'Rusk County, WI': '107',
+        'Sauk County, WI': '111',
+        'Sawyer County, WI': '113',
+        'Shawano County, WI': '115',
+        'Sheboygan County, WI': '117',
+        'St. Croix County, WI': '109',
+        'Taylor County, WI': '119',
+        'Trempealeau County, WI': '121',
+        'Vernon County, WI': '123',
+        'Vilas County, WI': '125',
+        'Walworth County, WI': '127',
+        'Washburn County, WI': '129',
+        'Washington County, WI': '131',
+        'Waukesha County, WI': '133',
+        'Waupaca County, WI': '135',
+        'Waushara County, WI': '137',
+        'Winnebago County, WI': '139',
+        'Wood County, WI': '141'},
+  56: { '--All--': '%',
+        'Albany County, WY': '001',
+        'Big Horn County, WY': '003',
+        'Campbell County, WY': '005',
+        'Carbon County, WY': '007',
+        'Converse County, WY': '009',
+        'Crook County, WY': '011',
+        'Fremont County, WY': '013',
+        'Goshen County, WY': '015',
+        'Hot Springs County, WY': '017',
+        'Johnson County, WY': '019',
+        'Laramie County, WY': '021',
+        'Lincoln County, WY': '023',
+        'Natrona County, WY': '025',
+        'Niobrara County, WY': '027',
+        'Park County, WY': '029',
+        'Platte County, WY': '031',
+        'Sheridan County, WY': '033',
+        'Sublette County, WY': '035',
+        'Sweetwater County, WY': '037',
+        'Teton County, WY': '039',
+        'Uinta County, WY': '041',
+        'Washakie County, WY': '043',
+        'Weston County, WY': '045'},
+  72: { '--All--': '%',
+        'Adjuntas Municipio, PR': '001',
+        'Aguada Municipio, PR': '003',
+        'Aguadilla Municipio, PR': '005',
+        'Aguas Buenas Municipio, PR': '007',
+        'Aibonito Municipio, PR': '009',
+        'Anasco Municipio, PR': '011',
+        'Arecibo Municipio, PR': '013',
+        'Arroyo Municipio, PR': '015',
+        'Barceloneta Municipio, PR': '017',
+        'Barranquitas Municipio, PR': '019',
+        'Bayamon Municipio, PR': '021',
+        'Cabo Rojo Municipio, PR': '023',
+        'Caguas Municipio, PR': '025',
+        'Camuy Municipio, PR': '027',
+        'Canovanas Municipio, PR': '029',
+        'Carolina Municipio, PR': '031',
+        'Catano Municipio, PR': '033',
+        'Cayey Municipio, PR': '035',
+        'Ceiba Municipio, PR': '037',
+        'Ciales Municipio, PR': '039',
+        'Cidra Municipio, PR': '041',
+        'Coamo Municipio, PR': '043',
+        'Comerio Municipio, PR': '045',
+        'Corozal Municipio, PR': '047',
+        'Culebra Municipio, PR': '049',
+        'Dorado Municipio, PR': '051',
+        'Fajardo Municipio, PR': '053',
+        'Florida Municipio, PR': '054',
+        'Guanica Municipio, PR': '055',
+        'Guayama Municipio, PR': '057',
+        'Guayanilla Municipio, PR': '059',
+        'Guaynabo Municipio, PR': '061',
+        'Gurabo Municipio, PR': '063',
+        'Hatillo Municipio, PR': '065',
+        'Hormigueros Municipio, PR': '067',
+        'Humacao Municipio, PR': '069',
+        'Isabela Municipio, PR': '071',
+        'Jayuya Municipio, PR': '073',
+        'Juana Diaz Municipio, PR': '075',
+        'Juncos Municipio, PR': '077',
+        'Lajas Municipio, PR': '079',
+        'Lares Municipio, PR': '081',
+        'Las Marias Municipio, PR': '083',
+        'Las Piedras Municipio, PR': '085',
+        'Loiza Municipio, PR': '087',
+        'Luquillo Municipio, PR': '089',
+        'Manati Municipio, PR': '091',
+        'Maricao Municipio, PR': '093',
+        'Maunabo Municipio, PR': '095',
+        'Mayaguez Municipio, PR': '097',
+        'Moca Municipio, PR': '099',
+        'Morovis Municipio, PR': '101',
+        'Naguabo Municipio, PR': '103',
+        'Naranjito Municipio, PR': '105',
+        'Orocovis Municipio, PR': '107',
+        'Patillas Municipio, PR': '109',
+        'Penuelas Municipio, PR': '111',
+        'Ponce Municipio, PR': '113',
+        'Quebradillas Municipio, PR': '115',
+        'Rincon Municipio, PR': '117',
+        'Rio Grande Municipio, PR': '119',
+        'Sabana Grande Municipio, PR': '121',
+        'Salinas Municipio, PR': '123',
+        'San German Municipio, PR': '125',
+        'San Juan Municipio, PR': '127',
+        'San Lorenzo Municipio, PR': '129',
+        'San Sebastian Municipio, PR': '131',
+        'Santa Isabel Municipio, PR': '133',
+        'Toa Alta Municipio, PR': '135',
+        'Toa Baja Municipio, PR': '137',
+        'Trujillo Alto Municipio, PR': '139',
+        'Utuado Municipio, PR': '141',
+        'Vega Alta Municipio, PR': '143',
+        'Vega Baja Municipio, PR': '145',
+        'Vieques Municipio, PR': '147',
+        'Villalba Municipio, PR': '149',
+        'Yabucoa Municipio, PR': '151',
+        'Yauco Municipio, PR': '153'},
+  '01': { 'Autauga County, AL': '001',
+          'Baldwin County, AL': '003',
+          'Barbour County, AL': '005',
+          'Bibb County, AL': '007',
+          'Blount County, AL': '009',
+          'Bullock County, AL': '011',
+          'Butler County, AL': '013',
+          'Calhoun County, AL': '015',
+          'Chambers County, AL': '017',
+          'Cherokee County, AL': '019',
+          'Chilton County, AL': '021',
+          'Choctaw County, AL': '023',
+          'Clarke County, AL': '025',
+          'Clay County, AL': '027',
+          'Cleburne County, AL': '029',
+          'Coffee County, AL': '031',
+          'Colbert County, AL': '033',
+          'Conecuh County, AL': '035',
+          'Coosa County, AL': '037',
+          'Covington County, AL': '039',
+          'Crenshaw County, AL': '041',
+          'Cullman County, AL': '043',
+          'Dale County, AL': '045',
+          'Dallas County, AL': '047',
+          'DeKalb County, AL': '049',
+          'Elmore County, AL': '051',
+          'Escambia County, AL': '053',
+          'Etowah County, AL': '055',
+          'Fayette County, AL': '057',
+          'Franklin County, AL': '059',
+          'Geneva County, AL': '061',
+          'Greene County, AL': '063',
+          'Hale County, AL': '065',
+          'Henry County, AL': '067',
+          'Houston County, AL': '069',
+          'Jackson County, AL': '071',
+          'Jefferson County, AL': '073',
+          'Lamar County, AL': '075',
+          'Lauderdale County, AL': '077',
+          'Lawrence County, AL': '079',
+          'Lee County, AL': '081',
+          'Limestone County, AL': '083',
+          'Lowndes County, AL': '085',
+          'Macon County, AL': '087',
+          'Madison County, AL': '089',
+          'Marengo County, AL': '091',
+          'Marion County, AL': '093',
+          'Marshall County, AL': '095',
+          'Mobile County, AL': '097',
+          'Monroe County, AL': '099',
+          'Montgomery County, AL': '101',
+          'Morgan County, AL': '103',
+          'Perry County, AL': '105',
+          'Pickens County, AL': '107',
+          'Pike County, AL': '109',
+          'Randolph County, AL': '111',
+          'Russell County, AL': '113',
+          'Shelby County, AL': '117',
+          'St. Clair County, AL': '115',
+          'Sumter County, AL': '119',
+          'Talladega County, AL': '121',
+          'Tallapoosa County, AL': '123',
+          'Tuscaloosa County, AL': '125',
+          'Walker County, AL': '127',
+          'Washington County, AL': '129',
+          'Wilcox County, AL': '131',
+          'Winston County, AL': '133'},
+  '02': { 'Aleutians East Borough, AK': '013',
+          'Aleutians West Census Area, AK': '016',
+          'Anchorage Borough/municipality, AK': '020',
+          'Bethel Census Area, AK': '050',
+          'Bristol Bay Borough, AK': '060',
+          'Denali Borough, AK': '068',
+          'Dillingham Census Area, AK': '070',
+          'Fairbanks North Star Borough, AK': '090',
+          'Haines Borough, AK': '100',
+          'Juneau Borough/city, AK': '110',
+          'Kenai Peninsula Borough, AK': '122',
+          'Ketchikan Gateway Borough, AK': '130',
+          'Kodiak Island Borough, AK': '150',
+          'Lake and Peninsula Borough, AK': '164',
+          'Matanuska-Susitna Borough, AK': '170',
+          'Nome Census Area, AK': '180',
+          'North Slope Borough, AK': '185',
+          'Northwest Arctic Borough, AK': '188',
+          'Prince of Wales-Outer Ketchikan Census Area, AK': '201',
+          'Sitka Borough/city, AK': '220',
+          'Skagway-Hoonah-Angoon Census Area, AK': '232',
+          'Southeast Fairbanks Census Area, AK': '240',
+          'Valdez-Cordova Census Area, AK': '261',
+          'Wade Hampton Census Area, AK': '270',
+          'Wrangell-Petersburg Census Area, AK': '280',
+          'Yakutat Borough, AK': '282',
+          'Yukon-Koyukuk Census Area, AK': '290'},
+  '04': { 'Apache County, AZ': '001',
+          'Cochise County, AZ': '003',
+          'Coconino County, AZ': '005',
+          'Gila County, AZ': '007',
+          'Graham County, AZ': '009',
+          'Greenlee County, AZ': '011',
+          'La Paz County, AZ': '012',
+          'Maricopa County, AZ': '013',
+          'Mohave County, AZ': '015',
+          'Navajo County, AZ': '017',
+          'Pima County, AZ': '019',
+          'Pinal County, AZ': '021',
+          'Santa Cruz County, AZ': '023',
+          'Yavapai County, AZ': '025',
+          'Yuma County, AZ': '027'},
+  '05': { 'Arkansas County, AR': '001',
+          'Ashley County, AR': '003',
+          'Baxter County, AR': '005',
+          'Benton County, AR': '007',
+          'Boone County, AR': '009',
+          'Bradley County, AR': '011',
+          'Calhoun County, AR': '013',
+          'Carroll County, AR': '015',
+          'Chicot County, AR': '017',
+          'Clark County, AR': '019',
+          'Clay County, AR': '021',
+          'Cleburne County, AR': '023',
+          'Cleveland County, AR': '025',
+          'Columbia County, AR': '027',
+          'Conway County, AR': '029',
+          'Craighead County, AR': '031',
+          'Crawford County, AR': '033',
+          'Crittenden County, AR': '035',
+          'Cross County, AR': '037',
+          'Dallas County, AR': '039',
+          'Desha County, AR': '041',
+          'Drew County, AR': '043',
+          'Faulkner County, AR': '045',
+          'Franklin County, AR': '047',
+          'Fulton County, AR': '049',
+          'Garland County, AR': '051',
+          'Grant County, AR': '053',
+          'Greene County, AR': '055',
+          'Hempstead County, AR': '057',
+          'Hot Spring County, AR': '059',
+          'Howard County, AR': '061',
+          'Independence County, AR': '063',
+          'Izard County, AR': '065',
+          'Jackson County, AR': '067',
+          'Jefferson County, AR': '069',
+          'Johnson County, AR': '071',
+          'Lafayette County, AR': '073',
+          'Lawrence County, AR': '075',
+          'Lee County, AR': '077',
+          'Lincoln County, AR': '079',
+          'Little River County, AR': '081',
+          'Logan County, AR': '083',
+          'Lonoke County, AR': '085',
+          'Madison County, AR': '087',
+          'Marion County, AR': '089',
+          'Miller County, AR': '091',
+          'Mississippi County, AR': '093',
+          'Monroe County, AR': '095',
+          'Montgomery County, AR': '097',
+          'Nevada County, AR': '099',
+          'Newton County, AR': '101',
+          'Ouachita County, AR': '103',
+          'Perry County, AR': '105',
+          'Phillips County, AR': '107',
+          'Pike County, AR': '109',
+          'Poinsett County, AR': '111',
+          'Polk County, AR': '113',
+          'Pope County, AR': '115',
+          'Prairie County, AR': '117',
+          'Pulaski County, AR': '119',
+          'Randolph County, AR': '121',
+          'Saline County, AR': '125',
+          'Scott County, AR': '127',
+          'Searcy County, AR': '129',
+          'Sebastian County, AR': '131',
+          'Sevier County, AR': '133',
+          'Sharp County, AR': '135',
+          'St. Francis County, AR': '123',
+          'Stone County, AR': '137',
+          'Union County, AR': '139',
+          'Van Buren County, AR': '141',
+          'Washington County, AR': '143',
+          'White County, AR': '145',
+          'Woodruff County, AR': '147',
+          'Yell County, AR': '149'},
+  '06': { 'Alameda County, CA': '001',
+          'Alpine County, CA': '003',
+          'Amador County, CA': '005',
+          'Butte County, CA': '007',
+          'Calaveras County, CA': '009',
+          'Colusa County, CA': '011',
+          'Contra Costa County, CA': '013',
+          'Del Norte County, CA': '015',
+          'El Dorado County, CA': '017',
+          'Fresno County, CA': '019',
+          'Glenn County, CA': '021',
+          'Humboldt County, CA': '023',
+          'Imperial County, CA': '025',
+          'Inyo County, CA': '027',
+          'Kern County, CA': '029',
+          'Kings County, CA': '031',
+          'Lake County, CA': '033',
+          'Lassen County, CA': '035',
+          'Los Angeles County, CA': '037',
+          'Madera County, CA': '039',
+          'Marin County, CA': '041',
+          'Mariposa County, CA': '043',
+          'Mendocino County, CA': '045',
+          'Merced County, CA': '047',
+          'Modoc County, CA': '049',
+          'Mono County, CA': '051',
+          'Monterey County, CA': '053',
+          'Napa County, CA': '055',
+          'Nevada County, CA': '057',
+          'Orange County, CA': '059',
+          'Placer County, CA': '061',
+          'Plumas County, CA': '063',
+          'Riverside County, CA': '065',
+          'Sacramento County, CA': '067',
+          'San Benito County, CA': '069',
+          'San Bernardino County, CA': '071',
+          'San Diego County, CA': '073',
+          'San Francisco County/city, CA': '075',
+          'San Joaquin County, CA': '077',
+          'San Luis Obispo County, CA': '079',
+          'San Mateo County, CA': '081',
+          'Santa Barbara County, CA': '083',
+          'Santa Clara County, CA': '085',
+          'Santa Cruz County, CA': '087',
+          'Shasta County, CA': '089',
+          'Sierra County, CA': '091',
+          'Siskiyou County, CA': '093',
+          'Solano County, CA': '095',
+          'Sonoma County, CA': '097',
+          'Stanislaus County, CA': '099',
+          'Sutter County, CA': '101',
+          'Tehama County, CA': '103',
+          'Trinity County, CA': '105',
+          'Tulare County, CA': '107',
+          'Tuolumne County, CA': '109',
+          'Ventura County, CA': '111',
+          'Yolo County, CA': '113',
+          'Yuba County, CA': '115'},
+  '08': { 'Adams County, CO': '001',
+          'Alamosa County, CO': '003',
+          'Arapahoe County, CO': '005',
+          'Archuleta County, CO': '007',
+          'Baca County, CO': '009',
+          'Bent County, CO': '011',
+          'Boulder County, CO': '013',
+          'Broomfield County/city, CO': '014',
+          'Chaffee County, CO': '015',
+          'Cheyenne County, CO': '017',
+          'Clear Creek County, CO': '019',
+          'Conejos County, CO': '021',
+          'Costilla County, CO': '023',
+          'Crowley County, CO': '025',
+          'Custer County, CO': '027',
+          'Delta County, CO': '029',
+          'Denver County/city, CO': '031',
+          'Dolores County, CO': '033',
+          'Douglas County, CO': '035',
+          'Eagle County, CO': '037',
+          'El Paso County, CO': '041',
+          'Elbert County, CO': '039',
+          'Fremont County, CO': '043',
+          'Garfield County, CO': '045',
+          'Gilpin County, CO': '047',
+          'Grand County, CO': '049',
+          'Gunnison County, CO': '051',
+          'Hinsdale County, CO': '053',
+          'Huerfano County, CO': '055',
+          'Jackson County, CO': '057',
+          'Jefferson County, CO': '059',
+          'Kiowa County, CO': '061',
+          'Kit Carson County, CO': '063',
+          'La Plata County, CO': '067',
+          'Lake County, CO': '065',
+          'Larimer County, CO': '069',
+          'Las Animas County, CO': '071',
+          'Lincoln County, CO': '073',
+          'Logan County, CO': '075',
+          'Mesa County, CO': '077',
+          'Mineral County, CO': '079',
+          'Moffat County, CO': '081',
+          'Montezuma County, CO': '083',
+          'Montrose County, CO': '085',
+          'Morgan County, CO': '087',
+          'Otero County, CO': '089',
+          'Ouray County, CO': '091',
+          'Park County, CO': '093',
+          'Phillips County, CO': '095',
+          'Pitkin County, CO': '097',
+          'Prowers County, CO': '099',
+          'Pueblo County, CO': '101',
+          'Rio Blanco County, CO': '103',
+          'Rio Grande County, CO': '105',
+          'Routt County, CO': '107',
+          'Saguache County, CO': '109',
+          'San Juan County, CO': '111',
+          'San Miguel County, CO': '113',
+          'Sedgwick County, CO': '115',
+          'Summit County, CO': '117',
+          'Teller County, CO': '119',
+          'Washington County, CO': '121',
+          'Weld County, CO': '123',
+          'Yuma County, CO': '125'},
+  '09': { 'Fairfield County, CT': '001',
+          'Hartford County, CT': '003',
+          'Litchfield County, CT': '005',
+          'Middlesex County, CT': '007',
+          'New Haven County, CT': '009',
+          'New London County, CT': '011',
+          'Tolland County, CT': '013',
+          'Windham County, CT': '015'},
+  '10': { 'Kent County, DE': '001',
+          'New Castle County, DE': '003',
+          'Sussex County, DE': '005'},
+  '11': { 'District of Columbia': '001'},
+  '12': { 'Alachua County, FL': '001',
+          'Baker County, FL': '003',
+          'Bay County, FL': '005',
+          'Bradford County, FL': '007',
+          'Brevard County, FL': '009',
+          'Broward County, FL': '011',
+          'Calhoun County, FL': '013',
+          'Charlotte County, FL': '015',
+          'Citrus County, FL': '017',
+          'Clay County, FL': '019',
+          'Collier County, FL': '021',
+          'Columbia County, FL': '023',
+          'DeSoto County, FL': '027',
+          'Dixie County, FL': '029',
+          'Duval County, FL': '031',
+          'Escambia County, FL': '033',
+          'Flagler County, FL': '035',
+          'Franklin County, FL': '037',
+          'Gadsden County, FL': '039',
+          'Gilchrist County, FL': '041',
+          'Glades County, FL': '043',
+          'Gulf County, FL': '045',
+          'Hamilton County, FL': '047',
+          'Hardee County, FL': '049',
+          'Hendry County, FL': '051',
+          'Hernando County, FL': '053',
+          'Highlands County, FL': '055',
+          'Hillsborough County, FL': '057',
+          'Holmes County, FL': '059',
+          'Indian River County, FL': '061',
+          'Jackson County, FL': '063',
+          'Jefferson County, FL': '065',
+          'Lafayette County, FL': '067',
+          'Lake County, FL': '069',
+          'Lee County, FL': '071',
+          'Leon County, FL': '073',
+          'Levy County, FL': '075',
+          'Liberty County, FL': '077',
+          'Madison County, FL': '079',
+          'Manatee County, FL': '081',
+          'Marion County, FL': '083',
+          'Martin County, FL': '085',
+          'Miami-Dade County, FL': '086',
+          'Monroe County, FL': '087',
+          'Nassau County, FL': '089',
+          'Okaloosa County, FL': '091',
+          'Okeechobee County, FL': '093',
+          'Orange County, FL': '095',
+          'Osceola County, FL': '097',
+          'Palm Beach County, FL': '099',
+          'Pasco County, FL': '101',
+          'Pinellas County, FL': '103',
+          'Polk County, FL': '105',
+          'Putnam County, FL': '107',
+          'Santa Rosa County, FL': '113',
+          'Sarasota County, FL': '115',
+          'Seminole County, FL': '117',
+          'St. Johns County, FL': '109',
+          'St. Lucie County, FL': '111',
+          'Sumter County, FL': '119',
+          'Suwannee County, FL': '121',
+          'Taylor County, FL': '123',
+          'Union County, FL': '125',
+          'Volusia County, FL': '127',
+          'Wakulla County, FL': '129',
+          'Walton County, FL': '131',
+          'Washington County, FL': '133'},
+  '13': { 'Appling County, GA': '001',
+          'Atkinson County, GA': '003',
+          'Bacon County, GA': '005',
+          'Baker County, GA': '007',
+          'Baldwin County, GA': '009',
+          'Banks County, GA': '011',
+          'Barrow County, GA': '013',
+          'Bartow County, GA': '015',
+          'Ben Hill County, GA': '017',
+          'Berrien County, GA': '019',
+          'Bibb County, GA': '021',
+          'Bleckley County, GA': '023',
+          'Brantley County, GA': '025',
+          'Brooks County, GA': '027',
+          'Bryan County, GA': '029',
+          'Bulloch County, GA': '031',
+          'Burke County, GA': '033',
+          'Butts County, GA': '035',
+          'Calhoun County, GA': '037',
+          'Camden County, GA': '039',
+          'Candler County, GA': '043',
+          'Carroll County, GA': '045',
+          'Catoosa County, GA': '047',
+          'Charlton County, GA': '049',
+          'Chatham County, GA': '051',
+          'Chattahoochee County, GA': '053',
+          'Chattooga County, GA': '055',
+          'Cherokee County, GA': '057',
+          'Clarke County, GA': '059',
+          'Clay County, GA': '061',
+          'Clayton County, GA': '063',
+          'Clinch County, GA': '065',
+          'Cobb County, GA': '067',
+          'Coffee County, GA': '069',
+          'Colquitt County, GA': '071',
+          'Columbia County, GA': '073',
+          'Cook County, GA': '075',
+          'Coweta County, GA': '077',
+          'Crawford County, GA': '079',
+          'Crisp County, GA': '081',
+          'Dade County, GA': '083',
+          'Dawson County, GA': '085',
+          'DeKalb County, GA': '089',
+          'Decatur County, GA': '087',
+          'Dodge County, GA': '091',
+          'Dooly County, GA': '093',
+          'Dougherty County, GA': '095',
+          'Douglas County, GA': '097',
+          'Early County, GA': '099',
+          'Echols County, GA': '101',
+          'Effingham County, GA': '103',
+          'Elbert County, GA': '105',
+          'Emanuel County, GA': '107',
+          'Evans County, GA': '109',
+          'Fannin County, GA': '111',
+          'Fayette County, GA': '113',
+          'Floyd County, GA': '115',
+          'Forsyth County, GA': '117',
+          'Franklin County, GA': '119',
+          'Fulton County, GA': '121',
+          'Gilmer County, GA': '123',
+          'Glascock County, GA': '125',
+          'Glynn County, GA': '127',
+          'Gordon County, GA': '129',
+          'Grady County, GA': '131',
+          'Greene County, GA': '133',
+          'Gwinnett County, GA': '135',
+          'Habersham County, GA': '137',
+          'Hall County, GA': '139',
+          'Hancock County, GA': '141',
+          'Haralson County, GA': '143',
+          'Harris County, GA': '145',
+          'Hart County, GA': '147',
+          'Heard County, GA': '149',
+          'Henry County, GA': '151',
+          'Houston County, GA': '153',
+          'Irwin County, GA': '155',
+          'Jackson County, GA': '157',
+          'Jasper County, GA': '159',
+          'Jeff Davis County, GA': '161',
+          'Jefferson County, GA': '163',
+          'Jenkins County, GA': '165',
+          'Johnson County, GA': '167',
+          'Jones County, GA': '169',
+          'Lamar County, GA': '171',
+          'Lanier County, GA': '173',
+          'Laurens County, GA': '175',
+          'Lee County, GA': '177',
+          'Liberty County, GA': '179',
+          'Lincoln County, GA': '181',
+          'Long County, GA': '183',
+          'Lowndes County, GA': '185',
+          'Lumpkin County, GA': '187',
+          'Macon County, GA': '193',
+          'Madison County, GA': '195',
+          'Marion County, GA': '197',
+          'McDuffie County, GA': '189',
+          'McIntosh County, GA': '191',
+          'Meriwether County, GA': '199',
+          'Miller County, GA': '201',
+          'Mitchell County, GA': '205',
+          'Monroe County, GA': '207',
+          'Montgomery County, GA': '209',
+          'Morgan County, GA': '211',
+          'Murray County, GA': '213',
+          'Muscogee County, GA': '215',
+          'Newton County, GA': '217',
+          'Oconee County, GA': '219',
+          'Oglethorpe County, GA': '221',
+          'Paulding County, GA': '223',
+          'Peach County, GA': '225',
+          'Pickens County, GA': '227',
+          'Pierce County, GA': '229',
+          'Pike County, GA': '231',
+          'Polk County, GA': '233',
+          'Pulaski County, GA': '235',
+          'Putnam County, GA': '237',
+          'Quitman County, GA': '239',
+          'Rabun County, GA': '241',
+          'Randolph County, GA': '243',
+          'Richmond County, GA': '245',
+          'Rockdale County, GA': '247',
+          'Schley County, GA': '249',
+          'Screven County, GA': '251',
+          'Seminole County, GA': '253',
+          'Spalding County, GA': '255',
+          'Stephens County, GA': '257',
+          'Stewart County, GA': '259',
+          'Sumter County, GA': '261',
+          'Talbot County, GA': '263',
+          'Taliaferro County, GA': '265',
+          'Tattnall County, GA': '267',
+          'Taylor County, GA': '269',
+          'Telfair County, GA': '271',
+          'Terrell County, GA': '273',
+          'Thomas County, GA': '275',
+          'Tift County, GA': '277',
+          'Toombs County, GA': '279',
+          'Towns County, GA': '281',
+          'Treutlen County, GA': '283',
+          'Troup County, GA': '285',
+          'Turner County, GA': '287',
+          'Twiggs County, GA': '289',
+          'Union County, GA': '291',
+          'Upson County, GA': '293',
+          'Walker County, GA': '295',
+          'Walton County, GA': '297',
+          'Ware County, GA': '299',
+          'Warren County, GA': '301',
+          'Washington County, GA': '303',
+          'Wayne County, GA': '305',
+          'Webster County, GA': '307',
+          'Wheeler County, GA': '309',
+          'White County, GA': '311',
+          'Whitfield County, GA': '313',
+          'Wilcox County, GA': '315',
+          'Wilkes County, GA': '317',
+          'Wilkinson County, GA': '319',
+          'Worth County, GA': '321'},
+  '15': { 'Hawaii County, HI': '001',
+          'Honolulu County/city, HI': '003',
+          'Kauai County, HI': '007',
+          'Maui County, HI': '009'},
+  '16': { 'Ada County, ID': '001',
+          'Adams County, ID': '003',
+          'Bannock County, ID': '005',
+          'Bear Lake County, ID': '007',
+          'Benewah County, ID': '009',
+          'Bingham County, ID': '011',
+          'Blaine County, ID': '013',
+          'Boise County, ID': '015',
+          'Bonner County, ID': '017',
+          'Bonneville County, ID': '019',
+          'Boundary County, ID': '021',
+          'Butte County, ID': '023',
+          'Camas County, ID': '025',
+          'Canyon County, ID': '027',
+          'Caribou County, ID': '029',
+          'Cassia County, ID': '031',
+          'Clark County, ID': '033',
+          'Clearwater County, ID': '035',
+          'Custer County, ID': '037',
+          'Elmore County, ID': '039',
+          'Franklin County, ID': '041',
+          'Fremont County, ID': '043',
+          'Gem County, ID': '045',
+          'Gooding County, ID': '047',
+          'Idaho County, ID': '049',
+          'Jefferson County, ID': '051',
+          'Jerome County, ID': '053',
+          'Kootenai County, ID': '055',
+          'Latah County, ID': '057',
+          'Lemhi County, ID': '059',
+          'Lewis County, ID': '061',
+          'Lincoln County, ID': '063',
+          'Madison County, ID': '065',
+          'Minidoka County, ID': '067',
+          'Nez Perce County, ID': '069',
+          'Oneida County, ID': '071',
+          'Owyhee County, ID': '073',
+          'Payette County, ID': '075',
+          'Power County, ID': '077',
+          'Shoshone County, ID': '079',
+          'Teton County, ID': '081',
+          'Twin Falls County, ID': '083',
+          'Valley County, ID': '085',
+          'Washington County, ID': '087'},
+  '17': { 'Adams County, IL': '001',
+          'Alexander County, IL': '003',
+          'Bond County, IL': '005',
+          'Boone County, IL': '007',
+          'Brown County, IL': '009',
+          'Bureau County, IL': '011',
+          'Calhoun County, IL': '013',
+          'Carroll County, IL': '015',
+          'Cass County, IL': '017',
+          'Champaign County, IL': '019',
+          'Christian County, IL': '021',
+          'Clark County, IL': '023',
+          'Clay County, IL': '025',
+          'Clinton County, IL': '027',
+          'Coles County, IL': '029',
+          'Cook County, IL': '031',
+          'Crawford County, IL': '033',
+          'Cumberland County, IL': '035',
+          'De Witt County, IL': '039',
+          'DeKalb County, IL': '037',
+          'Douglas County, IL': '041',
+          'DuPage County, IL': '043',
+          'Edgar County, IL': '045',
+          'Edwards County, IL': '047',
+          'Effingham County, IL': '049',
+          'Fayette County, IL': '051',
+          'Ford County, IL': '053',
+          'Franklin County, IL': '055',
+          'Fulton County, IL': '057',
+          'Gallatin County, IL': '059',
+          'Greene County, IL': '061',
+          'Grundy County, IL': '063',
+          'Hamilton County, IL': '065',
+          'Hancock County, IL': '067',
+          'Hardin County, IL': '069',
+          'Henderson County, IL': '071',
+          'Henry County, IL': '073',
+          'Iroquois County, IL': '075',
+          'Jackson County, IL': '077',
+          'Jasper County, IL': '079',
+          'Jefferson County, IL': '081',
+          'Jersey County, IL': '083',
+          'Jo Daviess County, IL': '085',
+          'Johnson County, IL': '087',
+          'Kane County, IL': '089',
+          'Kankakee County, IL': '091',
+          'Kendall County, IL': '093',
+          'Knox County, IL': '095',
+          'La Salle County, IL': '099',
+          'Lake County, IL': '097',
+          'Lawrence County, IL': '101',
+          'Lee County, IL': '103',
+          'Livingston County, IL': '105',
+          'Logan County, IL': '107',
+          'Macon County, IL': '115',
+          'Macoupin County, IL': '117',
+          'Madison County, IL': '119',
+          'Marion County, IL': '121',
+          'Marshall County, IL': '123',
+          'Mason County, IL': '125',
+          'Massac County, IL': '127',
+          'McDonough County, IL': '109',
+          'McHenry County, IL': '111',
+          'McLean County, IL': '113',
+          'Menard County, IL': '129',
+          'Mercer County, IL': '131',
+          'Monroe County, IL': '133',
+          'Montgomery County, IL': '135',
+          'Morgan County, IL': '137',
+          'Moultrie County, IL': '139',
+          'Ogle County, IL': '141',
+          'Peoria County, IL': '143',
+          'Perry County, IL': '145',
+          'Piatt County, IL': '147',
+          'Pike County, IL': '149',
+          'Pope County, IL': '151',
+          'Pulaski County, IL': '153',
+          'Putnam County, IL': '155',
+          'Randolph County, IL': '157',
+          'Richland County, IL': '159',
+          'Rock Island County, IL': '161',
+          'Saline County, IL': '165',
+          'Sangamon County, IL': '167',
+          'Schuyler County, IL': '169',
+          'Scott County, IL': '171',
+          'Shelby County, IL': '173',
+          'St. Clair County, IL': '163',
+          'Stark County, IL': '175',
+          'Stephenson County, IL': '177',
+          'Tazewell County, IL': '179',
+          'Union County, IL': '181',
+          'Vermilion County, IL': '183',
+          'Wabash County, IL': '185',
+          'Warren County, IL': '187',
+          'Washington County, IL': '189',
+          'Wayne County, IL': '191',
+          'White County, IL': '193',
+          'Whiteside County, IL': '195',
+          'Will County, IL': '197',
+          'Williamson County, IL': '199',
+          'Winnebago County, IL': '201',
+          'Woodford County, IL': '203'},
+  '18': { 'Adams County, IN': '001',
+          'Allen County, IN': '003',
+          'Bartholomew County, IN': '005',
+          'Benton County, IN': '007',
+          'Blackford County, IN': '009',
+          'Boone County, IN': '011',
+          'Brown County, IN': '013',
+          'Carroll County, IN': '015',
+          'Cass County, IN': '017',
+          'Clark County, IN': '019',
+          'Clay County, IN': '021',
+          'Clinton County, IN': '023',
+          'Crawford County, IN': '025',
+          'Daviess County, IN': '027',
+          'DeKalb County, IN': '033',
+          'Dearborn County, IN': '029',
+          'Decatur County, IN': '031',
+          'Delaware County, IN': '035',
+          'Dubois County, IN': '037',
+          'Elkhart County, IN': '039',
+          'Fayette County, IN': '041',
+          'Floyd County, IN': '043',
+          'Fountain County, IN': '045',
+          'Franklin County, IN': '047',
+          'Fulton County, IN': '049',
+          'Gibson County, IN': '051',
+          'Grant County, IN': '053',
+          'Greene County, IN': '055',
+          'Hamilton County, IN': '057',
+          'Hancock County, IN': '059',
+          'Harrison County, IN': '061',
+          'Hendricks County, IN': '063',
+          'Henry County, IN': '065',
+          'Howard County, IN': '067',
+          'Huntington County, IN': '069',
+          'Jackson County, IN': '071',
+          'Jasper County, IN': '073',
+          'Jay County, IN': '075',
+          'Jefferson County, IN': '077',
+          'Jennings County, IN': '079',
+          'Johnson County, IN': '081',
+          'Knox County, IN': '083',
+          'Kosciusko County, IN': '085',
+          'LaGrange County, IN': '087',
+          'LaPorte County, IN': '091',
+          'Lake County, IN': '089',
+          'Lawrence County, IN': '093',
+          'Madison County, IN': '095',
+          'Marion County, IN': '097',
+          'Marshall County, IN': '099',
+          'Martin County, IN': '101',
+          'Miami County, IN': '103',
+          'Monroe County, IN': '105',
+          'Montgomery County, IN': '107',
+          'Morgan County, IN': '109',
+          'Newton County, IN': '111',
+          'Noble County, IN': '113',
+          'Ohio County, IN': '115',
+          'Orange County, IN': '117',
+          'Owen County, IN': '119',
+          'Parke County, IN': '121',
+          'Perry County, IN': '123',
+          'Pike County, IN': '125',
+          'Porter County, IN': '127',
+          'Posey County, IN': '129',
+          'Pulaski County, IN': '131',
+          'Putnam County, IN': '133',
+          'Randolph County, IN': '135',
+          'Ripley County, IN': '137',
+          'Rush County, IN': '139',
+          'Scott County, IN': '143',
+          'Shelby County, IN': '145',
+          'Spencer County, IN': '147',
+          'St. Joseph County, IN': '141',
+          'Starke County, IN': '149',
+          'Steuben County, IN': '151',
+          'Sullivan County, IN': '153',
+          'Switzerland County, IN': '155',
+          'Tippecanoe County, IN': '157',
+          'Tipton County, IN': '159',
+          'Union County, IN': '161',
+          'Vanderburgh County, IN': '163',
+          'Vermillion County, IN': '165',
+          'Vigo County, IN': '167',
+          'Wabash County, IN': '169',
+          'Warren County, IN': '171',
+          'Warrick County, IN': '173',
+          'Washington County, IN': '175',
+          'Wayne County, IN': '177',
+          'Wells County, IN': '179',
+          'White County, IN': '181',
+          'Whitley County, IN': '183'},
+  '19': { 'Adair County, IA': '001',
+          'Adams County, IA': '003',
+          'Allamakee County, IA': '005',
+          'Appanoose County, IA': '007',
+          'Audubon County, IA': '009',
+          'Benton County, IA': '011',
+          'Black Hawk County, IA': '013',
+          'Boone County, IA': '015',
+          'Bremer County, IA': '017',
+          'Buchanan County, IA': '019',
+          'Buena Vista County, IA': '021',
+          'Butler County, IA': '023',
+          'Calhoun County, IA': '025',
+          'Carroll County, IA': '027',
+          'Cass County, IA': '029',
+          'Cedar County, IA': '031',
+          'Cerro Gordo County, IA': '033',
+          'Cherokee County, IA': '035',
+          'Chickasaw County, IA': '037',
+          'Clarke County, IA': '039',
+          'Clay County, IA': '041',
+          'Clayton County, IA': '043',
+          'Clinton County, IA': '045',
+          'Crawford County, IA': '047',
+          'Dallas County, IA': '049',
+          'Davis County, IA': '051',
+          'Decatur County, IA': '053',
+          'Delaware County, IA': '055',
+          'Des Moines County, IA': '057',
+          'Dickinson County, IA': '059',
+          'Dubuque County, IA': '061',
+          'Emmet County, IA': '063',
+          'Fayette County, IA': '065',
+          'Floyd County, IA': '067',
+          'Franklin County, IA': '069',
+          'Fremont County, IA': '071',
+          'Greene County, IA': '073',
+          'Grundy County, IA': '075',
+          'Guthrie County, IA': '077',
+          'Hamilton County, IA': '079',
+          'Hancock County, IA': '081',
+          'Hardin County, IA': '083',
+          'Harrison County, IA': '085',
+          'Henry County, IA': '087',
+          'Howard County, IA': '089',
+          'Humboldt County, IA': '091',
+          'Ida County, IA': '093',
+          'Iowa County, IA': '095',
+          'Jackson County, IA': '097',
+          'Jasper County, IA': '099',
+          'Jefferson County, IA': '101',
+          'Johnson County, IA': '103',
+          'Jones County, IA': '105',
+          'Keokuk County, IA': '107',
+          'Kossuth County, IA': '109',
+          'Lee County, IA': '111',
+          'Linn County, IA': '113',
+          'Louisa County, IA': '115',
+          'Lucas County, IA': '117',
+          'Lyon County, IA': '119',
+          'Madison County, IA': '121',
+          'Mahaska County, IA': '123',
+          'Marion County, IA': '125',
+          'Marshall County, IA': '127',
+          'Mills County, IA': '129',
+          'Mitchell County, IA': '131',
+          'Monona County, IA': '133',
+          'Monroe County, IA': '135',
+          'Montgomery County, IA': '137',
+          'Muscatine County, IA': '139',
+          "O'Brien County, IA": '141',
+          'Osceola County, IA': '143',
+          'Page County, IA': '145',
+          'Palo Alto County, IA': '147',
+          'Plymouth County, IA': '149',
+          'Pocahontas County, IA': '151',
+          'Polk County, IA': '153',
+          'Pottawattamie County, IA': '155',
+          'Poweshiek County, IA': '157',
+          'Ringgold County, IA': '159',
+          'Sac County, IA': '161',
+          'Scott County, IA': '163',
+          'Shelby County, IA': '165',
+          'Sioux County, IA': '167',
+          'Story County, IA': '169',
+          'Tama County, IA': '171',
+          'Taylor County, IA': '173',
+          'Union County, IA': '175',
+          'Van Buren County, IA': '177',
+          'Wapello County, IA': '179',
+          'Warren County, IA': '181',
+          'Washington County, IA': '183',
+          'Wayne County, IA': '185',
+          'Webster County, IA': '187',
+          'Winnebago County, IA': '189',
+          'Winneshiek County, IA': '191',
+          'Woodbury County, IA': '193',
+          'Worth County, IA': '195',
+          'Wright County, IA': '197'},
+  '20': { 'Allen County, KS': '001',
+          'Anderson County, KS': '003',
+          'Atchison County, KS': '005',
+          'Barber County, KS': '007',
+          'Barton County, KS': '009',
+          'Bourbon County, KS': '011',
+          'Brown County, KS': '013',
+          'Butler County, KS': '015',
+          'Chase County, KS': '017',
+          'Chautauqua County, KS': '019',
+          'Cherokee County, KS': '021',
+          'Cheyenne County, KS': '023',
+          'Clark County, KS': '025',
+          'Clay County, KS': '027',
+          'Cloud County, KS': '029',
+          'Coffey County, KS': '031',
+          'Comanche County, KS': '033',
+          'Cowley County, KS': '035',
+          'Crawford County, KS': '037',
+          'Decatur County, KS': '039',
+          'Dickinson County, KS': '041',
+          'Doniphan County, KS': '043',
+          'Douglas County, KS': '045',
+          'Edwards County, KS': '047',
+          'Elk County, KS': '049',
+          'Ellis County, KS': '051',
+          'Ellsworth County, KS': '053',
+          'Finney County, KS': '055',
+          'Ford County, KS': '057',
+          'Franklin County, KS': '059',
+          'Geary County, KS': '061',
+          'Gove County, KS': '063',
+          'Graham County, KS': '065',
+          'Grant County, KS': '067',
+          'Gray County, KS': '069',
+          'Greeley County, KS': '071',
+          'Greenwood County, KS': '073',
+          'Hamilton County, KS': '075',
+          'Harper County, KS': '077',
+          'Harvey County, KS': '079',
+          'Haskell County, KS': '081',
+          'Hodgeman County, KS': '083',
+          'Jackson County, KS': '085',
+          'Jefferson County, KS': '087',
+          'Jewell County, KS': '089',
+          'Johnson County, KS': '091',
+          'Kearny County, KS': '093',
+          'Kingman County, KS': '095',
+          'Kiowa County, KS': '097',
+          'Labette County, KS': '099',
+          'Lane County, KS': '101',
+          'Leavenworth County, KS': '103',
+          'Lincoln County, KS': '105',
+          'Linn County, KS': '107',
+          'Logan County, KS': '109',
+          'Lyon County, KS': '111',
+          'Marion County, KS': '115',
+          'Marshall County, KS': '117',
+          'McPherson County, KS': '113',
+          'Meade County, KS': '119',
+          'Miami County, KS': '121',
+          'Mitchell County, KS': '123',
+          'Montgomery County, KS': '125',
+          'Morris County, KS': '127',
+          'Morton County, KS': '129',
+          'Nemaha County, KS': '131',
+          'Neosho County, KS': '133',
+          'Ness County, KS': '135',
+          'Norton County, KS': '137',
+          'Osage County, KS': '139',
+          'Osborne County, KS': '141',
+          'Ottawa County, KS': '143',
+          'Pawnee County, KS': '145',
+          'Phillips County, KS': '147',
+          'Pottawatomie County, KS': '149',
+          'Pratt County, KS': '151',
+          'Rawlins County, KS': '153',
+          'Reno County, KS': '155',
+          'Republic County, KS': '157',
+          'Rice County, KS': '159',
+          'Riley County, KS': '161',
+          'Rooks County, KS': '163',
+          'Rush County, KS': '165',
+          'Russell County, KS': '167',
+          'Saline County, KS': '169',
+          'Scott County, KS': '171',
+          'Sedgwick County, KS': '173',
+          'Seward County, KS': '175',
+          'Shawnee County, KS': '177',
+          'Sheridan County, KS': '179',
+          'Sherman County, KS': '181',
+          'Smith County, KS': '183',
+          'Stafford County, KS': '185',
+          'Stanton County, KS': '187',
+          'Stevens County, KS': '189',
+          'Sumner County, KS': '191',
+          'Thomas County, KS': '193',
+          'Trego County, KS': '195',
+          'Wabaunsee County, KS': '197',
+          'Wallace County, KS': '199',
+          'Washington County, KS': '201',
+          'Wichita County, KS': '203',
+          'Wilson County, KS': '205',
+          'Woodson County, KS': '207',
+          'Wyandotte County, KS': '209'},
+  '21': { 'Adair County, KY': '001',
+          'Allen County, KY': '003',
+          'Anderson County, KY': '005',
+          'Ballard County, KY': '007',
+          'Barren County, KY': '009',
+          'Bath County, KY': '011',
+          'Bell County, KY': '013',
+          'Boone County, KY': '015',
+          'Bourbon County, KY': '017',
+          'Boyd County, KY': '019',
+          'Boyle County, KY': '021',
+          'Bracken County, KY': '023',
+          'Breathitt County, KY': '025',
+          'Breckinridge County, KY': '027',
+          'Bullitt County, KY': '029',
+          'Butler County, KY': '031',
+          'Caldwell County, KY': '033',
+          'Calloway County, KY': '035',
+          'Campbell County, KY': '037',
+          'Carlisle County, KY': '039',
+          'Carroll County, KY': '041',
+          'Carter County, KY': '043',
+          'Casey County, KY': '045',
+          'Christian County, KY': '047',
+          'Clark County, KY': '049',
+          'Clay County, KY': '051',
+          'Clinton County, KY': '053',
+          'Crittenden County, KY': '055',
+          'Cumberland County, KY': '057',
+          'Daviess County, KY': '059',
+          'Edmonson County, KY': '061',
+          'Elliott County, KY': '063',
+          'Estill County, KY': '065',
+          'Fayette County, KY': '067',
+          'Fleming County, KY': '069',
+          'Floyd County, KY': '071',
+          'Franklin County, KY': '073',
+          'Fulton County, KY': '075',
+          'Gallatin County, KY': '077',
+          'Garrard County, KY': '079',
+          'Grant County, KY': '081',
+          'Graves County, KY': '083',
+          'Grayson County, KY': '085',
+          'Green County, KY': '087',
+          'Greenup County, KY': '089',
+          'Hancock County, KY': '091',
+          'Hardin County, KY': '093',
+          'Harlan County, KY': '095',
+          'Harrison County, KY': '097',
+          'Hart County, KY': '099',
+          'Henderson County, KY': '101',
+          'Henry County, KY': '103',
+          'Hickman County, KY': '105',
+          'Hopkins County, KY': '107',
+          'Jackson County, KY': '109',
+          'Jefferson County, KY': '111',
+          'Jessamine County, KY': '113',
+          'Johnson County, KY': '115',
+          'Kenton County, KY': '117',
+          'Knott County, KY': '119',
+          'Knox County, KY': '121',
+          'Larue County, KY': '123',
+          'Laurel County, KY': '125',
+          'Lawrence County, KY': '127',
+          'Lee County, KY': '129',
+          'Leslie County, KY': '131',
+          'Letcher County, KY': '133',
+          'Lewis County, KY': '135',
+          'Lincoln County, KY': '137',
+          'Livingston County, KY': '139',
+          'Logan County, KY': '141',
+          'Lyon County, KY': '143',
+          'Madison County, KY': '151',
+          'Magoffin County, KY': '153',
+          'Marion County, KY': '155',
+          'Marshall County, KY': '157',
+          'Martin County, KY': '159',
+          'Mason County, KY': '161',
+          'McCracken County, KY': '145',
+          'McCreary County, KY': '147',
+          'McLean County, KY': '149',
+          'Meade County, KY': '163',
+          'Menifee County, KY': '165',
+          'Mercer County, KY': '167',
+          'Metcalfe County, KY': '169',
+          'Monroe County, KY': '171',
+          'Montgomery County, KY': '173',
+          'Morgan County, KY': '175',
+          'Muhlenberg County, KY': '177',
+          'Nelson County, KY': '179',
+          'Nicholas County, KY': '181',
+          'Ohio County, KY': '183',
+          'Oldham County, KY': '185',
+          'Owen County, KY': '187',
+          'Owsley County, KY': '189',
+          'Pendleton County, KY': '191',
+          'Perry County, KY': '193',
+          'Pike County, KY': '195',
+          'Powell County, KY': '197',
+          'Pulaski County, KY': '199',
+          'Robertson County, KY': '201',
+          'Rockcastle County, KY': '203',
+          'Rowan County, KY': '205',
+          'Russell County, KY': '207',
+          'Scott County, KY': '209',
+          'Shelby County, KY': '211',
+          'Simpson County, KY': '213',
+          'Spencer County, KY': '215',
+          'Taylor County, KY': '217',
+          'Todd County, KY': '219',
+          'Trigg County, KY': '221',
+          'Trimble County, KY': '223',
+          'Union County, KY': '225',
+          'Warren County, KY': '227',
+          'Washington County, KY': '229',
+          'Wayne County, KY': '231',
+          'Webster County, KY': '233',
+          'Whitley County, KY': '235',
+          'Wolfe County, KY': '237',
+          'Woodford County, KY': '239'},
+  '22': { 'Acadia Parish, LA': '001',
+          'Allen Parish, LA': '003',
+          'Ascension Parish, LA': '005',
+          'Assumption Parish, LA': '007',
+          'Avoyelles Parish, LA': '009',
+          'Beauregard Parish, LA': '011',
+          'Bienville Parish, LA': '013',
+          'Bossier Parish, LA': '015',
+          'Caddo Parish, LA': '017',
+          'Calcasieu Parish, LA': '019',
+          'Caldwell Parish, LA': '021',
+          'Cameron Parish, LA': '023',
+          'Catahoula Parish, LA': '025',
+          'Claiborne Parish, LA': '027',
+          'Concordia Parish, LA': '029',
+          'De Soto Parish, LA': '031',
+          'East Baton Rouge Parish, LA': '033',
+          'East Carroll Parish, LA': '035',
+          'East Feliciana Parish, LA': '037',
+          'Evangeline Parish, LA': '039',
+          'Franklin Parish, LA': '041',
+          'Grant Parish, LA': '043',
+          'Iberia Parish, LA': '045',
+          'Iberville Parish, LA': '047',
+          'Jackson Parish, LA': '049',
+          'Jefferson Davis Parish, LA': '053',
+          'Jefferson Parish, LA': '051',
+          'La Salle Parish, LA': '059',
+          'Lafayette Parish, LA': '055',
+          'Lafourche Parish, LA': '057',
+          'Lincoln Parish, LA': '061',
+          'Livingston Parish, LA': '063',
+          'Madison Parish, LA': '065',
+          'Morehouse Parish, LA': '067',
+          'Natchitoches Parish, LA': '069',
+          'Orleans Parish, LA': '071',
+          'Ouachita Parish, LA': '073',
+          'Plaquemines Parish, LA': '075',
+          'Pointe Coupee Parish, LA': '077',
+          'Rapides Parish, LA': '079',
+          'Red River Parish, LA': '081',
+          'Richland Parish, LA': '083',
+          'Sabine Parish, LA': '085',
+          'St. Bernard Parish, LA': '087',
+          'St. Charles Parish, LA': '089',
+          'St. Helena Parish, LA': '091',
+          'St. James Parish, LA': '093',
+          'St. John the Baptist Parish, LA': '095',
+          'St. Landry Parish, LA': '097',
+          'St. Martin Parish, LA': '099',
+          'St. Mary Parish, LA': '101',
+          'St. Tammany Parish, LA': '103',
+          'Tangipahoa Parish, LA': '105',
+          'Tensas Parish, LA': '107',
+          'Terrebonne Parish, LA': '109',
+          'Union Parish, LA': '111',
+          'Vermilion Parish, LA': '113',
+          'Vernon Parish, LA': '115',
+          'Washington Parish, LA': '117',
+          'Webster Parish, LA': '119',
+          'West Baton Rouge Parish, LA': '121',
+          'West Carroll Parish, LA': '123',
+          'West Feliciana Parish, LA': '125',
+          'Winn Parish, LA': '127'},
+  '23': { 'Androscoggin County, ME': '001',
+          'Aroostook County, ME': '003',
+          'Cumberland County, ME': '005',
+          'Franklin County, ME': '007',
+          'Hancock County, ME': '009',
+          'Kennebec County, ME': '011',
+          'Knox County, ME': '013',
+          'Lincoln County, ME': '015',
+          'Oxford County, ME': '017',
+          'Penobscot County, ME': '019',
+          'Piscataquis County, ME': '021',
+          'Sagadahoc County, ME': '023',
+          'Somerset County, ME': '025',
+          'Waldo County, ME': '027',
+          'Washington County, ME': '029',
+          'York County, ME': '031'},
+  '24': { 'Allegany County, MD': '001',
+          'Anne Arundel County, MD': '003',
+          'Baltimore County, MD': '005',
+          'Baltimore city, MD': '510',
+          'Calvert County, MD': '009',
+          'Caroline County, MD': '011',
+          'Carroll County, MD': '013',
+          'Cecil County, MD': '015',
+          'Charles County, MD': '017',
+          'Dorchester County, MD': '019',
+          'Frederick County, MD': '021',
+          'Garrett County, MD': '023',
+          'Harford County, MD': '025',
+          'Howard County, MD': '027',
+          'Kent County, MD': '029',
+          'Montgomery County, MD': '031',
+          "Prince George's County, MD": '033',
+          "Queen Anne's County, MD": '035',
+          'Somerset County, MD': '039',
+          "St. Mary's County, MD": '037',
+          'Talbot County, MD': '041',
+          'Washington County, MD': '043',
+          'Wicomico County, MD': '045',
+          'Worcester County, MD': '047'},
+  '25': { 'Barnstable County, MA': '001',
+          'Berkshire County, MA': '003',
+          'Bristol County, MA': '005',
+          'Dukes County, MA': '007',
+          'Essex County, MA': '009',
+          'Franklin County, MA': '011',
+          'Hampden County, MA': '013',
+          'Hampshire County, MA': '015',
+          'Middlesex County, MA': '017',
+          'Nantucket County/town, MA': '019',
+          'Norfolk County, MA': '021',
+          'Plymouth County, MA': '023',
+          'Suffolk County, MA': '025',
+          'Worcester County, MA': '027'},
+  '26': { 'Alcona County, MI': '001',
+          'Alger County, MI': '003',
+          'Allegan County, MI': '005',
+          'Alpena County, MI': '007',
+          'Antrim County, MI': '009',
+          'Arenac County, MI': '011',
+          'Baraga County, MI': '013',
+          'Barry County, MI': '015',
+          'Bay County, MI': '017',
+          'Benzie County, MI': '019',
+          'Berrien County, MI': '021',
+          'Branch County, MI': '023',
+          'Calhoun County, MI': '025',
+          'Cass County, MI': '027',
+          'Charlevoix County, MI': '029',
+          'Cheboygan County, MI': '031',
+          'Chippewa County, MI': '033',
+          'Clare County, MI': '035',
+          'Clinton County, MI': '037',
+          'Crawford County, MI': '039',
+          'Delta County, MI': '041',
+          'Dickinson County, MI': '043',
+          'Eaton County, MI': '045',
+          'Emmet County, MI': '047',
+          'Genesee County, MI': '049',
+          'Gladwin County, MI': '051',
+          'Gogebic County, MI': '053',
+          'Grand Traverse County, MI': '055',
+          'Gratiot County, MI': '057',
+          'Hillsdale County, MI': '059',
+          'Houghton County, MI': '061',
+          'Huron County, MI': '063',
+          'Ingham County, MI': '065',
+          'Ionia County, MI': '067',
+          'Iosco County, MI': '069',
+          'Iron County, MI': '071',
+          'Isabella County, MI': '073',
+          'Jackson County, MI': '075',
+          'Kalamazoo County, MI': '077',
+          'Kalkaska County, MI': '079',
+          'Kent County, MI': '081',
+          'Keweenaw County, MI': '083',
+          'Lake County, MI': '085',
+          'Lapeer County, MI': '087',
+          'Leelanau County, MI': '089',
+          'Lenawee County, MI': '091',
+          'Livingston County, MI': '093',
+          'Luce County, MI': '095',
+          'Mackinac County, MI': '097',
+          'Macomb County, MI': '099',
+          'Manistee County, MI': '101',
+          'Marquette County, MI': '103',
+          'Mason County, MI': '105',
+          'Mecosta County, MI': '107',
+          'Menominee County, MI': '109',
+          'Midland County, MI': '111',
+          'Missaukee County, MI': '113',
+          'Monroe County, MI': '115',
+          'Montcalm County, MI': '117',
+          'Montmorency County, MI': '119',
+          'Muskegon County, MI': '121',
+          'Newaygo County, MI': '123',
+          'Oakland County, MI': '125',
+          'Oceana County, MI': '127',
+          'Ogemaw County, MI': '129',
+          'Ontonagon County, MI': '131',
+          'Osceola County, MI': '133',
+          'Oscoda County, MI': '135',
+          'Otsego County, MI': '137',
+          'Ottawa County, MI': '139',
+          'Presque Isle County, MI': '141',
+          'Roscommon County, MI': '143',
+          'Saginaw County, MI': '145',
+          'Sanilac County, MI': '151',
+          'Schoolcraft County, MI': '153',
+          'Shiawassee County, MI': '155',
+          'St. Clair County, MI': '147',
+          'St. Joseph County, MI': '149',
+          'Tuscola County, MI': '157',
+          'Van Buren County, MI': '159',
+          'Washtenaw County, MI': '161',
+          'Wayne County, MI': '163',
+          'Wexford County, MI': '165'},
+  '27': { 'Aitkin County, MN': '001',
+          'Anoka County, MN': '003',
+          'Becker County, MN': '005',
+          'Beltrami County, MN': '007',
+          'Benton County, MN': '009',
+          'Big Stone County, MN': '011',
+          'Blue Earth County, MN': '013',
+          'Brown County, MN': '015',
+          'Carlton County, MN': '017',
+          'Carver County, MN': '019',
+          'Cass County, MN': '021',
+          'Chippewa County, MN': '023',
+          'Chisago County, MN': '025',
+          'Clay County, MN': '027',
+          'Clearwater County, MN': '029',
+          'Cook County, MN': '031',
+          'Cottonwood County, MN': '033',
+          'Crow Wing County, MN': '035',
+          'Dakota County, MN': '037',
+          'Dodge County, MN': '039',
+          'Douglas County, MN': '041',
+          'Faribault County, MN': '043',
+          'Fillmore County, MN': '045',
+          'Freeborn County, MN': '047',
+          'Goodhue County, MN': '049',
+          'Grant County, MN': '051',
+          'Hennepin County, MN': '053',
+          'Houston County, MN': '055',
+          'Hubbard County, MN': '057',
+          'Isanti County, MN': '059',
+          'Itasca County, MN': '061',
+          'Jackson County, MN': '063',
+          'Kanabec County, MN': '065',
+          'Kandiyohi County, MN': '067',
+          'Kittson County, MN': '069',
+          'Koochiching County, MN': '071',
+          'Lac qui Parle County, MN': '073',
+          'Lake County, MN': '075',
+          'Lake of the Woods County, MN': '077',
+          'Le Sueur County, MN': '079',
+          'Lincoln County, MN': '081',
+          'Lyon County, MN': '083',
+          'Mahnomen County, MN': '087',
+          'Marshall County, MN': '089',
+          'Martin County, MN': '091',
+          'McLeod County, MN': '085',
+          'Meeker County, MN': '093',
+          'Mille Lacs County, MN': '095',
+          'Morrison County, MN': '097',
+          'Mower County, MN': '099',
+          'Murray County, MN': '101',
+          'Nicollet County, MN': '103',
+          'Nobles County, MN': '105',
+          'Norman County, MN': '107',
+          'Olmsted County, MN': '109',
+          'Otter Tail County, MN': '111',
+          'Pennington County, MN': '113',
+          'Pine County, MN': '115',
+          'Pipestone County, MN': '117',
+          'Polk County, MN': '119',
+          'Pope County, MN': '121',
+          'Ramsey County, MN': '123',
+          'Red Lake County, MN': '125',
+          'Redwood County, MN': '127',
+          'Renville County, MN': '129',
+          'Rice County, MN': '131',
+          'Rock County, MN': '133',
+          'Roseau County, MN': '135',
+          'Scott County, MN': '139',
+          'Sherburne County, MN': '141',
+          'Sibley County, MN': '143',
+          'St. Louis County, MN': '137',
+          'Stearns County, MN': '145',
+          'Steele County, MN': '147',
+          'Stevens County, MN': '149',
+          'Swift County, MN': '151',
+          'Todd County, MN': '153',
+          'Traverse County, MN': '155',
+          'Wabasha County, MN': '157',
+          'Wadena County, MN': '159',
+          'Waseca County, MN': '161',
+          'Washington County, MN': '163',
+          'Watonwan County, MN': '165',
+          'Wilkin County, MN': '167',
+          'Winona County, MN': '169',
+          'Wright County, MN': '171',
+          'Yellow Medicine County, MN': '173'},
+  '28': { 'Adams County, MS': '001',
+          'Alcorn County, MS': '003',
+          'Amite County, MS': '005',
+          'Attala County, MS': '007',
+          'Benton County, MS': '009',
+          'Bolivar County, MS': '011',
+          'Calhoun County, MS': '013',
+          'Carroll County, MS': '015',
+          'Chickasaw County, MS': '017',
+          'Choctaw County, MS': '019',
+          'Claiborne County, MS': '021',
+          'Clarke County, MS': '023',
+          'Clay County, MS': '025',
+          'Coahoma County, MS': '027',
+          'Copiah County, MS': '029',
+          'Covington County, MS': '031',
+          'DeSoto County, MS': '033',
+          'Forrest County, MS': '035',
+          'Franklin County, MS': '037',
+          'George County, MS': '039',
+          'Greene County, MS': '041',
+          'Grenada County, MS': '043',
+          'Hancock County, MS': '045',
+          'Harrison County, MS': '047',
+          'Hinds County, MS': '049',
+          'Holmes County, MS': '051',
+          'Humphreys County, MS': '053',
+          'Issaquena County, MS': '055',
+          'Itawamba County, MS': '057',
+          'Jackson County, MS': '059',
+          'Jasper County, MS': '061',
+          'Jefferson County, MS': '063',
+          'Jefferson Davis County, MS': '065',
+          'Jones County, MS': '067',
+          'Kemper County, MS': '069',
+          'Lafayette County, MS': '071',
+          'Lamar County, MS': '073',
+          'Lauderdale County, MS': '075',
+          'Lawrence County, MS': '077',
+          'Leake County, MS': '079',
+          'Lee County, MS': '081',
+          'Leflore County, MS': '083',
+          'Lincoln County, MS': '085',
+          'Lowndes County, MS': '087',
+          'Madison County, MS': '089',
+          'Marion County, MS': '091',
+          'Marshall County, MS': '093',
+          'Monroe County, MS': '095',
+          'Montgomery County, MS': '097',
+          'Neshoba County, MS': '099',
+          'Newton County, MS': '101',
+          'Noxubee County, MS': '103',
+          'Oktibbeha County, MS': '105',
+          'Panola County, MS': '107',
+          'Pearl River County, MS': '109',
+          'Perry County, MS': '111',
+          'Pike County, MS': '113',
+          'Pontotoc County, MS': '115',
+          'Prentiss County, MS': '117',
+          'Quitman County, MS': '119',
+          'Rankin County, MS': '121',
+          'Scott County, MS': '123',
+          'Sharkey County, MS': '125',
+          'Simpson County, MS': '127',
+          'Smith County, MS': '129',
+          'Stone County, MS': '131',
+          'Sunflower County, MS': '133',
+          'Tallahatchie County, MS': '135',
+          'Tate County, MS': '137',
+          'Tippah County, MS': '139',
+          'Tishomingo County, MS': '141',
+          'Tunica County, MS': '143',
+          'Union County, MS': '145',
+          'Walthall County, MS': '147',
+          'Warren County, MS': '149',
+          'Washington County, MS': '151',
+          'Wayne County, MS': '153',
+          'Webster County, MS': '155',
+          'Wilkinson County, MS': '157',
+          'Winston County, MS': '159',
+          'Yalobusha County, MS': '161',
+          'Yazoo County, MS': '163'},
+  '29': { 'Adair County, MO': '001',
+          'Andrew County, MO': '003',
+          'Atchison County, MO': '005',
+          'Audrain County, MO': '007',
+          'Barry County, MO': '009',
+          'Barton County, MO': '011',
+          'Bates County, MO': '013',
+          'Benton County, MO': '015',
+          'Bollinger County, MO': '017',
+          'Boone County, MO': '019',
+          'Buchanan County, MO': '021',
+          'Butler County, MO': '023',
+          'Caldwell County, MO': '025',
+          'Callaway County, MO': '027',
+          'Camden County, MO': '029',
+          'Cape Girardeau County, MO': '031',
+          'Carroll County, MO': '033',
+          'Carter County, MO': '035',
+          'Cass County, MO': '037',
+          'Cedar County, MO': '039',
+          'Chariton County, MO': '041',
+          'Christian County, MO': '043',
+          'Clark County, MO': '045',
+          'Clay County, MO': '047',
+          'Clinton County, MO': '049',
+          'Cole County, MO': '051',
+          'Cooper County, MO': '053',
+          'Crawford County, MO': '055',
+          'Dade County, MO': '057',
+          'Dallas County, MO': '059',
+          'Daviess County, MO': '061',
+          'DeKalb County, MO': '063',
+          'Dent County, MO': '065',
+          'Douglas County, MO': '067',
+          'Dunklin County, MO': '069',
+          'Franklin County, MO': '071',
+          'Gasconade County, MO': '073',
+          'Gentry County, MO': '075',
+          'Greene County, MO': '077',
+          'Grundy County, MO': '079',
+          'Harrison County, MO': '081',
+          'Henry County, MO': '083',
+          'Hickory County, MO': '085',
+          'Holt County, MO': '087',
+          'Howard County, MO': '089',
+          'Howell County, MO': '091',
+          'Iron County, MO': '093',
+          'Jackson County, MO': '095',
+          'Jasper County, MO': '097',
+          'Jefferson County, MO': '099',
+          'Johnson County, MO': '101',
+          'Knox County, MO': '103',
+          'Laclede County, MO': '105',
+          'Lafayette County, MO': '107',
+          'Lawrence County, MO': '109',
+          'Lewis County, MO': '111',
+          'Lincoln County, MO': '113',
+          'Linn County, MO': '115',
+          'Livingston County, MO': '117',
+          'Macon County, MO': '121',
+          'Madison County, MO': '123',
+          'Maries County, MO': '125',
+          'Marion County, MO': '127',
+          'McDonald County, MO': '119',
+          'Mercer County, MO': '129',
+          'Miller County, MO': '131',
+          'Mississippi County, MO': '133',
+          'Moniteau County, MO': '135',
+          'Monroe County, MO': '137',
+          'Montgomery County, MO': '139',
+          'Morgan County, MO': '141',
+          'New Madrid County, MO': '143',
+          'Newton County, MO': '145',
+          'Nodaway County, MO': '147',
+          'Oregon County, MO': '149',
+          'Osage County, MO': '151',
+          'Ozark County, MO': '153',
+          'Pemiscot County, MO': '155',
+          'Perry County, MO': '157',
+          'Pettis County, MO': '159',
+          'Phelps County, MO': '161',
+          'Pike County, MO': '163',
+          'Platte County, MO': '165',
+          'Polk County, MO': '167',
+          'Pulaski County, MO': '169',
+          'Putnam County, MO': '171',
+          'Ralls County, MO': '173',
+          'Randolph County, MO': '175',
+          'Ray County, MO': '177',
+          'Reynolds County, MO': '179',
+          'Ripley County, MO': '181',
+          'Saline County, MO': '195',
+          'Schuyler County, MO': '197',
+          'Scotland County, MO': '199',
+          'Scott County, MO': '201',
+          'Shannon County, MO': '203',
+          'Shelby County, MO': '205',
+          'St. Charles County, MO': '183',
+          'St. Clair County, MO': '185',
+          'St. Francois County, MO': '187',
+          'St. Louis County, MO': '189',
+          'St. Louis city, MO': '510',
+          'Ste. Genevieve County, MO': '186',
+          'Stoddard County, MO': '207',
+          'Stone County, MO': '209',
+          'Sullivan County, MO': '211',
+          'Taney County, MO': '213',
+          'Texas County, MO': '215',
+          'Vernon County, MO': '217',
+          'Warren County, MO': '219',
+          'Washington County, MO': '221',
+          'Wayne County, MO': '223',
+          'Webster County, MO': '225',
+          'Worth County, MO': '227',
+          'Wright County, MO': '229'},
+  '30': { 'Beaverhead County, MT': '001',
+          'Big Horn County, MT': '003',
+          'Blaine County, MT': '005',
+          'Broadwater County, MT': '007',
+          'Carbon County, MT': '009',
+          'Carter County, MT': '011',
+          'Cascade County, MT': '013',
+          'Chouteau County, MT': '015',
+          'Custer County, MT': '017',
+          'Daniels County, MT': '019',
+          'Dawson County, MT': '021',
+          'Deer Lodge County, MT': '023',
+          'Fallon County, MT': '025',
+          'Fergus County, MT': '027',
+          'Flathead County, MT': '029',
+          'Gallatin County, MT': '031',
+          'Garfield County, MT': '033',
+          'Glacier County, MT': '035',
+          'Golden Valley County, MT': '037',
+          'Granite County, MT': '039',
+          'Hill County, MT': '041',
+          'Jefferson County, MT': '043',
+          'Judith Basin County, MT': '045',
+          'Lake County, MT': '047',
+          'Lewis and Clark County, MT': '049',
+          'Liberty County, MT': '051',
+          'Lincoln County, MT': '053',
+          'Madison County, MT': '057',
+          'McCone County, MT': '055',
+          'Meagher County, MT': '059',
+          'Mineral County, MT': '061',
+          'Missoula County, MT': '063',
+          'Musselshell County, MT': '065',
+          'Park County, MT': '067',
+          'Petroleum County, MT': '069',
+          'Phillips County, MT': '071',
+          'Pondera County, MT': '073',
+          'Powder River County, MT': '075',
+          'Powell County, MT': '077',
+          'Prairie County, MT': '079',
+          'Ravalli County, MT': '081',
+          'Richland County, MT': '083',
+          'Roosevelt County, MT': '085',
+          'Rosebud County, MT': '087',
+          'Sanders County, MT': '089',
+          'Sheridan County, MT': '091',
+          'Silver Bow County, MT': '093',
+          'Stillwater County, MT': '095',
+          'Sweet Grass County, MT': '097',
+          'Teton County, MT': '099',
+          'Toole County, MT': '101',
+          'Treasure County, MT': '103',
+          'Valley County, MT': '105',
+          'Wheatland County, MT': '107',
+          'Wibaux County, MT': '109',
+          'Yellowstone County, MT': '111'},
+  '31': { 'Adams County, NE': '001',
+          'Antelope County, NE': '003',
+          'Arthur County, NE': '005',
+          'Banner County, NE': '007',
+          'Blaine County, NE': '009',
+          'Boone County, NE': '011',
+          'Box Butte County, NE': '013',
+          'Boyd County, NE': '015',
+          'Brown County, NE': '017',
+          'Buffalo County, NE': '019',
+          'Burt County, NE': '021',
+          'Butler County, NE': '023',
+          'Cass County, NE': '025',
+          'Cedar County, NE': '027',
+          'Chase County, NE': '029',
+          'Cherry County, NE': '031',
+          'Cheyenne County, NE': '033',
+          'Clay County, NE': '035',
+          'Colfax County, NE': '037',
+          'Cuming County, NE': '039',
+          'Custer County, NE': '041',
+          'Dakota County, NE': '043',
+          'Dawes County, NE': '045',
+          'Dawson County, NE': '047',
+          'Deuel County, NE': '049',
+          'Dixon County, NE': '051',
+          'Dodge County, NE': '053',
+          'Douglas County, NE': '055',
+          'Dundy County, NE': '057',
+          'Fillmore County, NE': '059',
+          'Franklin County, NE': '061',
+          'Frontier County, NE': '063',
+          'Furnas County, NE': '065',
+          'Gage County, NE': '067',
+          'Garden County, NE': '069',
+          'Garfield County, NE': '071',
+          'Gosper County, NE': '073',
+          'Grant County, NE': '075',
+          'Greeley County, NE': '077',
+          'Hall County, NE': '079',
+          'Hamilton County, NE': '081',
+          'Harlan County, NE': '083',
+          'Hayes County, NE': '085',
+          'Hitchcock County, NE': '087',
+          'Holt County, NE': '089',
+          'Hooker County, NE': '091',
+          'Howard County, NE': '093',
+          'Jefferson County, NE': '095',
+          'Johnson County, NE': '097',
+          'Kearney County, NE': '099',
+          'Keith County, NE': '101',
+          'Keya Paha County, NE': '103',
+          'Kimball County, NE': '105',
+          'Knox County, NE': '107',
+          'Lancaster County, NE': '109',
+          'Lincoln County, NE': '111',
+          'Logan County, NE': '113',
+          'Loup County, NE': '115',
+          'Madison County, NE': '119',
+          'McPherson County, NE': '117',
+          'Merrick County, NE': '121',
+          'Morrill County, NE': '123',
+          'Nance County, NE': '125',
+          'Nemaha County, NE': '127',
+          'Nuckolls County, NE': '129',
+          'Otoe County, NE': '131',
+          'Pawnee County, NE': '133',
+          'Perkins County, NE': '135',
+          'Phelps County, NE': '137',
+          'Pierce County, NE': '139',
+          'Platte County, NE': '141',
+          'Polk County, NE': '143',
+          'Red Willow County, NE': '145',
+          'Richardson County, NE': '147',
+          'Rock County, NE': '149',
+          'Saline County, NE': '151',
+          'Sarpy County, NE': '153',
+          'Saunders County, NE': '155',
+          'Scotts Bluff County, NE': '157',
+          'Seward County, NE': '159',
+          'Sheridan County, NE': '161',
+          'Sherman County, NE': '163',
+          'Sioux County, NE': '165',
+          'Stanton County, NE': '167',
+          'Thayer County, NE': '169',
+          'Thomas County, NE': '171',
+          'Thurston County, NE': '173',
+          'Valley County, NE': '175',
+          'Washington County, NE': '177',
+          'Wayne County, NE': '179',
+          'Webster County, NE': '181',
+          'Wheeler County, NE': '183',
+          'York County, NE': '185'},
+  '32': { 'Carson City, NV': '510',
+          'Churchill County, NV': '001',
+          'Clark County, NV': '003',
+          'Douglas County, NV': '005',
+          'Elko County, NV': '007',
+          'Esmeralda County, NV': '009',
+          'Eureka County, NV': '011',
+          'Humboldt County, NV': '013',
+          'Lander County, NV': '015',
+          'Lincoln County, NV': '017',
+          'Lyon County, NV': '019',
+          'Mineral County, NV': '021',
+          'Nye County, NV': '023',
+          'Pershing County, NV': '027',
+          'Storey County, NV': '029',
+          'Washoe County, NV': '031',
+          'White Pine County, NV': '033'},
+  '33': { 'Belknap County, NH': '001',
+          'Carroll County, NH': '003',
+          'Cheshire County, NH': '005',
+          'Coos County, NH': '007',
+          'Grafton County, NH': '009',
+          'Hillsborough County, NH': '011',
+          'Merrimack County, NH': '013',
+          'Rockingham County, NH': '015',
+          'Strafford County, NH': '017',
+          'Sullivan County, NH': '019'},
+  '34': { 'Atlantic County, NJ': '001',
+          'Bergen County, NJ': '003',
+          'Burlington County, NJ': '005',
+          'Camden County, NJ': '007',
+          'Cape May County, NJ': '009',
+          'Cumberland County, NJ': '011',
+          'Essex County, NJ': '013',
+          'Gloucester County, NJ': '015',
+          'Hudson County, NJ': '017',
+          'Hunterdon County, NJ': '019',
+          'Mercer County, NJ': '021',
+          'Middlesex County, NJ': '023',
+          'Monmouth County, NJ': '025',
+          'Morris County, NJ': '027',
+          'Ocean County, NJ': '029',
+          'Passaic County, NJ': '031',
+          'Salem County, NJ': '033',
+          'Somerset County, NJ': '035',
+          'Sussex County, NJ': '037',
+          'Union County, NJ': '039',
+          'Warren County, NJ': '041'},
+  '35': { 'Bernalillo County, NM': '001',
+          'Catron County, NM': '003',
+          'Chaves County, NM': '005',
+          'Cibola County, NM': '006',
+          'Colfax County, NM': '007',
+          'Curry County, NM': '009',
+          'DeBaca County, NM': '011',
+          'Dona Ana County, NM': '013',
+          'Eddy County, NM': '015',
+          'Grant County, NM': '017',
+          'Guadalupe County, NM': '019',
+          'Harding County, NM': '021',
+          'Hidalgo County, NM': '023',
+          'Lea County, NM': '025',
+          'Lincoln County, NM': '027',
+          'Los Alamos County, NM': '028',
+          'Luna County, NM': '029',
+          'McKinley County, NM': '031',
+          'Mora County, NM': '033',
+          'Otero County, NM': '035',
+          'Quay County, NM': '037',
+          'Rio Arriba County, NM': '039',
+          'Roosevelt County, NM': '041',
+          'San Juan County, NM': '045',
+          'San Miguel County, NM': '047',
+          'Sandoval County, NM': '043',
+          'Santa Fe County, NM': '049',
+          'Sierra County, NM': '051',
+          'Socorro County, NM': '053',
+          'Taos County, NM': '055',
+          'Torrance County, NM': '057',
+          'Union County, NM': '059',
+          'Valencia County, NM': '061'},
+  '36': { 'Albany County, NY': '001',
+          'Allegany County, NY': '003',
+          'Bronx County, NY': '005',
+          'Broome County, NY': '007',
+          'Cattaraugus County, NY': '009',
+          'Cayuga County, NY': '011',
+          'Chautauqua County, NY': '013',
+          'Chemung County, NY': '015',
+          'Chenango County, NY': '017',
+          'Clinton County, NY': '019',
+          'Columbia County, NY': '021',
+          'Cortland County, NY': '023',
+          'Delaware County, NY': '025',
+          'Dutchess County, NY': '027',
+          'Erie County, NY': '029',
+          'Essex County, NY': '031',
+          'Franklin County, NY': '033',
+          'Fulton County, NY': '035',
+          'Genesee County, NY': '037',
+          'Greene County, NY': '039',
+          'Hamilton County, NY': '041',
+          'Herkimer County, NY': '043',
+          'Jefferson County, NY': '045',
+          'Kings County, NY': '047',
+          'Lewis County, NY': '049',
+          'Livingston County, NY': '051',
+          'Madison County, NY': '053',
+          'Monroe County, NY': '055',
+          'Montgomery County, NY': '057',
+          'Nassau County, NY': '059',
+          'New York County, NY': '061',
+          'Niagara County, NY': '063',
+          'Oneida County, NY': '065',
+          'Onondaga County, NY': '067',
+          'Ontario County, NY': '069',
+          'Orange County, NY': '071',
+          'Orleans County, NY': '073',
+          'Oswego County, NY': '075',
+          'Otsego County, NY': '077',
+          'Putnam County, NY': '079',
+          'Queens County, NY': '081',
+          'Rensselaer County, NY': '083',
+          'Richmond County, NY': '085',
+          'Rockland County, NY': '087',
+          'Saratoga County, NY': '091',
+          'Schenectady County, NY': '093',
+          'Schoharie County, NY': '095',
+          'Schuyler County, NY': '097',
+          'Seneca County, NY': '099',
+          'St. Lawrence County, NY': '089',
+          'Steuben County, NY': '101',
+          'Suffolk County, NY': '103',
+          'Sullivan County, NY': '105',
+          'Tioga County, NY': '107',
+          'Tompkins County, NY': '109',
+          'Ulster County, NY': '111',
+          'Warren County, NY': '113',
+          'Washington County, NY': '115',
+          'Wayne County, NY': '117',
+          'Westchester County, NY': '119',
+          'Wyoming County, NY': '121',
+          'Yates County, NY': '123'},
+  '37': { 'Alamance County, NC': '001',
+          'Alexander County, NC': '003',
+          'Alleghany County, NC': '005',
+          'Anson County, NC': '007',
+          'Ashe County, NC': '009',
+          'Avery County, NC': '011',
+          'Beaufort County, NC': '013',
+          'Bertie County, NC': '015',
+          'Bladen County, NC': '017',
+          'Brunswick County, NC': '019',
+          'Buncombe County, NC': '021',
+          'Burke County, NC': '023',
+          'Cabarrus County, NC': '025',
+          'Caldwell County, NC': '027',
+          'Camden County, NC': '029',
+          'Carteret County, NC': '031',
+          'Caswell County, NC': '033',
+          'Catawba County, NC': '035',
+          'Chatham County, NC': '037',
+          'Cherokee County, NC': '039',
+          'Chowan County, NC': '041',
+          'Clay County, NC': '043',
+          'Cleveland County, NC': '045',
+          'Columbus County, NC': '047',
+          'Craven County, NC': '049',
+          'Cumberland County, NC': '051',
+          'Currituck County, NC': '053',
+          'Dare County, NC': '055',
+          'Davidson County, NC': '057',
+          'Davie County, NC': '059',
+          'Duplin County, NC': '061',
+          'Durham County, NC': '063',
+          'Edgecombe County, NC': '065',
+          'Forsyth County, NC': '067',
+          'Franklin County, NC': '069',
+          'Gaston County, NC': '071',
+          'Gates County, NC': '073',
+          'Graham County, NC': '075',
+          'Granville County, NC': '077',
+          'Greene County, NC': '079',
+          'Guilford County, NC': '081',
+          'Halifax County, NC': '083',
+          'Harnett County, NC': '085',
+          'Haywood County, NC': '087',
+          'Henderson County, NC': '089',
+          'Hertford County, NC': '091',
+          'Hoke County, NC': '093',
+          'Hyde County, NC': '095',
+          'Iredell County, NC': '097',
+          'Jackson County, NC': '099',
+          'Johnston County, NC': '101',
+          'Jones County, NC': '103',
+          'Lee County, NC': '105',
+          'Lenoir County, NC': '107',
+          'Lincoln County, NC': '109',
+          'Macon County, NC': '113',
+          'Madison County, NC': '115',
+          'Martin County, NC': '117',
+          'McDowell County, NC': '111',
+          'Mecklenburg County, NC': '119',
+          'Mitchell County, NC': '121',
+          'Montgomery County, NC': '123',
+          'Moore County, NC': '125',
+          'Nash County, NC': '127',
+          'New Hanover County, NC': '129',
+          'Northampton County, NC': '131',
+          'Onslow County, NC': '133',
+          'Orange County, NC': '135',
+          'Pamlico County, NC': '137',
+          'Pasquotank County, NC': '139',
+          'Pender County, NC': '141',
+          'Perquimans County, NC': '143',
+          'Person County, NC': '145',
+          'Pitt County, NC': '147',
+          'Polk County, NC': '149',
+          'Randolph County, NC': '151',
+          'Richmond County, NC': '153',
+          'Robeson County, NC': '155',
+          'Rockingham County, NC': '157',
+          'Rowan County, NC': '159',
+          'Rutherford County, NC': '161',
+          'Sampson County, NC': '163',
+          'Scotland County, NC': '165',
+          'Stanly County, NC': '167',
+          'Stokes County, NC': '169',
+          'Surry County, NC': '171',
+          'Swain County, NC': '173',
+          'Transylvania County, NC': '175',
+          'Tyrrell County, NC': '177',
+          'Union County, NC': '179',
+          'Vance County, NC': '181',
+          'Wake County, NC': '183',
+          'Warren County, NC': '185',
+          'Washington County, NC': '187',
+          'Watauga County, NC': '189',
+          'Wayne County, NC': '191',
+          'Wilkes County, NC': '193',
+          'Wilson County, NC': '195',
+          'Yadkin County, NC': '197',
+          'Yancey County, NC': '199'},
+  '38': { 'Adams County, ND': '001',
+          'Barnes County, ND': '003',
+          'Benson County, ND': '005',
+          'Billings County, ND': '007',
+          'Bottineau County, ND': '009',
+          'Bowman County, ND': '011',
+          'Burke County, ND': '013',
+          'Burleigh County, ND': '015',
+          'Cass County, ND': '017',
+          'Cavalier County, ND': '019',
+          'Dickey County, ND': '021',
+          'Divide County, ND': '023',
+          'Dunn County, ND': '025',
+          'Eddy County, ND': '027',
+          'Emmons County, ND': '029',
+          'Foster County, ND': '031',
+          'Golden Valley County, ND': '033',
+          'Grand Forks County, ND': '035',
+          'Grant County, ND': '037',
+          'Griggs County, ND': '039',
+          'Hettinger County, ND': '041',
+          'Kidder County, ND': '043',
+          'LaMoure County, ND': '045',
+          'Logan County, ND': '047',
+          'McHenry County, ND': '049',
+          'McIntosh County, ND': '051',
+          'McKenzie County, ND': '053',
+          'McLean County, ND': '055',
+          'Mercer County, ND': '057',
+          'Morton County, ND': '059',
+          'Mountrail County, ND': '061',
+          'Nelson County, ND': '063',
+          'Oliver County, ND': '065',
+          'Pembina County, ND': '067',
+          'Pierce County, ND': '069',
+          'Ramsey County, ND': '071',
+          'Ransom County, ND': '073',
+          'Renville County, ND': '075',
+          'Richland County, ND': '077',
+          'Rolette County, ND': '079',
+          'Sargent County, ND': '081',
+          'Sheridan County, ND': '083',
+          'Sioux County, ND': '085',
+          'Slope County, ND': '087',
+          'Stark County, ND': '089',
+          'Steele County, ND': '091',
+          'Stutsman County, ND': '093',
+          'Towner County, ND': '095',
+          'Traill County, ND': '097',
+          'Walsh County, ND': '099',
+          'Ward County, ND': '101',
+          'Wells County, ND': '103',
+          'Williams County, ND': '105'},
+  '39': { 'Adams County, OH': '001',
+          'Allen County, OH': '003',
+          'Ashland County, OH': '005',
+          'Ashtabula County, OH': '007',
+          'Athens County, OH': '009',
+          'Auglaize County, OH': '011',
+          'Belmont County, OH': '013',
+          'Brown County, OH': '015',
+          'Butler County, OH': '017',
+          'Carroll County, OH': '019',
+          'Champaign County, OH': '021',
+          'Clark County, OH': '023',
+          'Clermont County, OH': '025',
+          'Clinton County, OH': '027',
+          'Columbiana County, OH': '029',
+          'Coshocton County, OH': '031',
+          'Crawford County, OH': '033',
+          'Cuyahoga County, OH': '035',
+          'Darke County, OH': '037',
+          'Defiance County, OH': '039',
+          'Delaware County, OH': '041',
+          'Erie County, OH': '043',
+          'Fairfield County, OH': '045',
+          'Fayette County, OH': '047',
+          'Franklin County, OH': '049',
+          'Fulton County, OH': '051',
+          'Gallia County, OH': '053',
+          'Geauga County, OH': '055',
+          'Greene County, OH': '057',
+          'Guernsey County, OH': '059',
+          'Hamilton County, OH': '061',
+          'Hancock County, OH': '063',
+          'Hardin County, OH': '065',
+          'Harrison County, OH': '067',
+          'Henry County, OH': '069',
+          'Highland County, OH': '071',
+          'Hocking County, OH': '073',
+          'Holmes County, OH': '075',
+          'Huron County, OH': '077',
+          'Jackson County, OH': '079',
+          'Jefferson County, OH': '081',
+          'Knox County, OH': '083',
+          'Lake County, OH': '085',
+          'Lawrence County, OH': '087',
+          'Licking County, OH': '089',
+          'Logan County, OH': '091',
+          'Lorain County, OH': '093',
+          'Lucas County, OH': '095',
+          'Madison County, OH': '097',
+          'Mahoning County, OH': '099',
+          'Marion County, OH': '101',
+          'Medina County, OH': '103',
+          'Meigs County, OH': '105',
+          'Mercer County, OH': '107',
+          'Miami County, OH': '109',
+          'Monroe County, OH': '111',
+          'Montgomery County, OH': '113',
+          'Morgan County, OH': '115',
+          'Morrow County, OH': '117',
+          'Muskingum County, OH': '119',
+          'Noble County, OH': '121',
+          'Ottawa County, OH': '123',
+          'Paulding County, OH': '125',
+          'Perry County, OH': '127',
+          'Pickaway County, OH': '129',
+          'Pike County, OH': '131',
+          'Portage County, OH': '133',
+          'Preble County, OH': '135',
+          'Putnam County, OH': '137',
+          'Richland County, OH': '139',
+          'Ross County, OH': '141',
+          'Sandusky County, OH': '143',
+          'Scioto County, OH': '145',
+          'Seneca County, OH': '147',
+          'Shelby County, OH': '149',
+          'Stark County, OH': '151',
+          'Summit County, OH': '153',
+          'Trumbull County, OH': '155',
+          'Tuscarawas County, OH': '157',
+          'Union County, OH': '159',
+          'Van Wert County, OH': '161',
+          'Vinton County, OH': '163',
+          'Warren County, OH': '165',
+          'Washington County, OH': '167',
+          'Wayne County, OH': '169',
+          'Williams County, OH': '171',
+          'Wood County, OH': '173',
+          'Wyandot County, OH': '175'},
+  '40': { 'Adair County, OK': '001',
+          'Alfalfa County, OK': '003',
+          'Atoka County, OK': '005',
+          'Beaver County, OK': '007',
+          'Beckham County, OK': '009',
+          'Blaine County, OK': '011',
+          'Bryan County, OK': '013',
+          'Caddo County, OK': '015',
+          'Canadian County, OK': '017',
+          'Carter County, OK': '019',
+          'Cherokee County, OK': '021',
+          'Choctaw County, OK': '023',
+          'Cimarron County, OK': '025',
+          'Cleveland County, OK': '027',
+          'Coal County, OK': '029',
+          'Comanche County, OK': '031',
+          'Cotton County, OK': '033',
+          'Craig County, OK': '035',
+          'Creek County, OK': '037',
+          'Custer County, OK': '039',
+          'Delaware County, OK': '041',
+          'Dewey County, OK': '043',
+          'Ellis County, OK': '045',
+          'Garfield County, OK': '047',
+          'Garvin County, OK': '049',
+          'Grady County, OK': '051',
+          'Grant County, OK': '053',
+          'Greer County, OK': '055',
+          'Harmon County, OK': '057',
+          'Harper County, OK': '059',
+          'Haskell County, OK': '061',
+          'Hughes County, OK': '063',
+          'Jackson County, OK': '065',
+          'Jefferson County, OK': '067',
+          'Johnston County, OK': '069',
+          'Kay County, OK': '071',
+          'Kingfisher County, OK': '073',
+          'Kiowa County, OK': '075',
+          'Latimer County, OK': '077',
+          'Le Flore County, OK': '079',
+          'Lincoln County, OK': '081',
+          'Logan County, OK': '083',
+          'Love County, OK': '085',
+          'Major County, OK': '093',
+          'Marshall County, OK': '095',
+          'Mayes County, OK': '097',
+          'McClain County, OK': '087',
+          'McCurtain County, OK': '089',
+          'McIntosh County, OK': '091',
+          'Murray County, OK': '099',
+          'Muskogee County, OK': '101',
+          'Noble County, OK': '103',
+          'Nowata County, OK': '105',
+          'Okfuskee County, OK': '107',
+          'Oklahoma County, OK': '109',
+          'Okmulgee County, OK': '111',
+          'Osage County, OK': '113',
+          'Ottawa County, OK': '115',
+          'Pawnee County, OK': '117',
+          'Payne County, OK': '119',
+          'Pittsburg County, OK': '121',
+          'Pontotoc County, OK': '123',
+          'Pottawatomie County, OK': '125',
+          'Pushmataha County, OK': '127',
+          'Roger Mills County, OK': '129',
+          'Rogers County, OK': '131',
+          'Seminole County, OK': '133',
+          'Sequoyah County, OK': '135',
+          'Stephens County, OK': '137',
+          'Texas County, OK': '139',
+          'Tillman County, OK': '141',
+          'Tulsa County, OK': '143',
+          'Wagoner County, OK': '145',
+          'Washington County, OK': '147',
+          'Washita County, OK': '149',
+          'Woods County, OK': '151',
+          'Woodward County, OK': '153'},
+  '41': { 'Baker County, OR': '001',
+          'Benton County, OR': '003',
+          'Clackamas County, OR': '005',
+          'Clatsop County, OR': '007',
+          'Columbia County, OR': '009',
+          'Coos County, OR': '011',
+          'Crook County, OR': '013',
+          'Curry County, OR': '015',
+          'Deschutes County, OR': '017',
+          'Douglas County, OR': '019',
+          'Gilliam County, OR': '021',
+          'Grant County, OR': '023',
+          'Harney County, OR': '025',
+          'Hood River County, OR': '027',
+          'Jackson County, OR': '029',
+          'Jefferson County, OR': '031',
+          'Josephine County, OR': '033',
+          'Klamath County, OR': '035',
+          'Lake County, OR': '037',
+          'Lane County, OR': '039',
+          'Lincoln County, OR': '041',
+          'Linn County, OR': '043',
+          'Malheur County, OR': '045',
+          'Marion County, OR': '047',
+          'Morrow County, OR': '049',
+          'Multnomah County, OR': '051',
+          'Polk County, OR': '053',
+          'Sherman County, OR': '055',
+          'Tillamook County, OR': '057',
+          'Umatilla County, OR': '059',
+          'Union County, OR': '061',
+          'Wallowa County, OR': '063',
+          'Wasco County, OR': '065',
+          'Washington County, OR': '067',
+          'Wheeler County, OR': '069',
+          'Yamhill County, OR': '071'},
+  '42': { 'Adams County, PA': '001',
+          'Allegheny County, PA': '003',
+          'Armstrong County, PA': '005',
+          'Beaver County, PA': '007',
+          'Bedford County, PA': '009',
+          'Berks County, PA': '011',
+          'Blair County, PA': '013',
+          'Bradford County, PA': '015',
+          'Bucks County, PA': '017',
+          'Butler County, PA': '019',
+          'Cambria County, PA': '021',
+          'Cameron County, PA': '023',
+          'Carbon County, PA': '025',
+          'Centre County, PA': '027',
+          'Chester County, PA': '029',
+          'Clarion County, PA': '031',
+          'Clearfield County, PA': '033',
+          'Clinton County, PA': '035',
+          'Columbia County, PA': '037',
+          'Crawford County, PA': '039',
+          'Cumberland County, PA': '041',
+          'Dauphin County, PA': '043',
+          'Delaware County, PA': '045',
+          'Elk County, PA': '047',
+          'Erie County, PA': '049',
+          'Fayette County, PA': '051',
+          'Forest County, PA': '053',
+          'Franklin County, PA': '055',
+          'Fulton County, PA': '057',
+          'Greene County, PA': '059',
+          'Huntingdon County, PA': '061',
+          'Indiana County, PA': '063',
+          'Jefferson County, PA': '065',
+          'Juniata County, PA': '067',
+          'Lackawanna County, PA': '069',
+          'Lancaster County, PA': '071',
+          'Lawrence County, PA': '073',
+          'Lebanon County, PA': '075',
+          'Lehigh County, PA': '077',
+          'Luzerne County, PA': '079',
+          'Lycoming County, PA': '081',
+          'McKean County, PA': '083',
+          'Mercer County, PA': '085',
+          'Mifflin County, PA': '087',
+          'Monroe County, PA': '089',
+          'Montgomery County, PA': '091',
+          'Montour County, PA': '093',
+          'Northampton County, PA': '095',
+          'Northumberland County, PA': '097',
+          'Perry County, PA': '099',
+          'Philadelphia County/city, PA': '101',
+          'Pike County, PA': '103',
+          'Potter County, PA': '105',
+          'Schuylkill County, PA': '107',
+          'Snyder County, PA': '109',
+          'Somerset County, PA': '111',
+          'Sullivan County, PA': '113',
+          'Susquehanna County, PA': '115',
+          'Tioga County, PA': '117',
+          'Union County, PA': '119',
+          'Venango County, PA': '121',
+          'Warren County, PA': '123',
+          'Washington County, PA': '125',
+          'Wayne County, PA': '127',
+          'Westmoreland County, PA': '129',
+          'Wyoming County, PA': '131',
+          'York County, PA': '133'},
+  '44': { 'Bristol County, RI': '001',
+          'Kent County, RI': '003',
+          'Newport County, RI': '005',
+          'Providence County, RI': '007',
+          'Washington County, RI': '009'},
+  '45': { 'Abbeville County, SC': '001',
+          'Aiken County, SC': '003',
+          'Allendale County, SC': '005',
+          'Anderson County, SC': '007',
+          'Bamberg County, SC': '009',
+          'Barnwell County, SC': '011',
+          'Beaufort County, SC': '013',
+          'Berkeley County, SC': '015',
+          'Calhoun County, SC': '017',
+          'Charleston County, SC': '019',
+          'Cherokee County, SC': '021',
+          'Chester County, SC': '023',
+          'Chesterfield County, SC': '025',
+          'Clarendon County, SC': '027',
+          'Colleton County, SC': '029',
+          'Darlington County, SC': '031',
+          'Dillon County, SC': '033',
+          'Dorchester County, SC': '035',
+          'Edgefield County, SC': '037',
+          'Fairfield County, SC': '039',
+          'Florence County, SC': '041',
+          'Georgetown County, SC': '043',
+          'Greenville County, SC': '045',
+          'Greenwood County, SC': '047',
+          'Hampton County, SC': '049',
+          'Horry County, SC': '051',
+          'Jasper County, SC': '053',
+          'Kershaw County, SC': '055',
+          'Lancaster County, SC': '057',
+          'Laurens County, SC': '059',
+          'Lee County, SC': '061',
+          'Lexington County, SC': '063',
+          'Marion County, SC': '067',
+          'Marlboro County, SC': '069',
+          'McCormick County, SC': '065',
+          'Newberry County, SC': '071',
+          'Oconee County, SC': '073',
+          'Orangeburg County, SC': '075',
+          'Pickens County, SC': '077',
+          'Richland County, SC': '079',
+          'Saluda County, SC': '081',
+          'Spartanburg County, SC': '083',
+          'Sumter County, SC': '085',
+          'Union County, SC': '087',
+          'Williamsburg County, SC': '089',
+          'York County, SC': '091'},
+  '46': { 'Aurora County, SD': '003',
+          'Beadle County, SD': '005',
+          'Bennett County, SD': '007',
+          'Bon Homme County, SD': '009',
+          'Brookings County, SD': '011',
+          'Brown County, SD': '013',
+          'Brule County, SD': '015',
+          'Buffalo County, SD': '017',
+          'Butte County, SD': '019',
+          'Campbell County, SD': '021',
+          'Charles Mix County, SD': '023',
+          'Clark County, SD': '025',
+          'Clay County, SD': '027',
+          'Codington County, SD': '029',
+          'Corson County, SD': '031',
+          'Custer County, SD': '033',
+          'Davison County, SD': '035',
+          'Day County, SD': '037',
+          'Deuel County, SD': '039',
+          'Dewey County, SD': '041',
+          'Douglas County, SD': '043',
+          'Edmunds County, SD': '045',
+          'Fall River County, SD': '047',
+          'Faulk County, SD': '049',
+          'Grant County, SD': '051',
+          'Gregory County, SD': '053',
+          'Haakon County, SD': '055',
+          'Hamlin County, SD': '057',
+          'Hand County, SD': '059',
+          'Hanson County, SD': '061',
+          'Harding County, SD': '063',
+          'Hughes County, SD': '065',
+          'Hutchinson County, SD': '067',
+          'Hyde County, SD': '069',
+          'Jackson County, SD': '071',
+          'Jerauld County, SD': '073',
+          'Jones County, SD': '075',
+          'Kingsbury County, SD': '077',
+          'Lake County, SD': '079',
+          'Lawrence County, SD': '081',
+          'Lincoln County, SD': '083',
+          'Lyman County, SD': '085',
+          'Marshall County, SD': '091',
+          'McCook County, SD': '087',
+          'McPherson County, SD': '089',
+          'Meade County, SD': '093',
+          'Mellette County, SD': '095',
+          'Miner County, SD': '097',
+          'Minnehaha County, SD': '099',
+          'Moody County, SD': '101',
+          'Pennington County, SD': '103',
+          'Perkins County, SD': '105',
+          'Potter County, SD': '107',
+          'Roberts County, SD': '109',
+          'Sanborn County, SD': '111',
+          'Shannon County, SD': '113',
+          'Spink County, SD': '115',
+          'Stanley County, SD': '117',
+          'Sully County, SD': '119',
+          'Todd County, SD': '121',
+          'Tripp County, SD': '123',
+          'Turner County, SD': '125',
+          'Union County, SD': '127',
+          'Walworth County, SD': '129',
+          'Yankton County, SD': '135',
+          'Ziebach County, SD': '137'},
+  '47': { 'Anderson County, TN': '001',
+          'Bedford County, TN': '003',
+          'Benton County, TN': '005',
+          'Bledsoe County, TN': '007',
+          'Blount County, TN': '009',
+          'Bradley County, TN': '011',
+          'Campbell County, TN': '013',
+          'Cannon County, TN': '015',
+          'Carroll County, TN': '017',
+          'Carter County, TN': '019',
+          'Cheatham County, TN': '021',
+          'Chester County, TN': '023',
+          'Claiborne County, TN': '025',
+          'Clay County, TN': '027',
+          'Cocke County, TN': '029',
+          'Coffee County, TN': '031',
+          'Crockett County, TN': '033',
+          'Cumberland County, TN': '035',
+          'Davidson County, TN': '037',
+          'DeKalb County, TN': '041',
+          'Decatur County, TN': '039',
+          'Dickson County, TN': '043',
+          'Dyer County, TN': '045',
+          'Fayette County, TN': '047',
+          'Fentress County, TN': '049',
+          'Franklin County, TN': '051',
+          'Gibson County, TN': '053',
+          'Giles County, TN': '055',
+          'Grainger County, TN': '057',
+          'Greene County, TN': '059',
+          'Grundy County, TN': '061',
+          'Hamblen County, TN': '063',
+          'Hamilton County, TN': '065',
+          'Hancock County, TN': '067',
+          'Hardeman County, TN': '069',
+          'Hardin County, TN': '071',
+          'Hawkins County, TN': '073',
+          'Haywood County, TN': '075',
+          'Henderson County, TN': '077',
+          'Henry County, TN': '079',
+          'Hickman County, TN': '081',
+          'Houston County, TN': '083',
+          'Humphreys County, TN': '085',
+          'Jackson County, TN': '087',
+          'Jefferson County, TN': '089',
+          'Johnson County, TN': '091',
+          'Knox County, TN': '093',
+          'Lake County, TN': '095',
+          'Lauderdale County, TN': '097',
+          'Lawrence County, TN': '099',
+          'Lewis County, TN': '101',
+          'Lincoln County, TN': '103',
+          'Loudon County, TN': '105',
+          'Macon County, TN': '111',
+          'Madison County, TN': '113',
+          'Marion County, TN': '115',
+          'Marshall County, TN': '117',
+          'Maury County, TN': '119',
+          'McMinn County, TN': '107',
+          'McNairy County, TN': '109',
+          'Meigs County, TN': '121',
+          'Monroe County, TN': '123',
+          'Montgomery County, TN': '125',
+          'Moore County, TN': '127',
+          'Morgan County, TN': '129',
+          'Obion County, TN': '131',
+          'Overton County, TN': '133',
+          'Perry County, TN': '135',
+          'Pickett County, TN': '137',
+          'Polk County, TN': '139',
+          'Putnam County, TN': '141',
+          'Rhea County, TN': '143',
+          'Roane County, TN': '145',
+          'Robertson County, TN': '147',
+          'Rutherford County, TN': '149',
+          'Scott County, TN': '151',
+          'Sequatchie County, TN': '153',
+          'Sevier County, TN': '155',
+          'Shelby County, TN': '157',
+          'Smith County, TN': '159',
+          'Stewart County, TN': '161',
+          'Sullivan County, TN': '163',
+          'Sumner County, TN': '165',
+          'Tipton County, TN': '167',
+          'Trousdale County, TN': '169',
+          'Unicoi County, TN': '171',
+          'Union County, TN': '173',
+          'Van Buren County, TN': '175',
+          'Warren County, TN': '177',
+          'Washington County, TN': '179',
+          'Wayne County, TN': '181',
+          'Weakley County, TN': '183',
+          'White County, TN': '185',
+          'Williamson County, TN': '187',
+          'Wilson County, TN': '189'},
+  '48': { 'Anderson County, TX': '001',
+          'Andrews County, TX': '003',
+          'Angelina County, TX': '005',
+          'Aransas County, TX': '007',
+          'Archer County, TX': '009',
+          'Armstrong County, TX': '011',
+          'Atascosa County, TX': '013',
+          'Austin County, TX': '015',
+          'Bailey County, TX': '017',
+          'Bandera County, TX': '019',
+          'Bastrop County, TX': '021',
+          'Baylor County, TX': '023',
+          'Bee County, TX': '025',
+          'Bell County, TX': '027',
+          'Bexar County, TX': '029',
+          'Blanco County, TX': '031',
+          'Borden County, TX': '033',
+          'Bosque County, TX': '035',
+          'Bowie County, TX': '037',
+          'Brazoria County, TX': '039',
+          'Brazos County, TX': '041',
+          'Brewster County, TX': '043',
+          'Briscoe County, TX': '045',
+          'Brooks County, TX': '047',
+          'Brown County, TX': '049',
+          'Burleson County, TX': '051',
+          'Burnet County, TX': '053',
+          'Caldwell County, TX': '055',
+          'Calhoun County, TX': '057',
+          'Callahan County, TX': '059',
+          'Cameron County, TX': '061',
+          'Camp County, TX': '063',
+          'Carson County, TX': '065',
+          'Cass County, TX': '067',
+          'Castro County, TX': '069',
+          'Chambers County, TX': '071',
+          'Cherokee County, TX': '073',
+          'Childress County, TX': '075',
+          'Clay County, TX': '077',
+          'Cochran County, TX': '079',
+          'Coke County, TX': '081',
+          'Coleman County, TX': '083',
+          'Collin County, TX': '085',
+          'Collingsworth County, TX': '087',
+          'Colorado County, TX': '089',
+          'Comal County, TX': '091',
+          'Comanche County, TX': '093',
+          'Concho County, TX': '095',
+          'Cooke County, TX': '097',
+          'Coryell County, TX': '099',
+          'Cottle County, TX': '101',
+          'Crane County, TX': '103',
+          'Crockett County, TX': '105',
+          'Crosby County, TX': '107',
+          'Culberson County, TX': '109',
+          'Dallam County, TX': '111',
+          'Dallas County, TX': '113',
+          'Dawson County, TX': '115',
+          'DeWitt County, TX': '123',
+          'Deaf Smith County, TX': '117',
+          'Delta County, TX': '119',
+          'Denton County, TX': '121',
+          'Dickens County, TX': '125',
+          'Dimmit County, TX': '127',
+          'Donley County, TX': '129',
+          'Duval County, TX': '131',
+          'Eastland County, TX': '133',
+          'Ector County, TX': '135',
+          'Edwards County, TX': '137',
+          'El Paso County, TX': '141',
+          'Ellis County, TX': '139',
+          'Erath County, TX': '143',
+          'Falls County, TX': '145',
+          'Fannin County, TX': '147',
+          'Fayette County, TX': '149',
+          'Fisher County, TX': '151',
+          'Floyd County, TX': '153',
+          'Foard County, TX': '155',
+          'Fort Bend County, TX': '157',
+          'Franklin County, TX': '159',
+          'Freestone County, TX': '161',
+          'Frio County, TX': '163',
+          'Gaines County, TX': '165',
+          'Galveston County, TX': '167',
+          'Garza County, TX': '169',
+          'Gillespie County, TX': '171',
+          'Glasscock County, TX': '173',
+          'Goliad County, TX': '175',
+          'Gonzales County, TX': '177',
+          'Gray County, TX': '179',
+          'Grayson County, TX': '181',
+          'Gregg County, TX': '183',
+          'Grimes County, TX': '185',
+          'Guadalupe County, TX': '187',
+          'Hale County, TX': '189',
+          'Hall County, TX': '191',
+          'Hamilton County, TX': '193',
+          'Hansford County, TX': '195',
+          'Hardeman County, TX': '197',
+          'Hardin County, TX': '199',
+          'Harris County, TX': '201',
+          'Harrison County, TX': '203',
+          'Hartley County, TX': '205',
+          'Haskell County, TX': '207',
+          'Hays County, TX': '209',
+          'Hemphill County, TX': '211',
+          'Henderson County, TX': '213',
+          'Hidalgo County, TX': '215',
+          'Hill County, TX': '217',
+          'Hockley County, TX': '219',
+          'Hood County, TX': '221',
+          'Hopkins County, TX': '223',
+          'Houston County, TX': '225',
+          'Howard County, TX': '227',
+          'Hudspeth County, TX': '229',
+          'Hunt County, TX': '231',
+          'Hutchinson County, TX': '233',
+          'Irion County, TX': '235',
+          'Jack County, TX': '237',
+          'Jackson County, TX': '239',
+          'Jasper County, TX': '241',
+          'Jeff Davis County, TX': '243',
+          'Jefferson County, TX': '245',
+          'Jim Hogg County, TX': '247',
+          'Jim Wells County, TX': '249',
+          'Johnson County, TX': '251',
+          'Jones County, TX': '253',
+          'Karnes County, TX': '255',
+          'Kaufman County, TX': '257',
+          'Kendall County, TX': '259',
+          'Kenedy County, TX': '261',
+          'Kent County, TX': '263',
+          'Kerr County, TX': '265',
+          'Kimble County, TX': '267',
+          'King County, TX': '269',
+          'Kinney County, TX': '271',
+          'Kleberg County, TX': '273',
+          'Knox County, TX': '275',
+          'La Salle County, TX': '283',
+          'Lamar County, TX': '277',
+          'Lamb County, TX': '279',
+          'Lampasas County, TX': '281',
+          'Lavaca County, TX': '285',
+          'Lee County, TX': '287',
+          'Leon County, TX': '289',
+          'Liberty County, TX': '291',
+          'Limestone County, TX': '293',
+          'Lipscomb County, TX': '295',
+          'Live Oak County, TX': '297',
+          'Llano County, TX': '299',
+          'Loving County, TX': '301',
+          'Lubbock County, TX': '303',
+          'Lynn County, TX': '305',
+          'Madison County, TX': '313',
+          'Marion County, TX': '315',
+          'Martin County, TX': '317',
+          'Mason County, TX': '319',
+          'Matagorda County, TX': '321',
+          'Maverick County, TX': '323',
+          'McCulloch County, TX': '307',
+          'McLennan County, TX': '309',
+          'McMullen County, TX': '311',
+          'Medina County, TX': '325',
+          'Menard County, TX': '327',
+          'Midland County, TX': '329',
+          'Milam County, TX': '331',
+          'Mills County, TX': '333',
+          'Mitchell County, TX': '335',
+          'Montague County, TX': '337',
+          'Montgomery County, TX': '339',
+          'Moore County, TX': '341',
+          'Morris County, TX': '343',
+          'Motley County, TX': '345',
+          'Nacogdoches County, TX': '347',
+          'Navarro County, TX': '349',
+          'Newton County, TX': '351',
+          'Nolan County, TX': '353',
+          'Nueces County, TX': '355',
+          'Ochiltree County, TX': '357',
+          'Oldham County, TX': '359',
+          'Orange County, TX': '361',
+          'Palo Pinto County, TX': '363',
+          'Panola County, TX': '365',
+          'Parker County, TX': '367',
+          'Parmer County, TX': '369',
+          'Pecos County, TX': '371',
+          'Polk County, TX': '373',
+          'Potter County, TX': '375',
+          'Presidio County, TX': '377',
+          'Rains County, TX': '379',
+          'Randall County, TX': '381',
+          'Reagan County, TX': '383',
+          'Real County, TX': '385',
+          'Red River County, TX': '387',
+          'Reeves County, TX': '389',
+          'Refugio County, TX': '391',
+          'Roberts County, TX': '393',
+          'Robertson County, TX': '395',
+          'Rockwall County, TX': '397',
+          'Runnels County, TX': '399',
+          'Rusk County, TX': '401',
+          'Sabine County, TX': '403',
+          'San Augustine County, TX': '405',
+          'San Jacinto County, TX': '407',
+          'San Patricio County, TX': '409',
+          'San Saba County, TX': '411',
+          'Schleicher County, TX': '413',
+          'Scurry County, TX': '415',
+          'Shackelford County, TX': '417',
+          'Shelby County, TX': '419',
+          'Sherman County, TX': '421',
+          'Smith County, TX': '423',
+          'Somervell County, TX': '425',
+          'Starr County, TX': '427',
+          'Stephens County, TX': '429',
+          'Sterling County, TX': '431',
+          'Stonewall County, TX': '433',
+          'Sutton County, TX': '435',
+          'Swisher County, TX': '437',
+          'Tarrant County, TX': '439',
+          'Taylor County, TX': '441',
+          'Terrell County, TX': '443',
+          'Terry County, TX': '445',
+          'Throckmorton County, TX': '447',
+          'Titus County, TX': '449',
+          'Tom Green County, TX': '451',
+          'Travis County, TX': '453',
+          'Trinity County, TX': '455',
+          'Tyler County, TX': '457',
+          'Upshur County, TX': '459',
+          'Upton County, TX': '461',
+          'Uvalde County, TX': '463',
+          'Val Verde County, TX': '465',
+          'Van Zandt County, TX': '467',
+          'Victoria County, TX': '469',
+          'Walker County, TX': '471',
+          'Waller County, TX': '473',
+          'Ward County, TX': '475',
+          'Washington County, TX': '477',
+          'Webb County, TX': '479',
+          'Wharton County, TX': '481',
+          'Wheeler County, TX': '483',
+          'Wichita County, TX': '485',
+          'Wilbarger County, TX': '487',
+          'Willacy County, TX': '489',
+          'Williamson County, TX': '491',
+          'Wilson County, TX': '493',
+          'Winkler County, TX': '495',
+          'Wise County, TX': '497',
+          'Wood County, TX': '499',
+          'Yoakum County, TX': '501',
+          'Young County, TX': '503',
+          'Zapata County, TX': '505',
+          'Zavala County, TX': '507'},
+  '49': { 'Beaver County, UT': '001',
+          'Box Elder County, UT': '003',
+          'Cache County, UT': '005',
+          'Carbon County, UT': '007',
+          'Daggett County, UT': '009',
+          'Davis County, UT': '011',
+          'Duchesne County, UT': '013',
+          'Emery County, UT': '015',
+          'Garfield County, UT': '017',
+          'Grand County, UT': '019',
+          'Iron County, UT': '021',
+          'Juab County, UT': '023',
+          'Kane County, UT': '025',
+          'Millard County, UT': '027',
+          'Morgan County, UT': '029',
+          'Piute County, UT': '031',
+          'Rich County, UT': '033',
+          'Salt Lake County, UT': '035',
+          'San Juan County, UT': '037',
+          'Sanpete County, UT': '039',
+          'Sevier County, UT': '041',
+          'Summit County, UT': '043',
+          'Tooele County, UT': '045',
+          'Uintah County, UT': '047',
+          'Utah County, UT': '049',
+          'Wasatch County, UT': '051',
+          'Washington County, UT': '053',
+          'Wayne County, UT': '055',
+          'Weber County, UT': '057'},
+  '50': { 'Addison County, VT': '001',
+          'Bennington County, VT': '003',
+          'Caledonia County, VT': '005',
+          'Chittenden County, VT': '007',
+          'Essex County, VT': '009',
+          'Franklin County, VT': '011',
+          'Grand Isle County, VT': '013',
+          'Lamoille County, VT': '015',
+          'Orange County, VT': '017',
+          'Orleans County, VT': '019',
+          'Rutland County, VT': '021',
+          'Washington County, VT': '023',
+          'Windham County, VT': '025',
+          'Windsor County, VT': '027'},
+  '51': { 'Accomack County, VA': '001',
+          'Albemarle County, VA': '003',
+          'Alexandria city, VA': '510',
+          'Alleghany County, VA': '005',
+          'Amelia County, VA': '007',
+          'Amherst County, VA': '009',
+          'Appomattox County, VA': '011',
+          'Arlington County, VA': '013',
+          'Augusta County, VA': '015',
+          'Bath County, VA': '017',
+          'Bedford County, VA': '019',
+          'Bedford city, VA': '515',
+          'Bland County, VA': '021',
+          'Botetourt County, VA': '023',
+          'Bristol city, VA': '520',
+          'Brunswick County, VA': '025',
+          'Buchanan County, VA': '027',
+          'Buckingham County, VA': '029',
+          'Buena Vista city, VA': '530',
+          'Campbell County, VA': '031',
+          'Caroline County, VA': '033',
+          'Carroll County, VA': '035',
+          'Charles City County, VA': '036',
+          'Charlotte County, VA': '037',
+          'Charlottesville city, VA': '540',
+          'Chesapeake city, VA': '550',
+          'Chesterfield County, VA': '041',
+          'Clarke County, VA': '043',
+          'Colonial Heights city, VA': '570',
+          'Covington city, VA': '580',
+          'Craig County, VA': '045',
+          'Culpeper County, VA': '047',
+          'Cumberland County, VA': '049',
+          'Danville city, VA': '590',
+          'Dickenson County, VA': '051',
+          'Dinwiddie County, VA': '053',
+          'Emporia city, VA': '595',
+          'Essex County, VA': '057',
+          'Fairfax County, VA': '059',
+          'Fairfax city, VA': '600',
+          'Falls Church city, VA': '610',
+          'Fauquier County, VA': '061',
+          'Floyd County, VA': '063',
+          'Fluvanna County, VA': '065',
+          'Franklin County, VA': '067',
+          'Franklin city, VA': '620',
+          'Frederick County, VA': '069',
+          'Fredericksburg city, VA': '630',
+          'Galax city, VA': '640',
+          'Giles County, VA': '071',
+          'Gloucester County, VA': '073',
+          'Goochland County, VA': '075',
+          'Grayson County, VA': '077',
+          'Greene County, VA': '079',
+          'Greensville County, VA': '081',
+          'Halifax County, VA': '083',
+          'Hampton city, VA': '650',
+          'Hanover County, VA': '085',
+          'Harrisonburg city, VA': '660',
+          'Henrico County, VA': '087',
+          'Henry County, VA': '089',
+          'Highland County, VA': '091',
+          'Hopewell city, VA': '670',
+          'Isle of Wight County, VA': '093',
+          'James City County, VA': '095',
+          'King George County, VA': '099',
+          'King William County, VA': '101',
+          'King and Queen County, VA': '097',
+          'Lancaster County, VA': '103',
+          'Lee County, VA': '105',
+          'Lexington city, VA': '678',
+          'Loudoun County, VA': '107',
+          'Louisa County, VA': '109',
+          'Lunenburg County, VA': '111',
+          'Lynchburg city, VA': '680',
+          'Madison County, VA': '113',
+          'Manassas Park city, VA': '685',
+          'Manassas city, VA': '683',
+          'Martinsville city, VA': '690',
+          'Mathews County, VA': '115',
+          'Mecklenburg County, VA': '117',
+          'Middlesex County, VA': '119',
+          'Montgomery County, VA': '121',
+          'Nelson County, VA': '125',
+          'New Kent County, VA': '127',
+          'Newport News city, VA': '700',
+          'Norfolk city, VA': '710',
+          'Northampton County, VA': '131',
+          'Northumberland County, VA': '133',
+          'Norton city, VA': '720',
+          'Nottoway County, VA': '135',
+          'Orange County, VA': '137',
+          'Page County, VA': '139',
+          'Patrick County, VA': '141',
+          'Petersburg city, VA': '730',
+          'Pittsylvania County, VA': '143',
+          'Poquoson city, VA': '735',
+          'Portsmouth city, VA': '740',
+          'Powhatan County, VA': '145',
+          'Prince Edward County, VA': '147',
+          'Prince George County, VA': '149',
+          'Prince William County, VA': '153',
+          'Pulaski County, VA': '155',
+          'Radford city, VA': '750',
+          'Rappahannock County, VA': '157',
+          'Richmond County, VA': '159',
+          'Richmond city, VA': '760',
+          'Roanoke County, VA': '161',
+          'Roanoke city, VA': '770',
+          'Rockbridge County, VA': '163',
+          'Rockingham County, VA': '165',
+          'Russell County, VA': '167',
+          'Salem city, VA': '775',
+          'Scott County, VA': '169',
+          'Shenandoah County, VA': '171',
+          'Smyth County, VA': '173',
+          'Southampton County, VA': '175',
+          'Spotsylvania County, VA': '177',
+          'Stafford County, VA': '179',
+          'Staunton city, VA': '790',
+          'Suffolk city, VA': '800',
+          'Surry County, VA': '181',
+          'Sussex County, VA': '183',
+          'Tazewell County, VA': '185',
+          'Virginia Beach city, VA': '810',
+          'Warren County, VA': '187',
+          'Washington County, VA': '191',
+          'Waynesboro city, VA': '820',
+          'Westmoreland County, VA': '193',
+          'Williamsburg city, VA': '830',
+          'Winchester city, VA': '840',
+          'Wise County, VA': '195',
+          'Wythe County, VA': '197',
+          'York County, VA': '199'},
+  '53': { 'Adams County, WA': '001',
+          'Asotin County, WA': '003',
+          'Benton County, WA': '005',
+          'Chelan County, WA': '007',
+          'Clallam County, WA': '009',
+          'Clark County, WA': '011',
+          'Columbia County, WA': '013',
+          'Cowlitz County, WA': '015',
+          'Douglas County, WA': '017',
+          'Ferry County, WA': '019',
+          'Franklin County, WA': '021',
+          'Garfield County, WA': '023',
+          'Grant County, WA': '025',
+          'Grays Harbor County, WA': '027',
+          'Island County, WA': '029',
+          'Jefferson County, WA': '031',
+          'King County, WA': '033',
+          'Kitsap County, WA': '035',
+          'Kittitas County, WA': '037',
+          'Klickitat County, WA': '039',
+          'Lewis County, WA': '041',
+          'Lincoln County, WA': '043',
+          'Mason County, WA': '045',
+          'Okanogan County, WA': '047',
+          'Pacific County, WA': '049',
+          'Pend Oreille County, WA': '051',
+          'Pierce County, WA': '053',
+          'San Juan County, WA': '055',
+          'Skagit County, WA': '057',
+          'Skamania County, WA': '059',
+          'Snohomish County, WA': '061',
+          'Spokane County, WA': '063',
+          'Stevens County, WA': '065',
+          'Thurston County, WA': '067',
+          'Wahkiakum County, WA': '069',
+          'Walla Walla County, WA': '071',
+          'Whatcom County, WA': '073',
+          'Whitman County, WA': '075',
+          'Yakima County, WA': '077'},
+  '54': { 'Barbour County, WV': '001',
+          'Berkeley County, WV': '003',
+          'Boone County, WV': '005',
+          'Braxton County, WV': '007',
+          'Brooke County, WV': '009',
+          'Cabell County, WV': '011',
+          'Calhoun County, WV': '013',
+          'Clay County, WV': '015',
+          'Doddridge County, WV': '017',
+          'Fayette County, WV': '019',
+          'Gilmer County, WV': '021',
+          'Grant County, WV': '023',
+          'Greenbrier County, WV': '025',
+          'Hampshire County, WV': '027',
+          'Hancock County, WV': '029',
+          'Hardy County, WV': '031',
+          'Harrison County, WV': '033',
+          'Jackson County, WV': '035',
+          'Jefferson County, WV': '037',
+          'Kanawha County, WV': '039',
+          'Lewis County, WV': '041',
+          'Lincoln County, WV': '043',
+          'Logan County, WV': '045',
+          'Marion County, WV': '049',
+          'Marshall County, WV': '051',
+          'Mason County, WV': '053',
+          'McDowell County, WV': '047',
+          'Mercer County, WV': '055',
+          'Mineral County, WV': '057',
+          'Mingo County, WV': '059',
+          'Monongalia County, WV': '061',
+          'Monroe County, WV': '063',
+          'Morgan County, WV': '065',
+          'Nicholas County, WV': '067',
+          'Ohio County, WV': '069',
+          'Pendleton County, WV': '071',
+          'Pleasants County, WV': '073',
+          'Pocahontas County, WV': '075',
+          'Preston County, WV': '077',
+          'Putnam County, WV': '079',
+          'Raleigh County, WV': '081',
+          'Randolph County, WV': '083',
+          'Ritchie County, WV': '085',
+          'Roane County, WV': '087',
+          'Summers County, WV': '089',
+          'Taylor County, WV': '091',
+          'Tucker County, WV': '093',
+          'Tyler County, WV': '095',
+          'Upshur County, WV': '097',
+          'Wayne County, WV': '099',
+          'Webster County, WV': '101',
+          'Wetzel County, WV': '103',
+          'Wirt County, WV': '105',
+          'Wood County, WV': '107',
+          'Wyoming County, WV': '109'},
+  '55': { 'Adams County, WI': '001',
+          'Ashland County, WI': '003',
+          'Barron County, WI': '005',
+          'Bayfield County, WI': '007',
+          'Brown County, WI': '009',
+          'Buffalo County, WI': '011',
+          'Burnett County, WI': '013',
+          'Calumet County, WI': '015',
+          'Chippewa County, WI': '017',
+          'Clark County, WI': '019',
+          'Columbia County, WI': '021',
+          'Crawford County, WI': '023',
+          'Dane County, WI': '025',
+          'Dodge County, WI': '027',
+          'Door County, WI': '029',
+          'Douglas County, WI': '031',
+          'Dunn County, WI': '033',
+          'Eau Claire County, WI': '035',
+          'Florence County, WI': '037',
+          'Fond du Lac County, WI': '039',
+          'Forest County, WI': '041',
+          'Grant County, WI': '043',
+          'Green County, WI': '045',
+          'Green Lake County, WI': '047',
+          'Iowa County, WI': '049',
+          'Iron County, WI': '051',
+          'Jackson County, WI': '053',
+          'Jefferson County, WI': '055',
+          'Juneau County, WI': '057',
+          'Kenosha County, WI': '059',
+          'Kewaunee County, WI': '061',
+          'La Crosse County, WI': '063',
+          'Lafayette County, WI': '065',
+          'Langlade County, WI': '067',
+          'Lincoln County, WI': '069',
+          'Manitowoc County, WI': '071',
+          'Marathon County, WI': '073',
+          'Marinette County, WI': '075',
+          'Marquette County, WI': '077',
+          'Menominee County, WI': '078',
+          'Milwaukee County, WI': '079',
+          'Monroe County, WI': '081',
+          'Oconto County, WI': '083',
+          'Oneida County, WI': '085',
+          'Outagamie County, WI': '087',
+          'Ozaukee County, WI': '089',
+          'Pepin County, WI': '091',
+          'Pierce County, WI': '093',
+          'Polk County, WI': '095',
+          'Portage County, WI': '097',
+          'Price County, WI': '099',
+          'Racine County, WI': '101',
+          'Richland County, WI': '103',
+          'Rock County, WI': '105',
+          'Rusk County, WI': '107',
+          'Sauk County, WI': '111',
+          'Sawyer County, WI': '113',
+          'Shawano County, WI': '115',
+          'Sheboygan County, WI': '117',
+          'St. Croix County, WI': '109',
+          'Taylor County, WI': '119',
+          'Trempealeau County, WI': '121',
+          'Vernon County, WI': '123',
+          'Vilas County, WI': '125',
+          'Walworth County, WI': '127',
+          'Washburn County, WI': '129',
+          'Washington County, WI': '131',
+          'Waukesha County, WI': '133',
+          'Waupaca County, WI': '135',
+          'Waushara County, WI': '137',
+          'Winnebago County, WI': '139',
+          'Wood County, WI': '141'},
+  '56': { 'Albany County, WY': '001',
+          'Big Horn County, WY': '003',
+          'Campbell County, WY': '005',
+          'Carbon County, WY': '007',
+          'Converse County, WY': '009',
+          'Crook County, WY': '011',
+          'Fremont County, WY': '013',
+          'Goshen County, WY': '015',
+          'Hot Springs County, WY': '017',
+          'Johnson County, WY': '019',
+          'Laramie County, WY': '021',
+          'Lincoln County, WY': '023',
+          'Natrona County, WY': '025',
+          'Niobrara County, WY': '027',
+          'Park County, WY': '029',
+          'Platte County, WY': '031',
+          'Sheridan County, WY': '033',
+          'Sublette County, WY': '035',
+          'Sweetwater County, WY': '037',
+          'Teton County, WY': '039',
+          'Uinta County, WY': '041',
+          'Washakie County, WY': '043',
+          'Weston County, WY': '045'},
+  '72': { 'Adjuntas Municipio, PR': '001',
+          'Aguada Municipio, PR': '003',
+          'Aguadilla Municipio, PR': '005',
+          'Aguas Buenas Municipio, PR': '007',
+          'Aibonito Municipio, PR': '009',
+          'Anasco Municipio, PR': '011',
+          'Arecibo Municipio, PR': '013',
+          'Arroyo Municipio, PR': '015',
+          'Barceloneta Municipio, PR': '017',
+          'Barranquitas Municipio, PR': '019',
+          'Bayamon Municipio, PR': '021',
+          'Cabo Rojo Municipio, PR': '023',
+          'Caguas Municipio, PR': '025',
+          'Camuy Municipio, PR': '027',
+          'Canovanas Municipio, PR': '029',
+          'Carolina Municipio, PR': '031',
+          'Catano Municipio, PR': '033',
+          'Cayey Municipio, PR': '035',
+          'Ceiba Municipio, PR': '037',
+          'Ciales Municipio, PR': '039',
+          'Cidra Municipio, PR': '041',
+          'Coamo Municipio, PR': '043',
+          'Comerio Municipio, PR': '045',
+          'Corozal Municipio, PR': '047',
+          'Culebra Municipio, PR': '049',
+          'Dorado Municipio, PR': '051',
+          'Fajardo Municipio, PR': '053',
+          'Florida Municipio, PR': '054',
+          'Guanica Municipio, PR': '055',
+          'Guayama Municipio, PR': '057',
+          'Guayanilla Municipio, PR': '059',
+          'Guaynabo Municipio, PR': '061',
+          'Gurabo Municipio, PR': '063',
+          'Hatillo Municipio, PR': '065',
+          'Hormigueros Municipio, PR': '067',
+          'Humacao Municipio, PR': '069',
+          'Isabela Municipio, PR': '071',
+          'Jayuya Municipio, PR': '073',
+          'Juana Diaz Municipio, PR': '075',
+          'Juncos Municipio, PR': '077',
+          'Lajas Municipio, PR': '079',
+          'Lares Municipio, PR': '081',
+          'Las Marias Municipio, PR': '083',
+          'Las Piedras Municipio, PR': '085',
+          'Loiza Municipio, PR': '087',
+          'Luquillo Municipio, PR': '089',
+          'Manati Municipio, PR': '091',
+          'Maricao Municipio, PR': '093',
+          'Maunabo Municipio, PR': '095',
+          'Mayaguez Municipio, PR': '097',
+          'Moca Municipio, PR': '099',
+          'Morovis Municipio, PR': '101',
+          'Naguabo Municipio, PR': '103',
+          'Naranjito Municipio, PR': '105',
+          'Orocovis Municipio, PR': '107',
+          'Patillas Municipio, PR': '109',
+          'Penuelas Municipio, PR': '111',
+          'Ponce Municipio, PR': '113',
+          'Quebradillas Municipio, PR': '115',
+          'Rincon Municipio, PR': '117',
+          'Rio Grande Municipio, PR': '119',
+          'Sabana Grande Municipio, PR': '121',
+          'Salinas Municipio, PR': '123',
+          'San German Municipio, PR': '125',
+          'San Juan Municipio, PR': '127',
+          'San Lorenzo Municipio, PR': '129',
+          'San Sebastian Municipio, PR': '131',
+          'Santa Isabel Municipio, PR': '133',
+          'Toa Alta Municipio, PR': '135',
+          'Toa Baja Municipio, PR': '137',
+          'Trujillo Alto Municipio, PR': '139',
+          'Utuado Municipio, PR': '141',
+          'Vega Alta Municipio, PR': '143',
+          'Vega Baja Municipio, PR': '145',
+          'Vieques Municipio, PR': '147',
+          'Villalba Municipio, PR': '149',
+          'Yabucoa Municipio, PR': '151',
+          'Yauco Municipio, PR': '153'},
+  "CA01": { '--All--': '%', },
+  "CA02": { '--All--': '%', },
+  "CA03": { '--All--': '%', },
+  "CA04": { '--All--': '%', },
+  "CA05": { '--All--': '%', },
+  "CA13": { '--All--': '%', },
+  "CA07": { '--All--': '%', },
+  "CA14": { '--All--': '%', },
+  "CA08": { '--All--': '%', },
+  "CA09": { '--All--': '%', },
+  "CA10": { '--All--': '%', },
+  "CA11": { '--All--': '%', },
+  "CA12": { '--All--': '%', },
+}
+
+
+if __name__ == "__main__":
+    from sys import argv
+    from pprint import PrettyPrinter
+    pp = PrettyPrinter(indent=2)
+    
+    import csv
+    fipsreader = csv.reader(open(argv[1],'rb'), delimiter=',', quotechar='"')
+    for row in fipsreader:
+        try:
+            FIPS_COUNTIES[int(row[1])][row[3]] = row[2]
+        except KeyError:
+            FIPS_COUNTIES[int(row[1])] = {'--All--': '%'}
+            FIPS_COUNTIES[int(row[1])][row[3]] = row[2]
+    
+    pp.pprint(FIPS_COUNTIES)
+    
\ No newline at end of file
diff --git a/chirpui/importdialog.py b/chirpui/importdialog.py
new file mode 100644
index 0000000..b7417ce
--- /dev/null
+++ b/chirpui/importdialog.py
@@ -0,0 +1,647 @@
+# 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 gtk
+import gobject
+import pango
+
+from chirp import errors, chirp_common, generic_xml, import_logic
+from chirpui import common
+
+class WaitWindow(gtk.Window):
+    def __init__(self, msg, parent=None):
+        gtk.Window.__init__(self)
+        self.set_title("Please Wait")
+        self.set_type_hint(gtk.gdk.WINDOW_TYPE_HINT_DIALOG)
+        if parent:
+            self.set_transient_for(parent)
+            self.set_position(gtk.WIN_POS_CENTER_ON_PARENT)
+        else:
+            self.set_position(gtk.WIN_POS_CENTER)
+
+        vbox = gtk.VBox(False, 2)
+
+        l = gtk.Label(msg)
+        l.show()
+        vbox.pack_start(l)
+
+        self.prog = gtk.ProgressBar()
+        self.prog.show()
+        vbox.pack_start(self.prog)
+
+        vbox.show()
+        self.add(vbox)
+
+    def grind(self):
+        while gtk.events_pending():
+            gtk.main_iteration(False)
+
+        self.prog.pulse()
+
+    def set(self, fraction):
+        while gtk.events_pending():
+            gtk.main_iteration(False)
+
+        self.prog.set_fraction(fraction)
+
+class ImportMemoryBankJob(common.RadioJob):
+    def __init__(self, cb, dst_mem, src_radio, src_mem):
+        common.RadioJob.__init__(self, cb, None)
+        self.__dst_mem = dst_mem
+        self.__src_radio = src_radio
+        self.__src_mem = src_mem
+
+    def execute(self, radio):
+        import_logic.import_bank(radio, self.__src_radio,
+                                 self.__dst_mem, self.__src_mem)
+        if self.cb:
+            gobject.idle_add(self.cb, *self.cb_args)
+
+class ImportDialog(gtk.Dialog):
+
+    def _check_for_dupe(self, location):
+        iter = self.__store.get_iter_first()
+        while iter:
+            imp, loc = self.__store.get(iter, self.col_import, self.col_nloc)
+            if imp and loc == location:
+                return True
+            iter = self.__store.iter_next(iter)
+
+        return False
+
+    def _toggle(self, rend, path, col):
+        iter = self.__store.get_iter(path)
+        imp, nloc = self.__store.get(iter, self.col_import, self.col_nloc)
+        if not imp and self._check_for_dupe(nloc):
+            d = gtk.MessageDialog(parent=self, buttons=gtk.BUTTONS_OK)
+            d.set_property("text",
+                           _("Location {number} is already being imported. "
+                             "Choose another value for 'New Location' "
+                             "before selection 'Import'").format(\
+                    number=nloc))
+            d.run()
+            d.destroy()
+        else:
+            self.__store[path][col] = not imp
+
+    def _render(self, _, rend, model, iter, colnum):
+        newloc, imp = model.get(iter, self.col_nloc, self.col_import)
+        lo,hi = self.dst_radio.get_features().memory_bounds
+
+        rend.set_property("text", "%i" % newloc)
+        if newloc in self.used_list and imp:
+            rend.set_property("foreground", "goldenrod")
+            rend.set_property("weight", pango.WEIGHT_BOLD)
+        elif newloc < lo or newloc > hi:
+            rend.set_property("foreground", "red")
+            rend.set_property("weight", pango.WEIGHT_BOLD)
+        else:
+            rend.set_property("foreground", "black")
+            rend.set_property("weight", pango.WEIGHT_NORMAL)
+
+    def _edited(self, rend, path, new, col):
+        iter = self.__store.get_iter(path)
+        
+        if col == self.col_nloc:
+            nloc, = self.__store.get(iter, self.col_nloc)
+
+            try:
+                val = int(new)
+            except ValueError:
+                common.show_error(_("Invalid value. Must be an integer."))
+                return
+
+            if val == nloc:
+                return
+
+            if self._check_for_dupe(val):
+                d = gtk.MessageDialog(parent=self, buttons=gtk.BUTTONS_OK)
+                d.set_property("text",
+                               _("Location {number} is already being "
+                                 "imported").format(number=val))
+                d.run()
+                d.destroy()
+                return
+
+            self.record_use_of(val)
+
+        elif col == self.col_name or col == self.col_comm:
+            val = str(new)
+
+        else:
+            return
+
+        self.__store.set(iter, col, val)
+
+    def get_import_list(self):
+        import_list = []
+        iter = self.__store.get_iter_first()
+        while iter:
+            old, new, name, comm, enb = self.__store.get(iter,
+                                             self.col_oloc,
+                                             self.col_nloc,
+                                             self.col_name,
+                                             self.col_comm,
+                                             self.col_import)
+            if enb:
+                import_list.append((old, new, name, comm))
+            iter = self.__store.iter_next(iter)
+
+        return import_list
+
+    def ensure_calls(self, dst_rthread, import_list):
+        rlist_changed = False
+        ulist_changed = False
+
+        if not isinstance(self.dst_radio, chirp_common.IcomDstarSupport):
+            return
+
+        ulist = self.dst_radio.get_urcall_list()
+        rlist = self.dst_radio.get_repeater_call_list()
+
+        for old, new in import_list:
+            mem = self.src_radio.get_memory(old)
+            if isinstance(mem, chirp_common.DVMemory):
+                if mem.dv_urcall not in ulist:
+                    print "Adding %s to ucall list" % mem.dv_urcall
+                    ulist.append(mem.dv_urcall)
+                    ulist_changed = True
+                if mem.dv_rpt1call not in rlist:
+                    print "Adding %s to rcall list" % mem.dv_rpt1call
+                    rlist.append(mem.dv_rpt1call)
+                    rlist_changed = True
+                if mem.dv_rpt2call not in rlist:
+                    print "Adding %s to rcall list" % mem.dv_rpt2call
+                    rlist.append(mem.dv_rpt2call)
+                    rlist_changed = True
+                
+        if ulist_changed:
+            job = common.RadioJob(None, "set_urcall_list", ulist)
+            job.set_desc(_("Updating URCALL list"))
+            dst_rthread._qsubmit(job, 0)
+
+        if rlist_changed:
+            job = common.RadioJob(None, "set_repeater_call_list", ulist)
+            job.set_desc(_("Updating RPTCALL list"))
+            dst_rthread._qsubmit(job, 0)
+            
+        return
+
+    def _convert_power(self, dst_levels, src_levels, mem):
+        if not dst_levels:
+            mem.power = None
+            return
+        elif not mem.power:
+            # Source radio does not support power levels, so choose the
+            # first (highest) level from the destination radio.
+            mem.power = dst_levels[0]
+            return ""
+
+        # If both radios support power levels, we need to decide how to
+        # convert the source power level to a valid one for the destination
+        # radio.  To do that, find the absolute level of the source value
+        # and calculate the different between it and all the levels of the
+        # destination, choosing the one that matches most closely.
+
+        deltas = [abs(mem.power - power) for power in dst_levels]
+        mem.power = dst_levels[deltas.index(min(deltas))]
+
+    def do_soft_conversions(self, dst_features, src_features, mem):
+        self._convert_power(dst_features.valid_power_levels,
+                            src_features.valid_power_levels,
+                            mem)
+
+        return mem
+
+    def do_import_banks(self):
+        try:
+            dst_banks = self.dst_radio.get_banks()
+            src_banks = self.src_radio.get_banks()
+            if not dst_banks or not src_banks:
+                raise Exception()
+        except Exception:
+            print "One or more of the radios doesn't support banks"
+            return
+
+        if not isinstance(self.dst_radio, generic_xml.XMLRadio) and \
+                len(dst_banks) != len(src_banks):
+            print "Source and destination radios have a different number of banks"
+        else:
+            self.dst_radio.set_banks(src_banks)
+
+    def do_import(self, dst_rthread):
+        i = 0
+        error_messages = {}
+        import_list = self.get_import_list()
+
+        src_features = self.src_radio.get_features()
+
+        for old, new, name, comm in import_list:
+            i += 1
+            print "%sing %i -> %i" % (self.ACTION, old, new)
+
+            src = self.src_radio.get_memory(old)
+
+            try:
+                mem = import_logic.import_mem(self.dst_radio,
+                                              src_features,
+                                              src,
+                                              {"number" : new,
+                                               "name"   : name,
+                                               "comment": comm})
+            except import_logic.ImportError, e:
+                print e
+                error_messages[new] = str(e)
+                continue
+
+            job = common.RadioJob(None, "set_memory", mem)
+            job.set_desc(_("Setting memory {number}").format(number=mem.number))
+            dst_rthread._qsubmit(job, 0)
+
+            job = ImportMemoryBankJob(None, mem, self.src_radio, src)
+            job.set_desc(_("Importing bank information"))
+            dst_rthread._qsubmit(job, 0)
+
+        if error_messages.keys():
+            msg = _("Error importing memories:") + "\r\n"
+            for num, msgs in error_messages.items():
+                msg += "%s: %s" % (num, ",".join(msgs))
+            common.show_error(msg)
+
+        return i
+
+    def make_view(self):
+        editable = [self.col_nloc, self.col_name, self.col_comm]
+
+        self.__store = gtk.ListStore(gobject.TYPE_BOOLEAN, # Import
+                                     gobject.TYPE_INT,     # Source loc
+                                     gobject.TYPE_INT,     # Destination loc
+                                     gobject.TYPE_STRING,  # Name
+                                     gobject.TYPE_STRING,  # Frequency
+                                     gobject.TYPE_STRING,  # Comment
+                                     gobject.TYPE_BOOLEAN,
+                                     gobject.TYPE_STRING)
+        self.__view = gtk.TreeView(self.__store)
+        self.__view.show()
+
+        tips = gtk.Tooltips()
+
+        for k in self.caps.keys():
+            t = self.types[k]
+
+            if t == gobject.TYPE_BOOLEAN:
+                rend = gtk.CellRendererToggle()
+                rend.connect("toggled", self._toggle, k)
+                column = gtk.TreeViewColumn(self.caps[k], rend,
+                                            active=k,
+                                            sensitive=self.col_okay,
+                                            activatable=self.col_okay)
+            else:
+                rend = gtk.CellRendererText()
+                if k in editable:
+                    rend.set_property("editable", True)
+                    rend.connect("edited", self._edited, k)
+                column = gtk.TreeViewColumn(self.caps[k], rend,
+                                            text=k,
+                                            sensitive=self.col_okay)
+
+            if k == self.col_nloc:
+                column.set_cell_data_func(rend, self._render, k)
+
+            if k in self.tips.keys():
+                print "Doing %s" % k
+                lab = gtk.Label(self.caps[k])
+                column.set_widget(lab)
+                tips.set_tip(lab, self.tips[k])
+                lab.show()
+            column.set_sort_column_id(k)
+            self.__view.append_column(column)
+
+        self.__view.set_tooltip_column(self.col_tmsg)
+        
+        sw = gtk.ScrolledWindow()
+        sw.set_policy(gtk.POLICY_AUTOMATIC, gtk.POLICY_AUTOMATIC)
+        sw.add(self.__view)
+        sw.show()
+
+        return sw
+
+    def __select_all(self, button, state):
+        iter = self.__store.get_iter_first()
+        while iter:
+            _state, okay, = self.__store.get(iter,
+                                             self.col_import,
+                                             self.col_okay)
+            if state is None:
+                _state = not _state and okay
+            else:
+                _state = state and okay
+            self.__store.set(iter, self.col_import, _state)
+            iter = self.__store.iter_next(iter)
+
+    def __incrnew(self, button, delta):
+        iter = self.__store.get_iter_first()
+        while iter:
+            pos = self.__store.get(iter, self.col_nloc)[0]
+            pos += delta
+            if pos < 0:
+                pos = 0
+            self.__store.set(iter, self.col_nloc, pos)
+            iter = self.__store.iter_next(iter)
+
+    def __autonew(self, button):
+        pos = self.dst_radio.get_features().memory_bounds[0]
+        iter = self.__store.get_iter_first()
+        while iter:
+            selected, okay = self.__store.get(iter,
+                                              self.col_import, self.col_okay)
+            if selected and okay:
+                self.__store.set(iter, self.col_nloc, pos)
+                pos += 1
+            iter = self.__store.iter_next(iter)
+
+    def __revrnew(self, button):
+        positions = []
+        iter = self.__store.get_iter_first()
+        while iter:
+            positions.append(self.__store.get(iter, self.col_nloc)[0])
+            iter = self.__store.iter_next(iter)
+
+        iter = self.__store.get_iter_first()
+        while iter:
+            self.__store.set(iter, self.col_nloc, positions.pop())
+            iter = self.__store.iter_next(iter)
+
+    def make_select(self):
+        hbox = gtk.HBox(True, 2)
+
+        all = gtk.Button(_("All"));
+        all.connect("clicked", self.__select_all, True)
+        all.set_size_request(50, 25)
+        all.show()
+        hbox.pack_start(all, 0, 0, 0)
+
+        none = gtk.Button(_("None"));
+        none.connect("clicked", self.__select_all, False)
+        none.set_size_request(50, 25)
+        none.show()
+        hbox.pack_start(none, 0, 0, 0)
+
+        inv = gtk.Button(_("Inverse"))
+        inv.connect("clicked", self.__select_all, None)
+        inv.set_size_request(50, 25)
+        inv.show()
+        hbox.pack_start(inv, 0, 0, 0)
+
+        frame = gtk.Frame(_("Select"))
+        frame.show()
+        frame.add(hbox)
+        hbox.show()
+
+        return frame
+
+    def make_adjust(self):
+        hbox = gtk.HBox(True, 2)
+
+        incr = gtk.Button("+100")
+        incr.connect("clicked", self.__incrnew, 100)
+        incr.set_size_request(50, 25)
+        incr.show()
+        hbox.pack_start(incr, 0, 0, 0)
+
+        incr = gtk.Button("+10")
+        incr.connect("clicked", self.__incrnew, 10)
+        incr.set_size_request(50, 25)
+        incr.show()
+        hbox.pack_start(incr, 0, 0, 0)
+
+        incr = gtk.Button("+1")
+        incr.connect("clicked", self.__incrnew, 1)
+        incr.set_size_request(50, 25)
+        incr.show()
+        hbox.pack_start(incr, 0, 0, 0)
+
+        decr = gtk.Button("-1")
+        decr.connect("clicked", self.__incrnew, -1)
+        decr.set_size_request(50, 25)
+        decr.show()
+        hbox.pack_start(decr, 0, 0, 0)
+
+        decr = gtk.Button("-10")
+        decr.connect("clicked", self.__incrnew, -10)
+        decr.set_size_request(50, 25)
+        decr.show()
+        hbox.pack_start(decr, 0, 0, 0)
+
+        decr = gtk.Button("-100")
+        decr.connect("clicked", self.__incrnew, -100)
+        decr.set_size_request(50, 25)
+        decr.show()
+        hbox.pack_start(decr, 0, 0, 0)
+
+        auto = gtk.Button(_("Auto"))
+        auto.connect("clicked", self.__autonew)
+        auto.set_size_request(50, 25)
+        auto.show()
+        hbox.pack_start(auto, 0, 0, 0)
+
+        revr = gtk.Button(_("Reverse"))
+        revr.connect("clicked", self.__revrnew)
+        revr.set_size_request(50, 25)
+        revr.show()
+        hbox.pack_start(revr, 0, 0, 0)
+
+        frame = gtk.Frame(_("Adjust New Location"))
+        frame.show()
+        frame.add(hbox)
+        hbox.show()
+
+        return frame
+
+    def make_options(self):
+        hbox = gtk.HBox(True, 2)
+
+        confirm = gtk.CheckButton(_("Confirm overwrites"))
+        confirm.connect("toggled", __set_confirm)
+        confirm.show()
+
+        hbox.pack_start(confirm, 0, 0, 0)
+
+        frame = gtk.Frame(_("Options"))
+        frame.add(hbox)
+        frame.show()
+        hbox.show()
+
+        return frame
+
+    def make_controls(self):
+        hbox = gtk.HBox(False, 2)
+        
+        hbox.pack_start(self.make_select(), 0, 0, 0)
+        hbox.pack_start(self.make_adjust(), 0, 0, 0)
+        #hbox.pack_start(self.make_options(), 0, 0, 0)
+        hbox.show()
+
+        return hbox
+
+    def build_ui(self):
+        self.vbox.pack_start(self.make_view(), 1, 1, 1)
+        self.vbox.pack_start(self.make_controls(), 0, 0, 0)
+
+    def record_use_of(self, number):
+        lo, hi = self.dst_radio.get_features().memory_bounds
+
+        if number < lo or number > hi:
+            return
+
+        try:
+            mem = self.dst_radio.get_memory(number)
+            if mem and not mem.empty and number not in self.used_list:
+                self.used_list.append(number)
+        except errors.InvalidMemoryLocation:
+            print "Location %i empty or at limit of destination radio" % number
+        except errors.InvalidDataError, e:
+            print "Got error from radio, assuming %i beyond limits: %s" % \
+                (number, e)
+
+    def populate_list(self):
+        start, end = self.src_radio.get_features().memory_bounds
+        for i in range(start, end+1):
+            if end > 50 and i % (end/50) == 0:
+                self.ww.set(float(i) / end)
+            try:
+                mem = self.src_radio.get_memory(i)
+            except errors.InvalidMemoryLocation, e:
+                continue
+            except Exception, e:
+                self.__store.append(row=(False,
+                                         i,
+                                         i,
+                                         "ERROR",
+                                         chirp_common.format_freq(0),
+                                         "",
+                                         False,
+                                         str(e),
+                                         ))
+                self.record_use_of(i)
+                continue
+            if mem.empty:
+                continue
+
+            self.ww.set(float(i) / end)
+            try:
+                msgs = self.dst_radio.validate_memory(
+                        import_logic.import_mem(self.dst_radio,
+                                                self.src_radio.get_features(),
+                                                mem))
+            except import_logic.DestNotCompatible:
+                msgs = self.dst_radio.validate_memory(mem)
+            errs = [x for x in msgs if isinstance(x, chirp_common.ValidationError)]
+            if errs:
+                msg = _("Cannot be imported because") + ":\r\n"
+                msg += ",".join(errs)
+            else:
+                errs = []
+                msg = "Memory can be imported into target"
+
+            self.__store.append(row=(not bool(msgs),
+                                     mem.number,
+                                     mem.number,
+                                     mem.name,
+                                     chirp_common.format_freq(mem.freq),
+                                     mem.comment,
+                                     not bool(errs),
+                                     msg
+                                     ))
+            self.record_use_of(mem.number)
+
+
+    TITLE = _("Import From File")
+    ACTION = _("Import")
+
+    def __init__(self, src_radio, dst_radio, parent=None):
+        gtk.Dialog.__init__(self,
+                            buttons=(gtk.STOCK_OK, gtk.RESPONSE_OK,
+                                     gtk.STOCK_CANCEL, gtk.RESPONSE_CANCEL),
+                            title=self.TITLE,
+                            parent=parent)
+
+        self.col_import = 0
+        self.col_nloc = 1
+        self.col_oloc = 2
+        self.col_name = 3
+        self.col_freq = 4
+        self.col_comm = 5
+        self.col_okay = 6
+        self.col_tmsg = 7
+
+        self.caps = {
+            self.col_import : self.ACTION,
+            self.col_nloc   : _("To"),
+            self.col_oloc   : _("From"),
+            self.col_name   : _("Name"),
+            self.col_freq   : _("Frequency"),
+            self.col_comm   : _("Comment"),
+            }
+
+        self.tips = {
+            self.col_nloc : _("Location memory will be imported into"),
+            self.col_oloc : _("Location of memory in the file being imported"),
+            }
+
+        self.types = {
+            self.col_import : gobject.TYPE_BOOLEAN,
+            self.col_oloc   : gobject.TYPE_INT,
+            self.col_nloc   : gobject.TYPE_INT,
+            self.col_name   : gobject.TYPE_STRING,
+            self.col_freq   : gobject.TYPE_STRING,
+            self.col_comm   : gobject.TYPE_STRING,
+            self.col_okay   : gobject.TYPE_BOOLEAN,
+            self.col_tmsg   : gobject.TYPE_STRING,
+            }
+
+        self.src_radio = src_radio
+        self.dst_radio = dst_radio
+
+        self.used_list = []
+        self.not_used_list = []
+
+        self.build_ui()
+        self.set_default_size(600, 400)
+
+        self.ww = WaitWindow(_("Preparing memory list..."), parent=parent)
+        self.ww.show()
+        self.ww.grind()
+
+        self.populate_list()
+
+        self.ww.hide()
+
+class ExportDialog(ImportDialog):
+    TITLE = _("Export To File")
+    ACTION = _("Export")
+
+if __name__ == "__main__":
+    from chirpui import editorset
+    import sys
+
+    f = sys.argv[1]
+    rc = editorset.radio_class_from_file(f)
+    radio = rc(f)
+
+    d = ImportDialog(radio)
+    d.run()
+
+    print d.get_import_list()
diff --git a/chirpui/inputdialog.py b/chirpui/inputdialog.py
new file mode 100644
index 0000000..a5c2def
--- /dev/null
+++ b/chirpui/inputdialog.py
@@ -0,0 +1,148 @@
+# 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 gtk
+
+from miscwidgets import make_choice
+from chirpui import reporting
+
+class TextInputDialog(gtk.Dialog):
+    def respond_ok(self, _):
+        self.response(gtk.RESPONSE_OK)
+
+    def __init__(self, **args):
+        buttons = (gtk.STOCK_CANCEL, gtk.RESPONSE_CANCEL,
+                   gtk.STOCK_OK, gtk.RESPONSE_OK)
+        gtk.Dialog.__init__(self, buttons=buttons, **args)
+
+        self.label = gtk.Label()
+        self.label.set_size_request(300, 100)
+        # pylint: disable-msg=E1101
+        self.vbox.pack_start(self.label, 1, 1, 0)
+       
+        self.text = gtk.Entry()
+        self.text.connect("activate", self.respond_ok, None)
+        # pylint: disable-msg=E1101
+        self.vbox.pack_start(self.text, 1, 1, 0)
+
+        self.label.show()
+        self.text.show()
+
+class ChoiceDialog(gtk.Dialog):
+    editable = False
+
+    def __init__(self, choices, **args):
+        buttons = (gtk.STOCK_CANCEL, gtk.RESPONSE_CANCEL,
+                   gtk.STOCK_OK, gtk.RESPONSE_OK)
+        gtk.Dialog.__init__(self, buttons=buttons, **args)
+
+        self.label = gtk.Label()
+        self.label.set_size_request(300, 100)
+        # pylint: disable-msg=E1101
+        self.vbox.pack_start(self.label, 1, 1, 0)
+        self.label.show()
+
+        try:
+            default = choices[0]
+        except IndexError:
+            default = None
+
+        self.choice = make_choice(sorted(choices), self.editable, default)
+        # pylint: disable-msg=E1101
+        self.vbox.pack_start(self.choice, 1, 1, 0)
+        self.choice.show()
+
+        self.set_default_response(gtk.RESPONSE_OK)
+
+class EditableChoiceDialog(ChoiceDialog):
+    editable = True
+
+    def __init__(self, choices, **args):
+        ChoiceDialog.__init__(self, choices, **args)
+
+        self.choice.child.set_activates_default(True)
+
+class ExceptionDialog(gtk.MessageDialog):
+    def __init__(self, exception, **args):
+        gtk.MessageDialog.__init__(self, buttons=gtk.BUTTONS_OK,
+                                   type=gtk.MESSAGE_ERROR, **args)
+        self.set_property("text", _("An error has occurred"))
+        self.format_secondary_text(str(exception))
+
+        import traceback
+        import sys
+        reporting.report_exception(traceback.format_exc(limit=30))
+        print "--- Exception Dialog: %s ---" % exception
+        traceback.print_exc(limit=100, file=sys.stdout)
+        print "----------------------------"
+
+class FieldDialog(gtk.Dialog):
+    def __init__(self, **kwargs):
+        if "buttons" not in kwargs.keys():
+            kwargs["buttons"] = (gtk.STOCK_OK, gtk.RESPONSE_OK,
+                                 gtk.STOCK_CANCEL, gtk.RESPONSE_CANCEL)
+
+        self.__fields = {}
+        self.set_default_response(gtk.RESPONSE_OK)
+
+        gtk.Dialog.__init__(self, **kwargs)
+
+    def response(self, _):
+        print "Blocking response"
+        return
+
+    def add_field(self, label, widget, validator=None):
+        box = gtk.HBox(True, 2)
+
+        lab = gtk.Label(label)
+        lab.show()
+
+        widget.set_size_request(150, -1)
+        widget.show()
+
+        box.pack_start(lab, 0, 0, 0)
+        box.pack_start(widget, 0, 0, 0)
+        box.show()
+
+        # pylint: disable-msg=E1101
+        self.vbox.pack_start(box, 0, 0, 0)
+    
+        self.__fields[label] = widget
+
+    def get_field(self, label):
+        return self.__fields.get(label, None)
+
+class OverwriteDialog(gtk.MessageDialog):
+    def __init__(self, filename):
+        gtk.Dialog.__init__(self,
+                            buttons=(_("Overwrite"), gtk.RESPONSE_OK,
+                                     gtk.STOCK_CANCEL, gtk.RESPONSE_CANCEL))
+
+        self.set_property("text", _("File Exists"))
+
+        text = \
+            _("The file {name} already exists. "
+              "Do you want to overwrite it?").format(name=filename)
+
+        self.format_secondary_text(text)
+
+if __name__ == "__main__":
+    # pylint: disable-msg=C0103
+    d = FieldDialog(buttons=(gtk.STOCK_OK, gtk.RESPONSE_OK))
+    d.add_field("Foo", gtk.Entry())
+    d.add_field("Bar", make_choice(["A", "B"]))
+    d.run()
+    gtk.main()
+    d.destroy()
diff --git a/chirpui/mainapp.py b/chirpui/mainapp.py
new file mode 100644
index 0000000..27c4b3c
--- /dev/null
+++ b/chirpui/mainapp.py
@@ -0,0 +1,1609 @@
+# 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/>.
+
+import os
+import tempfile
+import urllib
+from glob import glob
+import shutil
+import time
+
+import gtk
+import gobject
+gobject.threads_init()
+
+if __name__ == "__main__":
+    import sys
+    sys.path.insert(0, "..")
+
+from chirpui import inputdialog, common
+try:
+    import serial
+except ImportError,e:
+    common.log_exception()
+    common.show_error("\nThe Pyserial module is not installed!")
+from chirp import platform, generic_xml, generic_csv, directory, util
+from chirp import ic9x, kenwood_live, idrp, vx7, vx5, vx6
+from chirp import CHIRP_VERSION, chirp_common, detect, errors
+from chirp import icf, ic9x_icf
+from chirpui import editorset, clone, miscwidgets, config, reporting, fips
+
+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"]:
+            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 == 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)
+
+        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, -1, 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
+
+        print "Selected %s@%i and %s@%i" % (sel_a, sel_chan_a,
+                                            sel_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("Differences", 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)
+            a = util.hexprint(eset_a.rthread.radio._mmap.get_packed())
+            b = util.hexprint(eset_b.rthread.radio._mmap.get_packed())
+            common.show_diff_blob("Differences", common.simple_diff(a, b))
+        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 = [(_("CHIRP Radio Images") + " (*.img)", "*.img"),
+                     (_("CHIRP Files") + " (*.chirp)", "*.chirp"),
+                     (_("CSV Files") + " (*.csv)", "*.csv"),
+                     (_("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)
+            devices = radio.get_sub_devices()
+            del radio
+        else:
+            try:
+                radio = directory.get_radio_by_image(fname)
+            except errors.ImageDetectFailed:
+                radio = self._do_manual_select(fname)
+                if not radio:
+                    return
+                print "Manually selected %s" % radio
+            except Exception, e:
+                common.log_exception()
+                common.show_error(os.path.basename(fname) + ": " + str(e))
+                return
+
+            if radio.get_features().has_sub_devices:
+                devices = radio.get_sub_devices()
+            else:
+                devices = [radio]
+
+        prio = len(devices)
+        first_tab = False
+        for device in devices:
+            try:
+                eset = editorset.EditorSet(device, 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=error))
+                return
+    
+            eset.set_read_only(read_only)
+            self._connect_editorset(eset)
+            eset.show()
+            tab = self.tabs.append_page(eset, eset.get_tab_label())
+            if first_tab:
+                self.tabs.set_current_page(tab)
+                first_tab = False
+
+            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>.").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):
+        if radio.get_features().has_sub_devices:
+            devices = radio.get_sub_devices()
+        else:
+            devices = [radio]
+        
+        first_tab = True
+        for device in devices:
+            eset = editorset.EditorSet(device, self, tempname=tempname)
+            eset.connect("want-close", self.do_close)
+            eset.connect("status", self.ev_status)
+            eset.set_read_only(read_only)
+            eset.show()
+
+            tab = self.tabs.append_page(eset, eset.get_tab_label())
+            if first_tab:
+                self.tabs.set_current_page(tab)
+                first_tab = False
+
+        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)
+                                                     
+        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(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):
+        execpath = platform.get_platform().executable_path()
+        basepath = os.path.abspath(os.path.join(execpath, "stock_configs"))
+        if not os.path.exists(basepath):
+            basepath = "/usr/share/chirp/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))):
+                print "Skipping existing stock config"
+                continue
+            try:
+                shutil.copy(fn, stock_dir)
+                print "Copying %s -> %s" % (fn, stock_dir)
+            except Exception, e:
+                print "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:
+                print "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_experimental_warning()
+        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 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
+
+        print "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
+
+        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
+
+        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_CANCEL:
+                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 = [(_("CHIRP Files") + " (*.chirp)", "*.chirp"),
+                 (_("CHIRP Radio Images") + " (*.img)", "*.img"),
+                 (_("CSV Files") + " (*.csv)", "*.csv"),
+                 (_("EVE Files (VX5)") + " (*.eve)", "*.eve"),
+                 (_("ICF Files") + " (*.icf)", "*.icf"),
+                 (_("Kenwood HMK Files") + " (*.hmk)", "*.hmk"),
+                 (_("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_repeaterbook_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")
+            for k,v in fips.FIPS_COUNTIES[fips.FIPS_STATES[default_state]].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(self, do_import):
+        self.window.set_cursor(gtk.gdk.Cursor(gtk.gdk.WATCH))
+        if not self.do_repeaterbook_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 "%%")
+
+        # 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):
+            print "Failed, headers were:"
+            print str(headers)
+            common.show_error("RepeaterBook query failed")
+            self.window.set_cursor(None)
+            return
+
+        class RBRadio(generic_csv.CSVRadio,
+                      chirp_common.NetworkSourceRadio):
+            VENDOR = "RepeaterBook"
+            MODEL = ""
+
+        try:
+            # Validate CSV
+            radio = 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_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()
+            count = eset.do_import("rfinder://%s/%s/%f/%f/%i" % (email, passwd, lat, lon, miles))
+        else:
+            from chirp 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()
+            count = eset.do_import("radioreference://%s/%s/%s" % (zipcode, username, passwd))
+        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"),
+                 (_("CHIRP Files") + " (*.chirp)", "chirp"),
+                 ]
+
+        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])
+
+        d.set_name("CHIRP")
+        d.set_version(CHIRP_VERSION)
+        d.set_copyright("Copyright 2012 Dan Smith (KK7DS)")
+        d.set_website("http://chirp.danplanet.com")
+        d.set_authors(("Dan Smith <dsmith at danplanet.com>",
+                       _("With significant contributions by:"),
+                       "Marco IZ3GME",
+                       "Rick WZ3RO",
+                       "Tom KD7LXL",
+                       "Vernon N7OH"
+                       ))
+        d.set_translator_credits("Polish: Grzegorz SQ2RBY" +
+                                 os.linesep +
+                                 "Italian: Fabio IZ2QDH" +
+                                 os.linesep +
+                                 "Dutch: Michael PD4MT" +
+                                 os.linesep +
+                                 "German: Benjamin HB9EUK")
+        d.set_comments(verinfo)
+        
+        d.run()
+        d.destroy()
+
+    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)
+
+        label = gtk.Label(_("Visible columns for {radio}").format(radio=radio_name))
+        label.show()
+        vbox.pack_start(label)
+
+        fields = []
+        memedit = eset.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()
+        eset.editors["memedit"].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)
+            d.set_markup("<b><big>" + _("Reporting is disabled") + "</big></b>")
+            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_autorpt(self, action):
+        CONF.set_bool("autorpt", 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_change_language(self):
+        langs = ["Auto", "English", "Polish", "Italian", "Dutch", "German",
+                 "Hungarian"]
+        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:
+            print "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 ["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 ["qrbook", "irbook"]:
+            self.do_repeaterbook(action[0] == "i")
+        elif action == "about":
+            self.do_about()
+        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 == "autorpt":
+            self.do_toggle_autorpt(_action)
+        elif action == "developer":
+            self.do_toggle_developer(_action)
+        elif action in ["cut", "copy", "paste", "delete",
+                        "move_up", "move_dn", "exchange",
+                        "devshowraw", "devdiffraw"]:
+            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="move_up"/>
+      <menuitem action="move_dn"/>
+      <menuitem action="exchange"/>
+    </menu>
+    <menu action="view">
+      <menuitem action="columns"/>
+      <menuitem action="hide_unused"/>
+      <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="iradioreference"/>
+        <menuitem action="irbook"/>
+        <menuitem action="irfinder"/>
+      </menu>
+      <menu action="querysrc" name="querysrc">
+        <menuitem action="qradioreference"/>
+        <menuitem action="qrbook"/>
+        <menuitem action="qrfinder"/>
+      </menu>
+      <menu action="stock" name="stock"/>
+      <separator/>
+      <menuitem action="autorpt"/>
+      <separator/>
+      <menuitem action="cancelq"/>
+    </menu>
+    <menu action="help">
+      <menuitem action="about"/>
+      <menuitem action="report"/>
+      <menuitem action="developer"/>
+    </menu>
+  </menubar>
+</ui>
+"""
+        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"), "<Ctrl>x", None, self.mh),
+            ('copy', None, _("_Copy"), "<Ctrl>c", None, self.mh),
+            ('paste', None, _("_Paste"), "<Ctrl>v", None, self.mh),
+            ('delete', None, _("_Delete"), "Delete", None, self.mh),
+            ('move_up', None, _("Move _Up"), "<Control>Up", None, self.mh),
+            ('move_dn', None, _("Move Dow_n"), "<Control>Down", None, self.mh),
+            ('exchange', None, _("E_xchange"), "<Control><Shift>x", 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'), "<Control><Shift>r", None, self.mh),
+            ('devdiffraw', None, _("Diff raw memories"), "<Control><Shift>d", None, self.mh),
+            ('devdifftab', None, _("Diff tabs"), "<Control><Shift>t", None, self.mh),
+            ('language', None, _("Change language"), None, None, self.mh),
+            ('radio', None, _("_Radio"), None, None, self.mh),
+            ('download', None, _("Download From Radio"), "<Alt>d", None, self.mh),
+            ('upload', None, _("Upload To Radio"), "<Alt>u", None, self.mh),
+            ('import', None, _("Import"), "<Alt>i", None, self.mh),
+            ('export', None, _("Export"), "<Alt>x", None, self.mh),
+            ('importsrc', None, _("Import from data source"), 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),
+            ('querysrc', None, _("Query data source"), None, None, self.mh),
+            ('qradioreference', None, _("RadioReference.com"), None, None, self.mh),
+            ('qrfinder', None, _("RFinder"), None, None, self.mh),
+            ('qrbook', None, _("RepeaterBook"), 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),
+            ('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),
+            ]
+
+        conf = config.get()
+        re = not conf.get_bool("no_report");
+        hu = conf.get_bool("hide_unused", "memedit")
+        ro = conf.get_bool("autorpt", "memedit")
+        dv = conf.get_bool("developer", "state")
+
+        toggles = [\
+            ('report', None, _("Report statistics"), None, None, self.mh, re),
+            ('hide_unused', None, _("Hide Unused Fields"), None, None, self.mh, hu),
+            ('autorpt', None, _("Automatic Repeater Offset"), None, None, self.mh, ro),
+            ('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.recentmenu = self.menu_uim.get_widget("/MenuBar/file/recent")
+
+        # 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()
+
+        return self.tabs        
+
+    def close_out(self):
+        num = self.tabs.get_n_pages()
+        while num > 0:
+            num -= 1
+            print "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()
+
+        memedit = lambda 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):
+        execpath = platform.get_platform().executable_path()
+        path = os.path.abspath(os.path.join(execpath, "share", "chirp.png"))
+        if not os.path.exists(path):
+            path = "/usr/share/pixmaps/chirp.png"
+
+        if os.path.exists(path):
+            self.set_icon_from_file(path)
+        else:
+            print "Icon %s not found" % path
+
+    def _updates(self, version):
+        if not version:
+            return
+
+        if version == CHIRP_VERSION:
+            return
+
+        print "Server reports version %s is available" % version
+
+        # Report new updates every seven days
+        intv = 3600 * 24 * 7
+
+        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, parent=self,
+                              type=gtk.MESSAGE_INFO)
+        d.set_property("text",
+                       _("A new version of CHIRP is available: " +
+                         "{ver}. ".format(ver=version) +
+                         "It is recommended that you upgrade, so " +
+                         "go to http://chirp.danplanet.com soon!"))
+        d.run()
+        d.destroy()
+
+    def _init_macos(self, menu_bar):
+        try:
+            import gtk_osxapplication
+            macapp = gtk_osxapplication.OSXApplication()
+        except ImportError, e:
+            print "No MacOS support: %s" % e
+            return
+        
+        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)
+
+        macapp.set_use_quartz_accelerators(False)
+        macapp.ready()
+
+        print "Initialized MacOS support"
+
+    def __init__(self, *args, **kwargs):
+        gtk.Window.__init__(self, *args, **kwargs)
+
+        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)
+
+        self.set_default_size(800, 600)
+        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)
+
+        if not CONF.is_defined("autorpt", "memedit"):
+            print "autorpt not set et"
+            CONF.set_bool("autorpt", True, "memedit")
+
+        self.update_recent_files()
+        self.update_stock_configs()
+        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)
diff --git a/chirpui/memdetail.py b/chirpui/memdetail.py
new file mode 100644
index 0000000..cc605fc
--- /dev/null
+++ b/chirpui/memdetail.py
@@ -0,0 +1,324 @@
+# Copyright 2012 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 gtk
+import os
+
+from chirp import chirp_common, settings
+from chirpui import miscwidgets, common
+
+POL = ["NN", "NR", "RN", "RR"]
+
+class ValueEditor:
+    """Base class"""
+    def __init__(self, features, memory, errfn, name, data=None):
+        self._features = features
+        self._memory = memory
+        self._errfn = errfn
+        self._name = name
+        self._widget = None
+        self._init(data)
+
+    def _init(self, data):
+        """Type-specific initialization"""
+
+    def get_widget(self):
+        """Returns the widget associated with this editor"""
+        return self._widget
+
+    def _mem_value(self):
+        """Returns the raw value from the memory associated with this name"""
+        if self._name.startswith("extra_"):
+            return self._memory.extra[self._name.split("_", 1)[1]].value
+        else:
+            return getattr(self._memory, self._name)
+
+    def _get_value(self):
+        """Returns the value from the widget that should be set in the memory"""
+
+    def update(self):
+        """Updates the memory object with self._getvalue()"""
+
+        try:
+            newval = self._get_value()
+        except ValueError, e:
+            self._errfn(self._name, str(e))
+            return str(e)
+
+        if self._name.startswith("extra_"):
+            try:
+                self._memory.extra[self._name.split("_", 1)[1]].value = newval
+            except settings.InternalError, e:
+                self._errfn(self._name, str(e))
+                return str(e)
+        else:
+            try:
+                setattr(self._memory, self._name, newval)
+            except chirp_common.ImmutableValueError, e:
+                if getattr(self._memory, self._name) != self._get_value():
+                    self._errfn(self._name, str(e))
+                    return str(e)
+            except ValueError, e:
+                self._errfn(self._name, str(e))
+                return str(e)
+
+        all_msgs = self._features.validate_memory(self._memory)
+        errs = []
+        for msg in all_msgs:
+            if isinstance(msg, chirp_common.ValidationError):
+                errs.append(str(msg))
+        if errs:
+            self._errfn(self._name, errs)
+        else:
+            self._errfn(self._name, None)
+
+class StringEditor(ValueEditor):
+    def _init(self, data):
+        self._widget = gtk.Entry(int(data))
+        self._widget.set_text(str(self._mem_value()))
+        self._widget.connect("changed", self.changed)
+
+    def _get_value(self):
+        return self._widget.get_text()
+
+    def changed(self, _widget):
+        self.update()
+
+class ChoiceEditor(ValueEditor):
+    def _init(self, data):
+        self._widget = miscwidgets.make_choice([str(x) for x in data],
+                                               False,
+                                               str(self._mem_value()))
+        self._widget.connect("changed", self.changed)
+
+    def _get_value(self):
+        return self._widget.get_active_text()
+
+    def changed(self, _widget):
+        self.update()
+
+class PowerChoiceEditor(ChoiceEditor):
+    def _init(self, data):
+        self._choices = data
+        ChoiceEditor._init(self, data)
+
+    def _get_value(self):
+        choice = self._widget.get_active_text()
+        for level in self._choices:
+            if str(level) == choice:
+                return level
+        raise Exception("Internal error: power level went missing")
+
+class IntChoiceEditor(ChoiceEditor):
+    def _get_value(self):
+        return int(self._widget.get_active_text())
+
+class FloatChoiceEditor(ChoiceEditor):
+    def _get_value(self):
+        return float(self._widget.get_active_text())
+
+class FreqEditor(StringEditor):
+    def _init(self, data):
+        StringEditor._init(self, 0)
+
+    def _mem_value(self):
+        return chirp_common.format_freq(StringEditor._mem_value(self))
+
+    def _get_value(self):
+        return chirp_common.parse_freq(self._widget.get_text())
+
+class BooleanEditor(ValueEditor):
+    def _init(self, data):
+        self._widget = gtk.CheckButton("Enabled")
+        self._widget.set_active(self._mem_value())
+        self._widget.connect("toggled", self.toggled)
+
+    def _get_value(self):
+        return self._widget.get_active()
+
+    def toggled(self, _widget):
+        self.update()
+
+class OffsetEditor(FreqEditor):
+    pass
+
+class MemoryDetailEditor(gtk.Dialog):
+    """Detail editor for a memory"""
+
+    def _add(self, tab, row, name, editor, labeltxt):
+        label = gtk.Label(labeltxt)
+        img = gtk.Image()
+
+        label.show()
+        tab.attach(label, 0, 1, row, row+1)
+
+        editor.get_widget().show()
+        tab.attach(editor.get_widget(), 1, 2, row, row+1)
+
+        img.set_size_request(15, -1)
+        img.show()
+        tab.attach(img, 2, 3, row, row+1)
+
+        self._editors[name] = label, editor, img
+
+    def _set_doc(self, name, doc):
+        label, editor, _img = self._editors[name]
+        self._tips.set_tip(label, doc)
+        self._tips.set_tip(editor.get_widget(), doc)
+
+    def _make_ui(self):
+        tab = gtk.Table(len(self._order), 3, False)
+        self.vbox.pack_start(tab, 1, 1, 1)
+        tab.show()
+
+        row = 0
+
+        def _err(name, msg):
+            try:
+                _img = self._editors[name][2]
+            except KeyError:
+                print self._editors.keys()
+            if msg is None:
+                _img.clear()
+                self._tips.set_tip(_img, "")
+            else:
+                _img.set_from_stock(gtk.STOCK_DIALOG_ERROR, gtk.ICON_SIZE_MENU)
+                self._tips.set_tip(_img, str(msg))
+            self._errors[self._order.index(name)] = msg is not None
+            self.set_response_sensitive(gtk.RESPONSE_OK,
+                                        True not in self._errors)
+
+        for name in self._order:
+            labeltxt, editorcls, data = self._elements[name]
+
+            editor = editorcls(self._features, self._memory,
+                               _err, name, data)
+            self._add(tab, row, name, editor, labeltxt)
+            row += 1
+
+        for setting in self._memory.extra:
+            name = "extra_%s" % setting.get_name()
+            if isinstance(setting.value,
+                          settings.RadioSettingValueBoolean):
+                editor = BooleanEditor(self._features, self._memory,
+                                       _err, name)
+                self._add(tab, row, name, editor, setting.get_shortname())
+                self._set_doc(name, setting.__doc__)
+            elif isinstance(setting.value,
+                            settings.RadioSettingValueList):
+                editor = ChoiceEditor(self._features, self._memory,
+                                      _err, name, setting.value.get_options())
+                self._add(tab, row, name, editor, setting.get_shortname())
+                self._set_doc(name, setting.__doc__)
+            row += 1
+            self._order.append(name)
+
+    def __init__(self, features, memory, parent=None):
+        gtk.Dialog.__init__(self,
+                            title=_("Edit Memory"
+                                    "#{num}").format(num=memory.number),
+                            flags=gtk.DIALOG_MODAL,
+                            parent=parent,
+                            buttons=(gtk.STOCK_OK, gtk.RESPONSE_OK,
+                                     gtk.STOCK_CANCEL, gtk.RESPONSE_CANCEL))
+        self._tips = gtk.Tooltips()
+
+        self._features = features
+        self._memory = memory
+
+        self._editors = {}
+        self._elements = {
+            "freq" : (_("Frequency"), FreqEditor, None),
+            "name" : (_("Name"), StringEditor, features.valid_name_length),
+            "tmode" : (_("Tone Mode"), ChoiceEditor, features.valid_tmodes),
+            "rtone" : (_("Tone"), FloatChoiceEditor, chirp_common.TONES),
+            "ctone" : (_("ToneSql"), FloatChoiceEditor, chirp_common.TONES),
+            "dtcs"  : (_("DTCS Code"), IntChoiceEditor,
+                                       chirp_common.DTCS_CODES),
+            "dtcs_polarity" : (_("DTCS Pol"), ChoiceEditor, POL),
+            "cross_mode" : (_("Cross mode"),
+                            ChoiceEditor,
+                            features.valid_cross_modes),
+            "duplex" : (_("Duplex"), ChoiceEditor, features.valid_duplexes),
+            "offset" : (_("Offset"), OffsetEditor, None),
+            "mode" : (_("Mode"), ChoiceEditor, features.valid_modes),
+            "tuning_step" : (_("Tune Step"),
+                             FloatChoiceEditor,
+                             features.valid_tuning_steps),
+            "skip" : (_("Skip"), ChoiceEditor, features.valid_skips),
+            "comment" : (_("Comment"), StringEditor, 256),
+            }
+        self._order = ["freq", "name", "tmode", "rtone", "ctone", "cross_mode",
+                       "dtcs", "dtcs_polarity", "duplex", "offset",
+                       "mode", "tuning_step", "skip", "comment"]
+
+        if self._features.has_rx_dtcs:
+            self._elements['rx_dtcs'] = (_("RX DTCS Code"),
+                                         IntChoiceEditor,
+                                         chirp_common.DTCS_CODES)
+            self._order.insert(self._order.index("dtcs") + 1, "rx_dtcs")
+
+        if self._features.valid_power_levels:
+            self._elements["power"] = (_("Power"),
+                                       PowerChoiceEditor,
+                                       features.valid_power_levels)
+            self._order.insert(self._order.index("skip"), "power")
+
+        self._make_ui()
+        self.set_default_size(400, -1)
+
+        hide_rules = [
+            ("name", features.has_name),
+            ("tmode", len(features.valid_tmodes) > 0),
+            ("ctone", features.has_ctone),
+            ("dtcs", features.has_dtcs),
+            ("dtcs_polarity", features.has_dtcs_polarity),
+            ("cross_mode", "Cross" in features.valid_tmodes),
+            ("duplex", len(features.valid_duplexes) > 0),
+            ("offset", features.has_offset),
+            ("mode", len(features.valid_modes) > 0),
+            ("tuning_step", features.has_tuning_step),
+            ("skip", len(features.valid_skips) > 0),
+            ("comment", features.has_comment),
+            ]
+
+        for name, visible in hide_rules:
+            if not visible:
+                for widget in self._editors[name]:
+                    if isinstance(widget, ValueEditor):
+                        widget.get_widget().hide()
+                    else:
+                        widget.hide()
+
+        self._errors = [False] * len(self._order)
+
+        self.connect("response", self._validate)
+
+    def _validate(self, _dialog, response):
+        if response == gtk.RESPONSE_OK:
+            all_msgs = self._features.validate_memory(self._memory)
+            errors = []
+            for msg in all_msgs:
+                if isinstance(msg, chirp_common.ValidationError):
+                    errors.append(msg)
+            if errors:
+                common.show_error_text(_("Memory validation failed:"),
+                                       os.linesep +
+                                       os.linesep.join(errors))
+                self.emit_stop_by_name('response')
+
+    def get_memory(self):
+        self._memory.empty = False
+        return self._memory
diff --git a/chirpui/memedit.py b/chirpui/memedit.py
new file mode 100644
index 0000000..c598624
--- /dev/null
+++ b/chirpui/memedit.py
@@ -0,0 +1,1495 @@
+#
+# 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/>.
+
+if __name__ == "__main__":
+    import sys
+    sys.path.insert(0, "..")
+
+import threading
+
+import gtk
+import pango
+from gobject import TYPE_INT, \
+    TYPE_DOUBLE as TYPE_FLOAT, \
+    TYPE_STRING, \
+    TYPE_BOOLEAN, \
+    TYPE_PYOBJECT, \
+    TYPE_INT64
+import gobject
+import pickle
+import os
+
+from chirpui import common, shiftdialog, miscwidgets, config, memdetail
+from chirp import chirp_common, errors, directory, import_logic
+
+def handle_toggle(_, path, store, col):
+    store[path][col] = not store[path][col]    
+
+def handle_ed(_, iter, new, store, col):
+    old, = store.get(iter, col)
+    if old != new:
+        store.set(iter, col, new)
+        return True
+    else:
+        return False
+
+class ValueErrorDialog(gtk.MessageDialog):
+    def __init__(self, exception, **args):
+        gtk.MessageDialog.__init__(self, buttons=gtk.BUTTONS_OK, **args)
+        self.set_property("text", _("Invalid value for this field"))
+        self.format_secondary_text(str(exception))
+
+def iter_prev(store, iter):
+    row = store.get_path(iter)[0]
+    if row == 0:
+        return None
+    return store.get_iter((row - 1,))
+
+class MemoryEditor(common.Editor):
+    cols = [
+        (_("Loc")           , TYPE_INT,     gtk.CellRendererText,  ),
+        (_("Frequency")     , TYPE_INT64,   gtk.CellRendererText,  ),
+        (_("Name")          , TYPE_STRING,  gtk.CellRendererText,  ), 
+        (_("Tone Mode")     , TYPE_STRING,  gtk.CellRendererCombo, ),
+        (_("Tone")          , TYPE_FLOAT,   gtk.CellRendererCombo, ),
+        (_("ToneSql")       , TYPE_FLOAT,   gtk.CellRendererCombo, ),
+        (_("DTCS Code")     , TYPE_INT,     gtk.CellRendererCombo, ),
+        (_("DTCS Rx Code")  , TYPE_INT,     gtk.CellRendererCombo, ),
+        (_("DTCS Pol")      , TYPE_STRING,  gtk.CellRendererCombo, ),
+        (_("Cross Mode")    , TYPE_STRING,  gtk.CellRendererCombo, ),
+        (_("Duplex")        , TYPE_STRING,  gtk.CellRendererCombo, ),
+        (_("Offset")        , TYPE_INT64,   gtk.CellRendererText,  ),
+        (_("Mode")          , TYPE_STRING,  gtk.CellRendererCombo, ),
+        (_("Power")         , TYPE_STRING,  gtk.CellRendererCombo, ),
+        (_("Tune Step")     , TYPE_FLOAT,   gtk.CellRendererCombo, ),
+        (_("Skip")          , TYPE_STRING,  gtk.CellRendererCombo, ),
+        (_("Comment")       , TYPE_STRING,  gtk.CellRendererText,  ),
+        ("_filled"          , TYPE_BOOLEAN, None,                  ),
+        ("_hide_cols"       , TYPE_PYOBJECT,None,                  ),
+        ("_extd"            , TYPE_STRING,  None,                  ),
+        ]
+
+    defaults = {
+        _("Name")          : "",
+        _("Frequency")     : 146010000,
+        _("Tone")          : 88.5,
+        _("ToneSql")       : 88.5,
+        _("DTCS Code")     : 23,
+        _("DTCS Rx Code")  : 23,
+        _("DTCS Pol")      : "NN",
+        _("Cross Mode")    : "Tone->Tone",
+        _("Duplex")        : "",
+        _("Offset")        : 0,
+        _("Mode")          : "FM",
+        _("Power")         : "",
+        _("Tune Step")     : 5.0,
+        _("Tone Mode")     : "",
+        _("Skip")          : "",
+        _("Comment")       : "",
+        }
+
+    choices = {
+        _("Tone")          : chirp_common.TONES,
+        _("ToneSql")       : chirp_common.TONES,
+        _("DTCS Code")     : sorted(chirp_common.DTCS_CODES +
+                                    chirp_common.DTCS_EXTRA_CODES),
+        _("DTCS Rx Code")  : sorted(chirp_common.DTCS_CODES +
+                                    chirp_common.DTCS_EXTRA_CODES),
+        _("DTCS Pol")      : ["NN", "NR", "RN", "RR"],
+        _("Mode")          : chirp_common.MODES,
+        _("Power")         : [],
+        _("Duplex")        : ["", "-", "+", "split", "off"],
+        _("Tune Step")     : chirp_common.TUNING_STEPS,
+        _("Tone Mode")     : ["", "Tone", "TSQL", "DTCS"],
+        _("Cross Mode")    : chirp_common.CROSS_MODES,
+        }
+    
+    def ed_name(self, _, __, new, ___):
+        return self.rthread.radio.filter_name(new)
+
+    def ed_offset(self, _, path, new, __):
+        return chirp_common.parse_freq(new)
+
+    def ed_freq(self, _foo, path, new, colnum):
+        iter = self.store.get_iter(path)
+        prev, = self.store.get(iter, colnum)
+
+        def set_offset(path, offset):
+            if offset > 0:
+                dup = "+"
+            elif offset == 0:
+                dup = ""
+            else:
+                dup = "-"
+                offset *= -1
+
+            if offset:
+                self.store.set(iter, self.col(_("Offset")), offset)
+
+            self.store.set(iter, self.col(_("Duplex")), dup)
+
+        def set_ts(ts):
+            self.store.set(iter, self.col(_("Tune Step")), ts)
+
+        def get_ts(path):
+            return self.store.get(iter, self.col(_("Tune Step")))[0]
+
+        try:
+            new = chirp_common.parse_freq(new)
+        except ValueError, e:
+            print e
+            new = None
+
+        if not self._features.has_nostep_tuning:
+            set_ts(chirp_common.required_step(new))
+
+        if new and self._config.get_bool("autorpt") and new != prev:
+            try:
+                band = chirp_common.freq_to_band(new)
+                set_offset(path, 0)
+                for lo, hi, offset in chirp_common.STD_OFFSETS[band]:
+                    if new > lo and new < hi:
+                        set_offset(path, offset)
+                        break
+            except Exception, e:
+                pass
+
+        return new
+
+    def ed_loc(self, _, path, new, __):
+        iter = self.store.get_iter(path)
+        curloc, = self.store.get(iter, self.col(_("Loc")))
+
+        job = common.RadioJob(None, "erase_memory", curloc)
+        job.set_desc(_("Erasing memory {loc}").format(loc=curloc))
+        self.rthread.submit(job)
+
+        self.need_refresh = True
+
+        return new
+
+    def ed_duplex(self, _foo1, path, new, _foo2):
+        if new == "":
+            return # Fast path outta here
+
+        iter = self.store.get_iter(path)
+        freq, = self.store.get(iter, self.col(_("Frequency")))
+        if new == "split":
+            # If we're going to split mode, use the current
+            # RX frequency as the default TX frequency
+            self.store.set(iter, self.col("Offset"), freq)
+        else:
+            band = int(freq / 100000000)
+            if chirp_common.STD_OFFSETS.has_key(band):
+                offset = chirp_common.STD_OFFSETS[band][0][2]
+            else:
+                offset = 0
+            self.store.set(iter, self.col(_("Offset")), abs(offset))
+
+        return new
+
+    def _get_cols_to_hide(self, iter):
+        tmode, duplex = self.store.get(iter,
+                                       self.col(_("Tone Mode")),
+                                       self.col(_("Duplex")))
+
+        hide = []
+
+        if tmode == "Tone":
+            hide += [self.col(_("ToneSql")),
+                     self.col(_("DTCS Code")),
+                     self.col(_("DTCS Rx Code")),
+                     self.col(_("DTCS Pol"))]
+        elif tmode == "TSQL":
+            if self._features.has_ctone:
+                hide += [self.col(_("Tone"))]
+
+            hide += [self.col(_("DTCS Code")),
+                     self.col(_("DTCS Rx Code")),
+                     self.col(_("DTCS Pol"))]
+        elif tmode == "DTCS":
+            if self._features.has_rx_dtcs:
+                hide += [self.col(_("DTCS Code"))]
+
+            hide += [self.col(_("Tone")),
+                     self.col(_("ToneSql"))]
+        elif tmode == "" or tmode == "(None)":
+            hide += [self.col(_("Tone")),
+                     self.col(_("ToneSql")),
+                     self.col(_("DTCS Code")),
+                     self.col(_("DTCS Rx Code")),
+                     self.col(_("DTCS Pol"))]
+
+        if duplex == "" or duplex == "(None)":
+            hide += [self.col(_("Offset"))]
+
+        return hide
+
+    def maybe_hide_cols(self, iter):
+        hide_cols = self._get_cols_to_hide(iter)
+        self.store.set(iter, self.col("_hide_cols"), hide_cols)
+
+    def edited(self, rend, path, new, cap):
+        if self.read_only:
+            common.show_error(_("Unable to make changes to this model"))
+            return
+
+        iter = self.store.get_iter(path)
+        if not self.store.get(iter, self.col("_filled"))[0] \
+        and self.store.get(iter, self.col(_("Frequency")))[0] == 0:
+            print _("Editing new item, taking defaults")
+            self.insert_new(iter)
+
+        colnum = self.col(cap)
+        funcs = {
+            _("Loc") : self.ed_loc,
+            _("Name") : self.ed_name,
+            _("Frequency") : self.ed_freq,
+            _("Duplex") : self.ed_duplex,
+            _("Offset") : self.ed_offset,
+            }
+
+        if funcs.has_key(cap):
+            new = funcs[cap](rend, path, new, colnum)
+
+        if new is None:
+            print _("Bad value for {col}: {val}").format(col=cap, val=new)
+            return
+
+        if self.store.get_column_type(colnum) == TYPE_INT:
+            new = int(new)
+        elif self.store.get_column_type(colnum) == TYPE_FLOAT:
+            new = float(new)
+        elif self.store.get_column_type(colnum) == TYPE_BOOLEAN:
+            new = bool(new)
+        elif self.store.get_column_type(colnum) == TYPE_STRING:
+            if new == "(None)":
+                new = ""
+
+        if not handle_ed(rend, iter, new, self.store, self.col(cap)) and \
+                cap != _("Frequency"):
+            # No change was made
+            # For frequency, we make an exception, since the handler might
+            # have altered the duplex.  That needs to be fixed.
+            return
+
+        mem = self._get_memory(iter)
+
+        msgs = self.rthread.radio.validate_memory(mem)
+        if msgs:
+            common.show_error(_("Error setting memory") + \
+                                  "\r\n\r\n".join(msgs))
+            self.prefill()
+            return
+
+        mem.empty = False
+
+        job = common.RadioJob(self._set_memory_cb, "set_memory", mem)
+        job.set_desc(_("Writing memory {number}").format(number=mem.number))
+        self.rthread.submit(job)
+
+        self.store.set(iter, self.col("_filled"), True)
+
+        self.maybe_hide_cols(iter)
+
+        persist_defaults = [_("Power"), _("Frequency"), _("Mode")]
+        if cap in persist_defaults:
+            self.defaults[cap] = new
+
+    def _render(self, colnum, val, iter=None, hide=[]):
+        if colnum in hide and self.hide_unused:
+            return ""
+
+        if colnum == self.col(_("Frequency")):
+            val = chirp_common.format_freq(val)
+        elif colnum in [self.col(_("DTCS Code")), self.col(_("DTCS Rx Code"))]:
+            val = "%03i" % int(val)
+        elif colnum == self.col(_("Offset")):
+            val = chirp_common.format_freq(val)
+        elif colnum in [self.col(_("Tone")), self.col(_("ToneSql"))]:
+            val = "%.1f" % val
+        elif colnum in [self.col(_("Tone Mode")), self.col(_("Duplex"))]:
+            if not val:
+                val = "(None)"
+        elif colnum == self.col(_("Loc")) and iter is not None:
+            extd, = self.store.get(iter, self.col("_extd"))
+            if extd:
+                val = extd
+
+
+        return val
+
+    def render(self, _, rend, model, iter, colnum):
+        val, hide = model.get(iter, colnum, self.col("_hide_cols"))
+        val = self._render(colnum, val, iter, hide or [])
+        rend.set_property("text", "%s" % val)
+
+    def insert_new(self, iter, loc=None):
+        line = []
+        for key, val in self.defaults.items():
+            line.append(self.col(key))
+            line.append(val)
+        
+        if not loc:
+            loc, = self.store.get(iter, self.col(_("Loc")))
+
+        self.store.set(iter,
+                       0, loc,
+                       *tuple(line))
+        
+        return self._get_memory(iter)
+
+    def insert_easy(self, store, _iter, delta):
+        if delta < 0:
+            iter = store.insert_before(_iter)
+        else:
+            iter = store.insert_after(_iter)
+
+        newpos, = store.get(_iter, self.col(_("Loc")))
+        newpos += delta
+
+        print "Insert easy: %i" % delta
+
+        mem = self.insert_new(iter, newpos)
+        job = common.RadioJob(None, "set_memory", mem)
+        job.set_desc(_("Writing memory {number}").format(number=mem.number))
+        self.rthread.submit(job)
+
+    def insert_hard(self, store, _iter, delta, warn=True):
+	if isinstance(self.rthread.radio, chirp_common.LiveRadio) and warn:
+            txt = _("This operation requires moving all subsequent channels "
+                    "by one spot until an empty location is reached.  This "
+                    "can take a LONG time.  Are you sure you want to do this?")
+            if not common.ask_yesno_question(txt):
+                return False # No change
+
+        if delta <= 0:
+            iter = _iter
+        else:
+            iter = store.iter_next(_iter)
+
+        pos, = store.get(iter, self.col("Loc"))
+
+        sd = shiftdialog.ShiftDialog(self.rthread)
+
+        if delta == 0:
+            sd.delete(pos)
+            sd.destroy()
+            self.prefill()
+        else:
+            sd.insert(pos)
+            sd.destroy()
+            job = common.RadioJob(lambda x: self.prefill(), "erase_memory", pos)
+            job.set_desc(_("Adding memory {number}").format(number=pos))
+            self.rthread.submit(job)
+
+        return True # We changed memories
+
+    def _delete_rows(self, paths):
+        to_remove = []
+        for path in paths:
+            iter = self.store.get_iter(path)
+            cur_pos, = self.store.get(iter, self.col("Loc"))
+            to_remove.append(cur_pos)
+            self.store.set(iter, self.col("_filled"), False)
+            job = common.RadioJob(None, "erase_memory", cur_pos)
+            job.set_desc(_("Erasing memory {number}").format(number=cur_pos))
+            self.rthread.submit(job)
+            
+            def handler(mem):
+                if not isinstance(mem, Exception):
+                    if not mem.empty or self.show_empty:
+                        gobject.idle_add(self.set_memory, mem)
+            
+            job = common.RadioJob(handler, "get_memory", cur_pos)
+            job.set_desc(_("Getting memory {number}").format(number=cur_pos))
+            self.rthread.submit(job)
+            
+
+        if not self.show_empty:
+            # We need to actually remove the rows from the store
+            # now, but carefully! Get a list of deleted locations
+            # in order and proceed from the first path in the list
+            # until we run out of rows or we've removed all the
+            # desired ones.
+            to_remove.sort()
+            to_remove.reverse()
+            iter = self.store.get_iter(paths[0])
+            while to_remove and iter:
+                pos, = self.store.get(iter, self.col(_("Loc")))
+                if pos in to_remove:
+                    to_remove.remove(pos)
+                    if not self.store.remove(iter):
+                        break # This was the last row
+                else:
+                    iter = self.store.iter_next(iter)
+
+        return True # We changed memories
+
+    def _delete_rows_and_shift(self, paths):
+        iter = self.store.get_iter(paths[0])
+        starting_loc, = self.store.get(iter, self.col(_("Loc")))
+        for i in range(0, len(paths)):
+            sd = shiftdialog.ShiftDialog(self.rthread)
+            sd.delete(starting_loc, quiet=True)
+            sd.destroy()
+
+        self.prefill()
+        return True # We changed memories
+
+    def _move_up_down(self, paths, action):
+        if action.endswith("up"):
+            delta = -1
+            donor_path = paths[-1]
+            victim_path = paths[0]
+        else:
+            delta = 1
+            donor_path = paths[0]
+            victim_path = paths[-1]
+
+        try:
+            victim_path = (victim_path[0] + delta,)
+            if victim_path[0] < 0:
+                raise ValueError()
+            donor_loc = self.store.get(self.store.get_iter(donor_path),
+                                  self.col(_("Loc")))[0]
+            victim_loc = self.store.get(self.store.get_iter(victim_path),
+                                   self.col(_("Loc")))[0]
+        except ValueError:
+            self.emit("usermsg", "No room to %s" % (action.replace("_", " ")))
+            return False # No change
+
+        class Context:
+            pass
+        ctx = Context()
+
+        ctx.victim_mem = None
+        ctx.donor_loc = donor_loc
+        ctx.done_count = 0
+        ctx.path_count = len(paths)
+
+        # Steps:
+        # 1. Grab the victim (the one that will need to be saved and moved
+        #    from the front to the back or back to the front) and save it
+        # 2. Grab each memory along the way, storing it in the +delta
+        #    destination location after we get it
+        # 3. If we're the final move, then schedule storing the victim
+        #    in the hole we created
+
+        def update_selection():
+            sel = self.view.get_selection()
+            sel.unselect_all()
+            for path in paths:
+                gobject.idle_add(sel.select_path, (path[0]+delta,))
+
+        def save_victim(mem, ctx):
+            ctx.victim_mem = mem
+
+        def store_victim(mem, dest):
+            old = mem.number
+            mem.number = dest
+            job = common.RadioJob(None, "set_memory", mem)
+            job.set_desc(\
+                _("Moving memory from {old} to {new}").format(old=old,
+                                                              new=dest))
+            self.rthread.submit(job)
+            self._set_memory(self.store.get_iter(donor_path), mem)
+            update_selection()
+
+        def move_mem(mem, delta, ctx, iter):
+            old = mem.number
+            mem.number += delta
+            job = common.RadioJob(None, "set_memory", mem)
+            job.set_desc(\
+                _("Moving memory from {old} to {new}").format(old=old,
+                                                              new=old+delta))
+            self.rthread.submit(job)
+            self._set_memory(iter, mem)
+            ctx.done_count += 1
+            if ctx.done_count == ctx.path_count:
+                store_victim(ctx.victim_mem, ctx.donor_loc)
+
+        job = common.RadioJob(lambda m: save_victim(m, ctx),
+                              "get_memory", victim_loc)
+        job.set_desc(_("Getting memory {number}").format(number=victim_loc))
+        self.rthread.submit(job)
+
+        for i in range(len(paths)):
+            path = paths[i]
+            if delta > 0:
+                dest = i+1
+            else:
+                dest = i-1
+
+            if dest < 0 or dest >= len(paths):
+                dest = victim_path
+            else:
+                dest = paths[dest]
+
+            iter = self.store.get_iter(path)
+            loc, = self.store.get(iter, self.col(_("Loc")))
+            job = common.RadioJob(move_mem, "get_memory", loc)
+            job.set_cb_args(delta, ctx, self.store.get_iter(dest))
+            job.set_desc("Getting memory %i" % loc)
+            self.rthread.submit(job)
+
+        return True # We (scheduled some) change to the memories
+
+    def _exchange_memories(self, paths):
+        if len(paths) != 2:
+            self.emit("usermsg", "Select two memories first")
+            return False
+
+        loc_a, = self.store.get(self.store.get_iter(paths[0]),
+                                self.col(_("Loc")))
+        loc_b, = self.store.get(self.store.get_iter(paths[1]),
+                                self.col(_("Loc")))
+
+        def store_mem(mem, dst):
+            src = mem.number
+            mem.number = dst
+            job = common.RadioJob(None, "set_memory", mem)
+            job.set_desc(_("Moving memory from {old} to {new}").format(old=src,
+                                                                       new=dst))
+            self.rthread.submit(job)
+            if dst == loc_a:
+                self.prefill()
+
+        job = common.RadioJob(lambda m: store_mem(m, loc_b),
+                              "get_memory", loc_a)
+        job.set_desc(_("Getting memory {number}").format(number=loc_a))
+        self.rthread.submit(job)
+
+        job = common.RadioJob(lambda m: store_mem(m, loc_a),
+                              "get_memory", loc_b)
+        job.set_desc(_("Getting memory {number}").format(number=loc_b))
+        self.rthread.submit(job)
+
+        # We (scheduled some) change to the memories
+        return True
+
+    def _show_raw(self, cur_pos):
+        def idle_show_raw(result):
+            gobject.idle_add(common.show_diff_blob,
+                             _("Raw memory {number}").format(number=cur_pos),
+                                                             result)
+
+        job = common.RadioJob(idle_show_raw, "get_raw_memory", cur_pos)
+        job.set_desc(_("Getting raw memory {number}").format(number=cur_pos))
+        self.rthread.submit(job)
+
+    def _diff_raw(self, paths):
+        if len(paths) != 2:
+            common.show_error(_("You can only diff two memories!"))
+            return
+
+        loc_a = self.store.get(self.store.get_iter(paths[0]),
+                               self.col(_("Loc")))[0]
+        loc_b = self.store.get(self.store.get_iter(paths[1]),
+                               self.col(_("Loc")))[0]
+
+        raw = {}
+
+        def diff_raw(which, result):
+            raw[which] = _("Memory {number}").format(number=which) + \
+                os.linesep + result
+
+            if len(raw.keys()) == 2:
+                diff = common.simple_diff(raw[loc_a], raw[loc_b])
+                gobject.idle_add(common.show_diff_blob,
+                                 _("Diff of {a} and {b}").format(a=loc_a,
+                                                                 b=loc_b),
+                                 diff)
+
+        job = common.RadioJob(lambda r: diff_raw(loc_a, r),
+                              "get_raw_memory", loc_a)
+        job.set_desc(_("Getting raw memory {number}").format(number=loc_a))
+        self.rthread.submit(job)
+
+        job = common.RadioJob(lambda r: diff_raw(loc_b, r),
+                              "get_raw_memory", loc_b)
+        job.set_desc(_("Getting raw memory {number}").format(number=loc_b))
+        self.rthread.submit(job)
+
+    def edit_memory(self, memory):
+        dlg = memdetail.MemoryDetailEditor(self._features, memory)
+        r = dlg.run()
+        if r == gtk.RESPONSE_OK:
+            self.need_refresh = True
+            mem = dlg.get_memory()
+            mem.name = self.rthread.radio.filter_name(mem.name)
+            job = common.RadioJob(self._set_memory_cb, "set_memory", mem)
+            job.set_desc(_("Writing memory {number}").format(number=mem.number))
+            self.rthread.submit(job)
+            self.emit("changed")
+        dlg.destroy()
+
+    def mh(self, _action, store, paths):
+        action = _action.get_name()
+        iter = store.get_iter(paths[0])
+        cur_pos, = store.get(iter, self.col(_("Loc")))
+
+        require_contiguous = ["delete_s", "move_up", "move_dn"]
+        if action in require_contiguous:
+            last = paths[0][0]
+            for path in paths[1:]:
+                if path[0] != last+1:
+                    self.emit("usermsg", _("Memories must be contiguous"))
+                    return
+                last = path[0]
+
+        changed = False
+
+        if action == "insert_next":
+            changed = self.insert_hard(store, iter, 1)
+        elif action == "insert_prev":
+            changed = self.insert_hard(store, iter, -1)
+        elif action == "delete":
+            changed = self._delete_rows(paths)
+        elif action == "delete_s":
+            changed = self._delete_rows_and_shift(paths)
+        elif action in ["move_up", "move_dn"]:
+            changed = self._move_up_down(paths, action)
+        elif action == "exchange":
+            changed = self._exchange_memories(paths)
+        elif action in ["cut", "copy"]:
+            changed = self.copy_selection(action=="cut")
+        elif action == "paste":
+            changed = self.paste_selection()
+        elif action == "devshowraw":
+            self._show_raw(cur_pos)
+        elif action == "devdiffraw":
+            self._diff_raw(paths)
+        elif action == "edit":
+            job = common.RadioJob(self.edit_memory, "get_memory", cur_pos)
+            self.rthread.submit(job)
+
+        if changed:
+            self.emit("changed")
+
+    def hotkey(self, action):
+        if self._in_editing:
+            # Don't forward potentially-dangerous hotkeys to the menu
+            # handler if we're editing a cell right now
+            return
+
+        self.emit("usermsg", "")
+        (store, paths) = self.view.get_selection().get_selected_rows()
+        if len(paths) == 0:
+            return
+        self.mh(action, store, paths)
+
+    def make_context_menu(self):
+        if self._config.get_bool("developer", "state"):
+            devmenu = """
+<separator/>
+<menuitem action="devshowraw"/>
+<menuitem action="devdiffraw"/>
+"""
+        else:
+            devmenu = ""
+
+        menu_xml = """
+<ui>
+  <popup name="Menu"> 
+    <menuitem action="edit"/>
+    <menuitem action="insert_prev"/>
+    <menuitem action="insert_next"/>
+    <menuitem action="delete"/>
+    <menuitem action="delete_s"/>
+    <menuitem action="move_up"/>
+    <menuitem action="move_dn"/>
+    <menuitem action="exchange"/>
+    <separator/>
+    <menuitem action="cut"/>
+    <menuitem action="copy"/>
+    <menuitem action="paste"/>
+    %s
+  </popup>
+</ui>
+""" % devmenu
+
+
+        (store, paths) = self.view.get_selection().get_selected_rows()
+        issingle = len(paths) == 1
+        istwo = len(paths) == 2
+
+        actions = [
+            ("edit", _("Edit")),
+            ("insert_prev", _("Insert row above")),
+            ("insert_next", _("Insert row below")),
+            ("delete", issingle and _("Delete") or _("Delete all")),
+            ("delete_s", _("Delete (and shift up)")),
+            ("move_up", _("Move up")),
+            ("move_dn", _("Move down")),
+            ("exchange", _("Exchange memories")),
+            ("cut", _("Cut")),
+            ("copy", _("Copy")),
+            ("paste", _("Paste")),
+            ("devshowraw", _("Show Raw Memory")),
+            ("devdiffraw", _("Diff Raw Memories")),
+            ]
+
+        no_multiple = ["insert_prev", "insert_next", "paste", "devshowraw"]
+        only_two = ["devdiffraw", "exchange"]
+
+        ag = gtk.ActionGroup("Menu")
+
+        for name, label in actions:
+            a = gtk.Action(name, label, "", 0)
+            a.connect("activate", self.mh, store, paths)
+            if name in no_multiple:
+                a.set_sensitive(issingle)
+            if name in only_two:
+                a.set_sensitive(istwo)
+            ag.add_action(a)
+
+        uim = gtk.UIManager()
+        uim.insert_action_group(ag, 0)
+        uim.add_ui_from_string(menu_xml)
+
+        return uim.get_widget("/Menu")
+
+    def click_cb(self, view, event):
+        self.emit("usermsg", "")
+        if event.button != 3:
+            return False
+
+        menu = self.make_context_menu()
+        menu.popup(None, None, None, event.button, event.time)
+
+        return True
+        
+    def get_column_visible(self, col):
+        column = self.view.get_column(col)
+        return column.get_visible()
+
+    def set_column_visible(self, col, visible):
+        column = self.view.get_column(col)
+        column.set_visible(visible)
+    
+    def cell_editing_started(self, rend, event, path):
+        self._in_editing = True
+
+    def cell_editing_stopped(self, *args):
+        self._in_editing = False
+
+    def make_editor(self):
+        types = tuple([x[1] for x in self.cols])
+        self.store = gtk.ListStore(*types)
+
+        self.view = gtk.TreeView(self.store)
+        self.view.get_selection().set_mode(gtk.SELECTION_MULTIPLE)
+        self.view.set_rules_hint(True)
+
+        sw = gtk.ScrolledWindow()
+        sw.set_policy(gtk.POLICY_AUTOMATIC, gtk.POLICY_AUTOMATIC)
+        sw.add(self.view)
+
+        filled = self.col("_filled")
+
+        default_col_order = [x for x,y,z in self.cols if z]
+        try:
+            col_order = self._config.get("column_order_%s" % \
+                                             self.__class__.__name__).split(",")
+            if len(col_order) != len(default_col_order):
+                raise Exception()
+            for i in col_order:
+                if i not in default_col_order:
+                    raise Exception()
+        except Exception, e:
+            print e
+            col_order = default_col_order
+
+        non_editable = ["Loc"]
+
+        unsupported_cols = self.get_unsupported_columns()
+        visible_cols = self.get_columns_visible()
+
+        cols = {}
+        i = 0
+        for _cap, _type, _rend in self.cols:
+            if not _rend:
+                continue
+            rend = _rend()
+            rend.connect('editing-started', self.cell_editing_started)
+            rend.connect('editing-canceled', self.cell_editing_stopped)
+            rend.connect('edited', self.cell_editing_stopped)
+
+            if _type == TYPE_BOOLEAN:
+                #rend.set_property("activatable", True)
+                #rend.connect("toggled", handle_toggle, self.store, i)
+                col = gtk.TreeViewColumn(_cap, rend, active=i, sensitive=filled)
+            elif _rend == gtk.CellRendererCombo:
+                if isinstance(self.choices[_cap], gtk.ListStore):
+                    choices = self.choices[_cap]
+                else:
+                    choices = gtk.ListStore(TYPE_STRING, TYPE_STRING)
+                    for choice in self.choices[_cap]:
+                        choices.append([choice, self._render(i, choice)])
+                rend.set_property("model", choices)
+                rend.set_property("text-column", 1)
+                rend.set_property("editable", True)
+                rend.set_property("has-entry", False)
+                rend.connect("edited", self.edited, _cap)
+                col = gtk.TreeViewColumn(_cap, rend, text=i, sensitive=filled)
+                col.set_cell_data_func(rend, self.render, i)
+            else:
+                rend.set_property("editable", _cap not in non_editable)
+                rend.connect("edited", self.edited, _cap)
+                col = gtk.TreeViewColumn(_cap, rend, text=i, sensitive=filled)
+                col.set_cell_data_func(rend, self.render, i)
+                
+            col.set_reorderable(True)
+            col.set_sort_column_id(i)
+            col.set_resizable(True)
+            col.set_min_width(1)
+            col.set_visible(not _cap.startswith("_") and
+                            _cap in visible_cols and
+                            not _cap in unsupported_cols)
+            cols[_cap] = col
+            i += 1
+
+        for cap in col_order:
+            self.view.append_column(cols[cap])
+
+        self.store.set_sort_column_id(self.col(_("Loc")), gtk.SORT_ASCENDING)
+
+        self.view.show()
+        sw.show()
+
+        self.view.connect("button_press_event", self.click_cb)
+
+        return sw
+
+    def col(self, caption):
+        try:
+            return self._cached_cols[caption]
+        except KeyError:
+            raise Exception(_("Internal Error: Column {name} not found").format(name=caption))
+
+    def prefill(self):
+        self.store.clear()
+        self._rows_in_store = 0
+
+        lo = int(self.lo_limit_adj.get_value())
+        hi = int(self.hi_limit_adj.get_value())
+
+        def handler(mem, number):
+            if not isinstance(mem, Exception):
+                if not mem.empty or self.show_empty:
+                    gobject.idle_add(self.set_memory, mem)
+            else:
+                mem = chirp_common.Memory()
+                mem.number = number
+                mem.name = "ERROR"
+                mem.empty = True
+                gobject.idle_add(self.set_memory, mem)
+
+        for i in range(lo, hi+1):
+            job = common.RadioJob(handler, "get_memory", i)
+            job.set_desc(_("Getting memory {number}").format(number=i))
+            job.set_cb_args(i)
+            self.rthread.submit(job, 2)
+
+        if self.show_special:
+            for i in self._features.valid_special_chans:
+                job = common.RadioJob(handler, "get_memory", i)
+                job.set_desc(_("Getting channel {chan}").format(chan=i))
+                job.set_cb_args(i)
+                self.rthread.submit(job, 2)
+
+    def _set_memory(self, iter, memory):
+        self.store.set(iter,
+                       self.col("_filled"), not memory.empty,
+                       self.col(_("Loc")), memory.number,
+                       self.col("_extd"), memory.extd_number,
+                       self.col(_("Name")), memory.name,
+                       self.col(_("Frequency")), memory.freq,
+                       self.col(_("Tone Mode")), memory.tmode,
+                       self.col(_("Tone")), memory.rtone,
+                       self.col(_("ToneSql")), memory.ctone,
+                       self.col(_("DTCS Code")), memory.dtcs,
+                       self.col(_("DTCS Rx Code")), memory.rx_dtcs,
+                       self.col(_("DTCS Pol")), memory.dtcs_polarity,
+                       self.col(_("Cross Mode")), memory.cross_mode,
+                       self.col(_("Duplex")), memory.duplex,
+                       self.col(_("Offset")), memory.offset,
+                       self.col(_("Mode")), memory.mode,
+                       self.col(_("Power")), memory.power or "",
+                       self.col(_("Tune Step")), memory.tuning_step,
+                       self.col(_("Skip")), memory.skip,
+                       self.col(_("Comment")), memory.comment)
+
+        hide = self._get_cols_to_hide(iter)
+        self.store.set(iter, self.col("_hide_cols"), hide)
+
+    def set_memory(self, memory):
+        iter = self.store.get_iter_first()
+
+        while iter is not None:
+            loc, = self.store.get(iter, self.col(_("Loc")))
+            if loc == memory.number:
+                return self._set_memory(iter, memory)
+
+            iter = self.store.iter_next(iter)
+
+        iter = self.store.append()
+        self._rows_in_store += 1
+        self._set_memory(iter, memory)
+
+    def clear_memory(self, number):
+        iter = self.store.get_iter_first()
+        while iter:
+            loc, = self.store.get(iter, self.col(_("Loc")))
+            if loc == number:
+                print "Deleting %i" % number
+                # FIXME: Make the actual remove happen on callback
+                self.store.remove(iter)
+                job = common.RadioJob(None, "erase_memory", number)
+                job.set_desc(_("Erasing memory {number}").format(number=number))
+                self.rthread.submit()
+                break
+            iter = self.store.iter_next(iter)
+
+    def _set_mem_vals(self, mem, vals, iter):
+        power_levels = {"" : None}
+        for i in self._features.valid_power_levels:
+            power_levels[str(i)] = i
+
+        mem.freq = vals[self.col(_("Frequency"))]
+        mem.number = vals[self.col(_("Loc"))]
+        mem.extd_number = vals[self.col("_extd")]
+        mem.name = vals[self.col(_("Name"))]
+        mem.vfo = 0
+        mem.rtone = vals[self.col(_("Tone"))]
+        mem.ctone = vals[self.col(_("ToneSql"))]
+        mem.dtcs = vals[self.col(_("DTCS Code"))]
+        mem.rx_dtcs = vals[self.col(_("DTCS Rx Code"))]
+        mem.tmode = vals[self.col(_("Tone Mode"))]
+        mem.cross_mode = vals[self.col(_("Cross Mode"))]
+        mem.dtcs_polarity = vals[self.col(_("DTCS Pol"))]
+        mem.duplex = vals[self.col(_("Duplex"))]
+        mem.offset = vals[self.col(_("Offset"))]
+        mem.mode = vals[self.col(_("Mode"))]
+        mem.power = power_levels[vals[self.col(_("Power"))]]
+        mem.tuning_step = vals[self.col(_("Tune Step"))]
+        mem.skip = vals[self.col(_("Skip"))]
+        mem.comment = vals[self.col(_("Comment"))]
+        mem.empty = not vals[self.col("_filled")]
+
+    def _get_memory(self, iter):
+        vals = self.store.get(iter, *range(0, len(self.cols)))
+        mem = chirp_common.Memory()
+        self._set_mem_vals(mem, vals, iter)
+
+        return mem
+
+    def _limit_key(self, which):
+        if which not in ["lo", "hi"]:
+            raise Exception(_("Internal Error: Invalid limit {number").format(number=which))
+        return "%s_%s" % (directory.radio_class_id(self.rthread.radio.__class__),
+                          which)
+
+    def _store_limit(self, sb, which):
+        self._config.set_int(self._limit_key(which), int(sb.get_value()))
+
+    def make_controls(self, min, max):
+        hbox = gtk.HBox(False, 2)
+
+        lab = gtk.Label(_("Memory range:"))
+        lab.show()
+        hbox.pack_start(lab, 0, 0, 0)
+
+        lokey = self._limit_key("lo")
+        hikey = self._limit_key("hi")
+        lostart = self._config.is_defined(lokey) and \
+            self._config.get_int(lokey) or min
+        histart = self._config.is_defined(hikey) and \
+            self._config.get_int(hikey) or 25
+
+        self.lo_limit_adj = gtk.Adjustment(lostart, min, max-1, 1, 10)
+        lo = gtk.SpinButton(self.lo_limit_adj)
+        lo.connect("value-changed", self._store_limit, "lo")
+        lo.show()
+        hbox.pack_start(lo, 0, 0, 0)
+
+        lab = gtk.Label(" - ")
+        lab.show()
+        hbox.pack_start(lab, 0, 0, 0)
+
+        self.hi_limit_adj = gtk.Adjustment(histart, min+1, max, 1, 10)
+        hi = gtk.SpinButton(self.hi_limit_adj)
+        hi.connect("value-changed", self._store_limit, "hi")
+        hi.show()
+        hbox.pack_start(hi, 0, 0, 0)
+
+        refresh = gtk.Button(_("Go"))
+        refresh.show()
+        refresh.connect("clicked", lambda x: self.prefill())
+        hbox.pack_start(refresh, 0, 0, 0)
+
+        def activate_go(widget):
+            refresh.clicked()
+
+        def set_hi(widget, event):
+            loval = self.lo_limit_adj.get_value()
+            hival = self.hi_limit_adj.get_value()
+            if loval >= hival:
+                self.hi_limit_adj.set_value(loval + 25)
+        
+        lo.connect_after("focus-out-event", set_hi)
+        lo.connect_after("activate", activate_go)
+        hi.connect_after("activate", activate_go)
+
+        sep = gtk.VSeparator()
+        sep.show()
+        sep.set_size_request(20, -1)
+        hbox.pack_start(sep, 0, 0, 0)
+
+        showspecial = gtk.CheckButton(_("Special Channels"))
+        showspecial.set_active(self.show_special)
+        showspecial.connect("toggled",
+                            lambda x: self.set_show_special(x.get_active()))
+        showspecial.show()
+        hbox.pack_start(showspecial, 0, 0, 0)
+
+        showempty = gtk.CheckButton(_("Show Empty"))
+        showempty.set_active(self.show_empty);
+        showempty.connect("toggled",
+                          lambda x: self.set_show_empty(x.get_active()))
+        showempty.show()
+        hbox.pack_start(showempty, 0, 0, 0)
+
+        hbox.show()
+
+        return hbox
+
+    def set_show_special(self, show):
+        self.show_special = show
+        self.prefill()
+        self._config.set_bool("show_special", show)
+
+    def set_show_empty(self, show):
+        self.show_empty = show
+        self.prefill()
+        self._config.set_bool("hide_empty", not show)
+
+    def set_read_only(self, read_only):
+        self.read_only = read_only
+
+    def get_read_only(self):
+        return self.read_only
+
+    def set_hide_unused(self, hide_unused):
+        self.hide_unused = hide_unused
+        self.prefill()
+        self._config.set_bool("hide_unused", hide_unused)
+
+    def __cache_columns(self):
+        # We call self.col() a lot.  Caching the name->column# lookup
+        # makes a significant performance improvement
+        self._cached_cols = {}
+        i = 0
+        for x in self.cols:
+            self._cached_cols[x[0]] = i
+            i += 1
+
+    def get_unsupported_columns(self):
+        maybe_hide = [
+            ("has_dtcs", _("DTCS Code")),
+            ("has_rx_dtcs", _("DTCS Rx Code")),
+            ("has_dtcs_polarity", _("DTCS Pol")),
+            ("has_mode", _("Mode")),
+            ("has_offset", _("Offset")),
+            ("has_name", _("Name")),
+            ("has_tuning_step", _("Tune Step")),
+            ("has_name", _("Name")),
+            ("has_ctone", _("ToneSql")),
+            ("has_cross", _("Cross Mode")),
+            ("has_comment", _("Comment")),
+            ("valid_tmodes", _("Tone Mode")),
+            ("valid_tmodes", _("Tone")),
+            ("valid_duplexes", _("Duplex")),
+            ("valid_skips", _("Skip")),
+            ("valid_power_levels", _("Power")),
+            ]
+
+        unsupported = []
+        for feature, colname in maybe_hide:
+            if feature.startswith("has_"):
+                supported = self._features[feature]
+                print "%s supported: %s" % (colname, supported)
+            elif feature.startswith("valid_"):
+                supported = len(self._features[feature]) != 0
+
+            if not supported:
+                unsupported.append(colname)
+
+        return unsupported
+
+    def get_columns_visible(self):
+        unsupported = self.get_unsupported_columns()
+        driver = directory.radio_class_id(self.rthread.radio.__class__)
+        user_visible = self._config.get(driver, "memedit_columns")
+        if user_visible:
+            user_visible = user_visible.split(",")
+        else:
+            # No setting for this radio, so assume all
+            user_visible = [x[0] for x in self.cols if x not in unsupported]
+        return user_visible
+
+    def __init__(self, rthread):
+        common.Editor.__init__(self)
+        self.rthread = rthread
+
+        self.defaults = dict(self.defaults)
+
+        self._config = config.get("memedit")
+
+        self.allowed_bands = [144, 440]
+        self.count = 100
+        self.show_special = self._config.get_bool("show_special")
+        self.show_empty = not self._config.get_bool("hide_empty")
+        self.hide_unused = self._config.get_bool("hide_unused")
+        self.read_only = False
+
+        self.need_refresh = False
+        self._in_editing = False
+
+        self.lo_limit_adj = self.hi_limit_adj = None
+        self.store = self.view = None
+
+        self.__cache_columns()
+
+        self._features = self.rthread.radio.get_features()
+
+        (min, max) = self._features.memory_bounds
+
+        self.choices[_("Mode")] = self._features["valid_modes"]
+        self.choices[_("Tone Mode")] = self._features["valid_tmodes"]
+        self.choices[_("Cross Mode")] = self._features["valid_cross_modes"]
+        self.choices[_("Skip")] = self._features["valid_skips"]
+        self.choices[_("Power")] = [str(x) for x in
+                                    self._features["valid_power_levels"]]
+        self.choices[_("DTCS Pol")] = self._features["valid_dtcs_pols"]
+
+        if self._features["valid_power_levels"]:
+            self.defaults[_("Power")] = self._features["valid_power_levels"][0]
+
+        self.choices[_("Duplex")] = list(self._features.valid_duplexes)
+
+        if self.defaults[_("Mode")] not in self._features.valid_modes:
+            self.defaults[_("Mode")] = self._features.valid_modes[0]
+
+        vbox = gtk.VBox(False, 2)
+        vbox.pack_start(self.make_controls(min, max), 0, 0, 0)
+        vbox.pack_start(self.make_editor(), 1, 1, 1)
+        vbox.show()
+
+        self.prefill()
+        
+        self.choices["Mode"] = self._features.valid_modes
+
+        self.root = vbox
+
+        self.prefill()
+
+        # Run low priority jobs to get the rest of the memories
+        hi = int(self.hi_limit_adj.get_value())
+        for i in range(hi, max+1):
+            job = common.RadioJob(None, "get_memory", i)
+            job.set_desc(_("Getting memory {number}").format(number=i))
+            self.rthread.submit(job, 10)
+
+    def _set_memory_cb(self, result):
+        if isinstance(result, Exception):
+            # FIXME: This can't be in the thread
+            dlg = ValueErrorDialog(result)
+            dlg.run()
+            dlg.destroy()
+            self.prefill()
+        elif self.need_refresh:
+            self.prefill()
+            self.need_refresh = False
+
+        self.emit('changed')
+
+    def copy_selection(self, cut=False):
+        (store, paths) = self.view.get_selection().get_selected_rows()
+
+        maybe_cut = []
+        selection = []
+
+        for path in paths:
+            iter = store.get_iter(path)
+            mem = self._get_memory(iter)
+            selection.append(mem.dupe())
+            maybe_cut.append((iter, mem))
+        
+        if cut:
+            for iter, mem in maybe_cut:
+                mem.empty = True
+                job = common.RadioJob(self._set_memory_cb,
+                                      "erase_memory", mem.number)
+                job.set_desc(_("Cutting memory {number}").format(number=mem.number))
+                self.rthread.submit(job)
+
+                self._set_memory(iter, mem)
+
+        result = pickle.dumps((self._features, selection))
+        clipboard = gtk.Clipboard(selection="PRIMARY")
+        clipboard.set_text(result)
+
+        return cut # Only changed if we did a cut
+
+    def _paste_selection(self, clipboard, text, data):
+        if not text:
+            return
+
+        (store, paths) = self.view.get_selection().get_selected_rows()
+        if len(paths) > 1:
+            common.show_error("To paste, select only the starting location")
+            return
+
+        iter = store.get_iter(paths[0])
+
+        always = False
+
+        try:
+            src_features, mem_list = pickle.loads(text)
+        except Exception:
+            print "Paste failed to unpickle"
+            return
+
+        if (paths[0][0] + len(mem_list)) > self._rows_in_store:
+            common.show_error(_("Unable to paste {src} memories into "
+                                "{dst} rows. Increase the memory bounds "
+                                "or show empty memories.").format(\
+                    src=len(mem_list),
+                    dst=(self._rows_in_store - paths[0][0])))
+            return
+
+        for mem in mem_list:
+            loc, filled = store.get(iter,
+                                    self.col(_("Loc")), self.col("_filled"))
+            if filled and not always:
+                d = miscwidgets.YesNoDialog(title=_("Overwrite?"),
+                                            buttons=(gtk.STOCK_YES, 1,
+                                                     gtk.STOCK_NO, 2,
+                                                     gtk.STOCK_CANCEL, 3,
+                                                     "All", 4))
+                d.set_text(_("Overwrite location {number}?").format(number=loc))
+                r = d.run()
+                d.destroy()
+                if r == 4:
+                    always = True
+                elif r == 3:
+                    break
+                elif r == 2:
+                    iter = store.iter_next(iter)
+                    continue
+
+            mem.name = self.rthread.radio.filter_name(mem.name)
+
+            src_number = mem.number
+            mem.number = loc
+            
+            try:
+                mem = import_logic.import_mem(self.rthread.radio,
+                                              src_features,
+                                              mem)
+            except import_logic.DestNotCompatible:
+                msgs = self.rthread.radio.validate_memory(mem)
+                errs = [x for x in msgs if isinstance(x,
+                                                      chirp_common.ValidationError)]
+                if errs:
+                    d = miscwidgets.YesNoDialog(title=_("Incompatible Memory"),
+                                                buttons=(gtk.STOCK_OK, 1,
+                                                         gtk.STOCK_CANCEL, 2))
+                    d.set_text(_("Pasted memory {number} is not compatible with "
+                                 "this radio because:").format(number=src_number) +\
+                                   os.linesep + os.linesep.join(msgs))
+                    r = d.run()
+                    d.destroy()
+                    if r == 2:
+                        break
+                    else:
+                        iter = store.iter_next(iter)
+                        continue
+
+            self._set_memory(iter, mem)
+            iter = store.iter_next(iter)
+
+            job = common.RadioJob(self._set_memory_cb, "set_memory", mem)
+            job.set_desc(_("Writing memory {number}").format(number=mem.number))
+            self.rthread.submit(job)
+
+    def paste_selection(self):
+        clipboard = gtk.Clipboard(selection="PRIMARY")
+        clipboard.request_text(self._paste_selection)
+
+    def prepare_close(self):
+        cols = self.view.get_columns()
+        self._config.set("column_order_%s" % self.__class__.__name__,
+                         ",".join([x.get_title() for x in cols]))
+            
+
+class DstarMemoryEditor(MemoryEditor):
+    def _get_cols_to_hide(self, iter):
+        hide = MemoryEditor._get_cols_to_hide(self, iter)
+
+        mode, = self.store.get(iter, self.col(_("Mode")))
+        if mode != "DV":
+            hide += [self.col("URCALL"),
+                     self.col("RPT1CALL"),
+                     self.col("RPT2CALL")]
+
+        return hide
+
+    def render(self, null, rend, model, iter, colnum):
+        MemoryEditor.render(self, null, rend, model, iter, colnum)
+
+        vals = model.get(iter, *tuple(range(0, len(self.cols))))
+        val = vals[colnum]
+
+        def _enabled(sensitive):
+            rend.set_property("sensitive", sensitive)
+
+        def d_unless_mode(mode):
+            _enabled(vals[self.col(_("Mode"))] == mode)
+
+        _dv_columns = [_("URCALL"), _("RPT1CALL"), _("RPT2CALL"),
+                       _("Digital Code")]
+        dv_columns = [self.col(x) for x in _dv_columns]
+        if colnum in dv_columns:
+            d_unless_mode("DV")
+
+    def _get_memory(self, iter):
+        vals = self.store.get(iter, *range(0, len(self.cols)))
+        if vals[self.col(_("Mode"))] != "DV":
+            return MemoryEditor._get_memory(self, iter)
+
+        mem = chirp_common.DVMemory()
+
+        MemoryEditor._set_mem_vals(self, mem, vals, iter)
+
+        mem.dv_urcall = vals[self.col(_("URCALL"))]
+        mem.dv_rpt1call = vals[self.col(_("RPT1CALL"))]
+        mem.dv_rpt2call = vals[self.col(_("RPT2CALL"))]
+        mem.dv_code = vals[self.col(_("Digital Code"))]
+
+        return mem
+
+    def __init__(self, rthread):
+        # I think self.cols is "static" or "unbound" or something else
+        # like that and += modifies the type, not self (how bizarre)
+        self.cols = list(self.cols)
+        new_cols = [("URCALL", TYPE_STRING, gtk.CellRendererCombo),
+                    ("RPT1CALL", TYPE_STRING, gtk.CellRendererCombo),
+                    ("RPT2CALL", TYPE_STRING, gtk.CellRendererCombo),
+                    ("Digital Code", TYPE_INT, gtk.CellRendererText),
+                    ]
+        for col in new_cols:
+            index = self.cols.index(("_filled", TYPE_BOOLEAN, None))
+            self.cols.insert(index, col)
+
+        self.choices = dict(self.choices)
+        self.defaults = dict(self.defaults)
+
+        self.choices["URCALL"] = gtk.ListStore(TYPE_STRING, TYPE_STRING)
+        self.choices["RPT1CALL"] = gtk.ListStore(TYPE_STRING, TYPE_STRING)
+        self.choices["RPT2CALL"] = gtk.ListStore(TYPE_STRING, TYPE_STRING)
+
+        self.defaults["URCALL"] = ""
+        self.defaults["RPT1CALL"] = ""
+        self.defaults["RPT2CALL"] = ""
+        self.defaults["Digital Code"] = 0
+
+        MemoryEditor.__init__(self, rthread)
+    
+        def ucall_cb(calls):
+            self.defaults["URCALL"] = calls[0]
+            for call in calls:
+                self.choices["URCALL"].append((call, call))
+        
+        if self._features.requires_call_lists:
+            ujob = common.RadioJob(ucall_cb, "get_urcall_list")
+            ujob.set_desc(_("Downloading URCALL list"))
+            rthread.submit(ujob)
+
+        def rcall_cb(calls):
+            self.defaults["RPT1CALL"] = calls[0]
+            self.defaults["RPT2CALL"] = calls[0]
+            for call in calls:
+                self.choices["RPT1CALL"].append((call, call))
+                self.choices["RPT2CALL"].append((call, call))
+
+        if self._features.requires_call_lists:
+            rjob = common.RadioJob(rcall_cb, "get_repeater_call_list")
+            rjob.set_desc(_("Downloading RPTCALL list"))
+            rthread.submit(rjob)
+
+        _dv_columns = ["URCALL", "RPT1CALL", "RPT2CALL", "Digital Code"]
+
+        if not self._features.requires_call_lists:
+            for i in _dv_columns:
+                if not self.choices.has_key(i):
+                    continue
+                column = self.view.get_column(self.col(i))
+                rend = column.get_cell_renderers()[0]
+                rend.set_property("has-entry", True)
+
+        for i in _dv_columns:
+            col = self.view.get_column(self.col(i))
+            rend = col.get_cell_renderers()[0]
+            rend.set_property("family", "Monospace")
+
+    def set_urcall_list(self, urcalls):
+        store = self.choices["URCALL"]
+
+        store.clear()
+        for call in urcalls:
+            store.append((call, call))
+
+    def set_repeater_list(self, repeaters):
+        for listname in ["RPT1CALL", "RPT2CALL"]:
+            store = self.choices[listname]
+
+            store.clear()
+            for call in repeaters:
+                store.append((call, call))
+
+    def _set_memory(self, iter, memory):
+        MemoryEditor._set_memory(self, iter, memory)
+
+        if isinstance(memory, chirp_common.DVMemory):
+            self.store.set(iter,
+                           self.col("URCALL"), memory.dv_urcall,
+                           self.col("RPT1CALL"), memory.dv_rpt1call,
+                           self.col("RPT2CALL"), memory.dv_rpt2call,
+                           self.col("Digital Code"), memory.dv_code,
+                           )
+        else:
+            self.store.set(iter,
+                           self.col("URCALL"), "",
+                           self.col("RPT1CALL"), "",
+                           self.col("RPT2CALL"), "",
+                           self.col("Digital Code"), 0,
+                           )
+
+class ID800MemoryEditor(DstarMemoryEditor):
+    pass
diff --git a/chirpui/miscwidgets.py b/chirpui/miscwidgets.py
new file mode 100644
index 0000000..6562363
--- /dev/null
+++ b/chirpui/miscwidgets.py
@@ -0,0 +1,743 @@
+# 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 gtk
+import gobject
+import pango
+
+import os
+
+from chirp import platform
+
+class KeyedListWidget(gtk.HBox):
+    __gsignals__ = {
+        "item-selected" : (gobject.SIGNAL_RUN_LAST,
+                           gobject.TYPE_NONE,
+                           (gobject.TYPE_STRING,)),
+        "item-toggled" : (gobject.SIGNAL_ACTION,
+                          gobject.TYPE_BOOLEAN,
+                          (gobject.TYPE_STRING, gobject.TYPE_BOOLEAN)),
+        "item-set" : (gobject.SIGNAL_ACTION,
+                      gobject.TYPE_BOOLEAN,
+                      (gobject.TYPE_STRING,)),
+        }
+
+    def _toggle(self, rend, path, colnum):
+        if self.__toggle_connected:
+            self.__store[path][colnum] = not self.__store[path][colnum]
+            iter = self.__store.get_iter(path)
+            id, = self.__store.get(iter, 0)
+            self.emit("item-toggled", id, self.__store[path][colnum])
+
+    def _edited(self, rend, path, new, colnum):
+        iter = self.__store.get_iter(path)
+        key, oldval = self.__store.get(iter, 0, colnum)
+        self.__store.set(iter, colnum, new)
+        if not self.emit("item-set", key):
+            self.__store.set(iter, colnum, oldval)
+
+    def _mouse(self, view, event):
+        x, y = event.get_coords()
+        path = self.__view.get_path_at_pos(int(x), int(y))
+        if path:
+            self.__view.set_cursor_on_cell(path[0])
+
+        sel = self.get_selected()
+        if sel:
+            self.emit("item-selected", sel)
+
+    def _make_view(self):
+        colnum = -1
+    
+        for typ, cap in self.columns:
+            colnum += 1
+            if colnum == 0:
+                continue # Key column
+    
+            if typ in [gobject.TYPE_STRING, gobject.TYPE_INT, gobject.TYPE_FLOAT]:
+                rend = gtk.CellRendererText()
+                rend.set_property("ellipsize", pango.ELLIPSIZE_END)
+                column = gtk.TreeViewColumn(cap, rend, text=colnum)
+            elif typ in [gobject.TYPE_BOOLEAN]:
+                rend = gtk.CellRendererToggle()
+                rend.connect("toggled", self._toggle, colnum)
+                column = gtk.TreeViewColumn(cap, rend, active=colnum)
+            else:
+                raise Exception("Unsupported type %s" % typ)
+            
+            column.set_sort_column_id(colnum)
+            self.__view.append_column(column)
+    
+        self.__view.connect("button_press_event", self._mouse)
+
+    def set_item(self, key, *values):
+        iter = self.__store.get_iter_first()
+        while iter:
+            id, = self.__store.get(iter, 0)
+            if id == key:
+                self.__store.insert_after(iter, row=(id,)+values)
+                self.__store.remove(iter)
+                return
+            iter = self.__store.iter_next(iter)
+    
+        self.__store.append(row=(key,) + values)
+
+        self.emit("item-set", key)
+    
+    def get_item(self, key):
+        iter = self.__store.get_iter_first()
+        while iter:
+            vals = self.__store.get(iter, *tuple(range(len(self.columns))))
+            if vals[0] == key:
+                return vals
+            iter = self.__store.iter_next(iter)
+    
+        return None
+    
+    def del_item(self, key):
+        iter = self.__store.get_iter_first()
+        while iter:
+            id, = self.__store.get(iter, 0)
+            if id == key:
+                self.__store.remove(iter)
+                return True
+
+            iter = self.__store.iter_next(iter)
+    
+        return False
+    
+    def has_item(self, key):
+        return self.get_item(key) is not None
+    
+    def get_selected(self):
+        try:
+            (store, iter) = self.__view.get_selection().get_selected()
+            return store.get(iter, 0)[0]
+        except Exception, e:
+            print "Unable to find selected: %s" % e
+            return None
+
+    def select_item(self, key):
+        if key is None:
+            sel = self.__view.get_selection()
+            sel.unselect_all()
+            return True
+
+        iter = self.__store.get_iter_first()
+        while iter:
+            if self.__store.get(iter, 0)[0] == key:
+                selection = self.__view.get_selection()
+                path = self.__store.get_path(iter)
+                selection.select_path(path)
+                return True
+            iter = self.__store.iter_next(iter)
+
+        return False
+        
+    def get_keys(self):
+        keys = []
+        iter = self.__store.get_iter_first()
+        while iter:
+            key, = self.__store.get(iter, 0)
+            keys.append(key)
+            iter = self.__store.iter_next(iter)
+
+        return keys
+
+    def __init__(self, columns):
+        gtk.HBox.__init__(self, True, 0)
+    
+        self.columns = columns
+    
+        types = tuple([x for x,y in columns])
+    
+        self.__store = gtk.ListStore(*types)
+        self.__view = gtk.TreeView(self.__store)
+    
+        self.pack_start(self.__view, 1, 1, 1)
+    
+        self.__toggle_connected = False
+
+        self._make_view()
+        self.__view.show()
+
+    def connect(self, signame, *args):
+        if signame == "item-toggled":
+            self.__toggle_connected = True
+        
+        gtk.HBox.connect(self, signame, *args)
+
+    def set_editable(self, column, is_editable):
+        col = self.__view.get_column(column)
+        rend = col.get_cell_renderers()[0]
+        rend.set_property("editable", True)
+        rend.connect("edited", self._edited, column + 1)
+
+    def set_sort_column(self, column, value=None):
+        if not value:
+            value = column
+        col = self.__view.get_column(column)
+        col.set_sort_column_id(value)
+
+    def get_renderer(self, colnum):
+        return self.__view.get_column(colnum).get_cell_renderers()[0]
+
+class ListWidget(gtk.HBox):
+    __gsignals__ = {
+        "click-on-list" : (gobject.SIGNAL_RUN_LAST,
+                           gobject.TYPE_NONE,
+                           (gtk.TreeView, gtk.gdk.Event)),
+        "item-toggled" : (gobject.SIGNAL_RUN_LAST,
+                          gobject.TYPE_NONE,
+                          (gobject.TYPE_PYOBJECT,)),
+        }
+
+    store_type = gtk.ListStore
+
+    def mouse_cb(self, view, event):
+        self.emit("click-on-list", view, event)
+
+    # pylint: disable-msg=W0613
+    def _toggle(self, render, path, column):
+        self._store[path][column] = not self._store[path][column]
+        iter = self._store.get_iter(path)
+        vals = tuple(self._store.get(iter, *tuple(range(self._ncols))))
+        for cb in self.toggle_cb:
+            cb(*vals)
+        self.emit("item-toggled", vals)
+
+    def make_view(self, columns):
+        self._view = gtk.TreeView(self._store)
+
+        for _type, _col in columns:
+            if _col.startswith("__"):
+                continue
+
+            index = columns.index((_type, _col))
+            if _type == gobject.TYPE_STRING or \
+                    _type == gobject.TYPE_INT or \
+                    _type == gobject.TYPE_FLOAT:
+                rend = gtk.CellRendererText()
+                column = gtk.TreeViewColumn(_col, rend, text=index)
+                column.set_resizable(True)
+                rend.set_property("ellipsize", pango.ELLIPSIZE_END)
+            elif _type == gobject.TYPE_BOOLEAN:
+                rend = gtk.CellRendererToggle()
+                rend.connect("toggled", self._toggle, index)
+                column = gtk.TreeViewColumn(_col, rend, active=index)
+            else:
+                raise Exception("Unknown column type (%i)" % index)
+
+            column.set_sort_column_id(index)
+            self._view.append_column(column)
+
+        self._view.connect("button_press_event", self.mouse_cb)
+
+    def __init__(self, columns, parent=True):
+        gtk.HBox.__init__(self)
+
+        # pylint: disable-msg=W0612
+        col_types = tuple([x for x, y in columns])
+        self._ncols = len(col_types)
+        
+        self._store = self.store_type(*col_types)
+        self._view = None
+        self.make_view(columns)
+
+        self._view.show()
+        if parent:
+            self.pack_start(self._view, 1, 1, 1)
+
+        self.toggle_cb = []
+
+    def packable(self):
+        return self._view
+
+    def add_item(self, *vals):
+        if len(vals) != self._ncols:
+            raise Exception("Need %i columns" % self._ncols)
+
+        args = []
+        i = 0
+        for val in vals:
+            args.append(i)
+            args.append(val)
+            i += 1
+
+        args = tuple(args)
+
+        iter = self._store.append()
+        self._store.set(iter, *args)
+
+    def _remove_item(self, model, path, iter, match):
+        vals = model.get(iter, *tuple(range(0, self._ncols)))
+        if vals == match:
+            model.remove(iter)
+
+    def remove_item(self, *vals):
+        if len(vals) != self._ncols:
+            raise Exception("Need %i columns" % self._ncols)
+
+    def remove_selected(self):
+        try:
+            (lst, iter) = self._view.get_selection().get_selected()
+            lst.remove(iter)
+        except Exception, e:
+            print "Unable to remove selected: %s" % e
+
+    def get_selected(self, take_default=False):
+        (lst, iter) = self._view.get_selection().get_selected()
+        if not iter and take_default:
+            iter = lst.get_iter_first()
+
+        return lst.get(iter, *tuple(range(self._ncols)))
+
+    def move_selected(self, delta):
+        (lst, iter) = self._view.get_selection().get_selected()
+
+        pos = int(lst.get_path(iter)[0])
+
+        try:
+            target = None
+
+            if delta > 0 and pos > 0:
+                target = lst.get_iter(pos-1)
+            elif delta < 0:
+                target = lst.get_iter(pos+1)
+        except Exception, e:
+            return False
+
+        if target:
+            return lst.swap(iter, target)
+
+    def _get_value(self, model, path, iter, lst):
+        lst.append(model.get(iter, *tuple(range(0, self._ncols))))
+
+    def get_values(self):
+        lst = []
+
+        self._store.foreach(self._get_value, lst)
+
+        return lst
+
+    def set_values(self, lst):
+        self._store.clear()
+
+        for i in lst:
+            self.add_item(*i)
+
+class TreeWidget(ListWidget):
+    store_type = gtk.TreeStore
+
+    # pylint: disable-msg=W0613
+    def _toggle(self, render, path, column):
+        self._store[path][column] = not self._store[path][column]
+        iter = self._store.get_iter(path)
+        vals = tuple(self._store.get(iter, *tuple(range(self._ncols))))
+
+        piter = self._store.iter_parent(iter)
+        if piter:
+            parent = self._store.get(piter, self._key)[0]
+        else:
+            parent = None
+
+        for cb in self.toggle_cb:
+            cb(parent, *vals)
+
+    def __init__(self, columns, key, parent=True):
+        ListWidget.__init__(self, columns, parent)
+
+        self._key = key
+
+    def _add_item(self, piter, *vals):
+        args = []
+        i = 0
+        for val in vals:
+            args.append(i)
+            args.append(val)
+            i += 1
+
+        args = tuple(args)
+
+        iter = self._store.append(piter)
+        self._store.set(iter, *args)
+
+    def _iter_of(self, key, iter=None):
+        if not iter:
+            iter = self._store.get_iter_first()
+
+        while iter is not None:
+            _id = self._store.get(iter, self._key)[0]
+            if _id == key:
+                return iter
+
+            iter = self._store.iter_next(iter)
+
+        return None
+
+    def add_item(self, parent, *vals):
+        if len(vals) != self._ncols:
+            raise Exception("Need %i columns" % self._ncols)
+
+        if not parent:
+            self._add_item(None, *vals)
+        else:
+            iter = self._iter_of(parent)
+            if iter:
+                self._add_item(iter, *vals)
+            else:
+                raise Exception("Parent not found: %s", parent)
+
+    def _set_values(self, parent, vals):
+        if isinstance(vals, dict):
+            for key, val in vals.items():
+                iter = self._store.append(parent)
+                self._store.set(iter, self._key, key)
+                self._set_values(iter, val)
+        elif isinstance(vals, list):
+            for i in vals:
+                self._set_values(parent, i)
+        elif isinstance(vals, tuple):
+            self._add_item(parent, *vals)
+        else:
+            print "Unknown type: %s" % vals
+
+    def set_values(self, vals):
+        self._store.clear()
+        self._set_values(self._store.get_iter_first(), vals)
+
+    def del_item(self, parent, key):
+        iter = self._iter_of(key,
+                             self._store.iter_children(self._iter_of(parent)))
+        if iter:
+            self._store.remove(iter)
+        else:
+            raise Exception("Item not found")
+
+    def get_item(self, parent, key):
+        iter = self._iter_of(key,
+                             self._store.iter_children(self._iter_of(parent)))
+
+        if iter:
+            return self._store.get(iter, *(tuple(range(0, self._ncols))))
+        else:
+            raise Exception("Item not found")
+
+    def set_item(self, parent, *vals):
+        iter = self._iter_of(vals[self._key],
+                             self._store.iter_children(self._iter_of(parent)))
+
+        if iter:
+            args = []
+            i = 0
+
+            for val in vals:
+                args.append(i)
+                args.append(val)
+                i += 1
+
+            self._store.set(iter, *(tuple(args)))
+        else:
+            raise Exception("Item not found")
+
+class ProgressDialog(gtk.Window):
+    def __init__(self, title, parent=None):
+        gtk.Window.__init__(self, gtk.WINDOW_TOPLEVEL)
+        self.set_modal(True)
+        self.set_type_hint(gtk.gdk.WINDOW_TYPE_HINT_DIALOG)
+        self.set_title(title)
+        if parent:
+            self.set_transient_for(parent)
+
+        self.set_resizable(False)
+
+        vbox = gtk.VBox(False, 2)
+
+        self.label = gtk.Label("")
+        self.label.set_size_request(100, 50)
+        self.label.show()
+
+        self.pbar = gtk.ProgressBar()
+        self.pbar.show()
+        
+        vbox.pack_start(self.label, 0, 0, 0)
+        vbox.pack_start(self.pbar, 0, 0, 0)
+
+        vbox.show()
+
+        self.add(vbox)
+
+    def set_text(self, text):
+        self.label.set_text(text)
+        self.queue_draw()
+
+        while gtk.events_pending():
+            gtk.main_iteration_do(False)
+
+    def set_fraction(self, frac):
+        self.pbar.set_fraction(frac)
+        self.queue_draw()
+
+        while gtk.events_pending():
+            gtk.main_iteration_do(False)
+
+class LatLonEntry(gtk.Entry):
+    def __init__(self, *args):
+        gtk.Entry.__init__(self, *args)
+
+        self.connect("changed", self.format)
+
+    def format(self, entry):
+        string = entry.get_text()
+        if string is None:
+            return
+
+        deg = u"\u00b0"
+
+        while " " in string:
+            if "." in string:
+                break
+            elif deg not in string:
+                string = string.replace(" ", deg)
+            elif "'" not in string:
+                string = string.replace(" ", "'")
+            elif '"' not in string:
+                string = string.replace(" ", '"')
+            else:
+                string = string.replace(" ", "")
+
+        entry.set_text(string)
+
+    def parse_dd(self, string):
+        return float(string)
+
+    def parse_dm(self, string):
+        string = string.strip()
+        string = string.replace('  ', ' ')
+        
+        (_degrees, _minutes) = string.split(' ', 2)
+
+        degrees = int(_degrees)
+        minutes = float(_minutes)
+
+        return degrees + (minutes / 60.0)
+
+    def parse_dms(self, string):
+        string = string.replace(u"\u00b0", " ")
+        string = string.replace('"', ' ')
+        string = string.replace("'", ' ')
+        string = string.replace('  ', ' ')
+        string = string.strip()
+
+        items = string.split(' ')
+
+        if len(items) > 3:
+            raise Exception("Invalid format")
+        elif len(items) == 3:
+            deg = items[0]
+            mns = items[1]
+            sec = items[2]
+        elif len(items) == 2:
+            deg = items[0]
+            mns = items[1]
+            sec = 0
+        elif len(items) == 1:
+            deg = items[0]
+            mns = 0
+            sec = 0
+        else:
+            deg = 0
+            mns = 0
+            sec = 0
+
+        degrees = int(deg)
+        minutes = int(mns)
+        seconds = float(sec)
+        
+        return degrees + (minutes / 60.0) + (seconds / 3600.0)
+
+    def value(self):
+        string = self.get_text()
+
+        try:
+            return self.parse_dd(string)
+        except:
+            try:
+                return self.parse_dm(string)
+            except:
+                try:
+                    return self.parse_dms(string)
+                except Exception, e:
+                    print "DMS: %s" % e
+
+        raise Exception("Invalid format")
+
+    def validate(self):
+        try:
+            self.value()
+            return True
+        except:
+            return False
+
+class YesNoDialog(gtk.Dialog):
+    def __init__(self, title="", parent=None, buttons=None):
+        gtk.Dialog.__init__(self, title=title, parent=parent, buttons=buttons)
+
+        self._label = gtk.Label("")
+        self._label.show()
+
+        # pylint: disable-msg=E1101
+        self.vbox.pack_start(self._label, 1, 1, 1)
+
+    def set_text(self, text):
+        self._label.set_text(text)
+
+def make_choice(options, editable=True, default=None):
+    if editable:
+        sel = gtk.combo_box_entry_new_text()
+    else:
+        sel = gtk.combo_box_new_text()
+
+    for opt in options:
+        sel.append_text(opt)
+
+    if default:
+        try:
+            idx = options.index(default)
+            sel.set_active(idx)
+        except:
+            pass
+
+    return sel
+
+class FilenameBox(gtk.HBox):
+    __gsignals__ = {
+        "filename-changed" : (gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, ()),
+        }
+
+    def do_browse(self, _, dir):
+        if self.filename.get_text():
+            start = os.path.dirname(self.filename.get_text())
+        else:
+            start = None
+
+        if dir:
+            fn = platform.get_platform().gui_select_dir(start)
+        else:
+            fn = platform.get_platform().gui_save_file(start, types=self.types)
+        if fn:
+            self.filename.set_text(fn)
+
+    def do_changed(self, _):
+        self.emit("filename_changed")
+
+    def __init__(self, find_dir=False, types=[]):
+        gtk.HBox.__init__(self, False, 0)
+
+        self.types = types
+
+        self.filename = gtk.Entry()
+        self.filename.show()
+        self.pack_start(self.filename, 1, 1, 1)
+
+        browse = gtk.Button("...")
+        browse.show()
+        self.pack_start(browse, 0, 0, 0)
+
+        self.filename.connect("changed", self.do_changed)
+        browse.connect("clicked", self.do_browse, find_dir)
+
+    def set_filename(self, fn):
+        self.filename.set_text(fn)
+
+    def get_filename(self):
+        return self.filename.get_text()    
+
+def make_pixbuf_choice(options, default=None):
+    store = gtk.ListStore(gtk.gdk.Pixbuf, gobject.TYPE_STRING)
+    box = gtk.ComboBox(store)
+
+    cell = gtk.CellRendererPixbuf()
+    box.pack_start(cell, True)
+    box.add_attribute(cell, "pixbuf", 0)
+
+    cell = gtk.CellRendererText()
+    box.pack_start(cell, True)
+    box.add_attribute(cell, "text", 1)
+
+    _default = None
+    for pic, value in options:
+        iter = store.append()
+        store.set(iter, 0, pic, 1, value)
+        if default == value:
+            _default = options.index((pic, value))
+
+    if _default:
+        box.set_active(_default)
+
+    return box
+
+def test():
+    win = gtk.Window(gtk.WINDOW_TOPLEVEL)
+    lst = ListWidget([(gobject.TYPE_STRING, "Foo"),
+                    (gobject.TYPE_BOOLEAN, "Bar")])
+
+    lst.add_item("Test1", True)
+    lst.set_values([("Test2", True), ("Test3", False)])
+    
+    lst.show()
+    win.add(lst)
+    win.show()
+
+    win1 = ProgressDialog("foo")
+    win1.show()
+
+    win2 = gtk.Window(gtk.WINDOW_TOPLEVEL)
+    lle = LatLonEntry()
+    lle.show()
+    win2.add(lle)
+    win2.show()
+
+    win3 = gtk.Window(gtk.WINDOW_TOPLEVEL)
+    lst = TreeWidget([(gobject.TYPE_STRING, "Id"),
+                      (gobject.TYPE_STRING, "Value")],
+                     1)
+    #l.add_item(None, "Foo", "Bar")
+    #l.add_item("Foo", "Bar", "Baz")
+    lst.set_values({"Fruit" : [("Apple", "Red"), ("Orange", "Orange")],
+                    "Pizza" : [("Cheese", "Simple"), ("Pepperoni", "Yummy")]})
+    lst.add_item("Fruit", "Bananna", "Yellow")
+    lst.show()
+    win3.add(lst)
+    win3.show()
+
+    def print_val(entry):
+        if entry.validate():
+            print "Valid: %s" % entry.value()
+        else:
+            print "Invalid"
+    lle.connect("activate", print_val)
+
+    lle.set_text("45 13 12")
+
+    try:
+        gtk.main()
+    except KeyboardInterrupt:
+        pass
+
+    print lst.get_values()
+
+if __name__ == "__main__":
+    test()
diff --git a/chirpui/reporting.py b/chirpui/reporting.py
new file mode 100644
index 0000000..c48c6bb
--- /dev/null
+++ b/chirpui/reporting.py
@@ -0,0 +1,185 @@
+# Copyright 2011 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/>.
+
+# README:
+#
+# I know that collecting data is not very popular.  I don't like it
+# either.  However, it's hard to tell what drivers people are using
+# and I think it would be helpful if I had that information.  This is
+# completely optional, so you can turn it off if you want.  It doesn't
+# report anything other than version and model usage information.  The
+# code below is very conservative, and will disable itself if reporting
+# fails even once or takes too long to perform.  It's run in a thread
+# so that the user shouldn't even notice it's happening.
+#
+
+import threading
+import os
+import time
+
+from chirp import CHIRP_VERSION, platform
+
+REPORT_URL = "http://chirp.danplanet.com/report/report.php?do_report"
+ENABLED = True
+DEBUG = os.getenv("CHIRP_DEBUG") == "y"
+THREAD_SEM = threading.Semaphore(10) # Maximum number of outstanding threads
+LAST = 0
+LAST_TYPE = None
+
+try:
+    # Don't let failure to import any of these modules cause trouble
+    from chirpui import config
+    import xmlrpclib
+except:
+    ENABLED = False
+
+def debug(string):
+    if DEBUG:
+        print string
+
+def should_report():
+    if not ENABLED:
+        debug("Not reporting due to recent failure")
+        return False
+
+    conf = config.get()
+    if conf.get_bool("no_report"):
+        debug("Reporting disabled")
+        return False
+
+    return True
+
+def _report_model_usage(model, direction, success):
+    global ENABLED
+    if direction not in ["live", "download", "upload", "import", "export", "importsrc"]:
+        print "Invalid direction `%s'" % direction
+        return True # This is a bug, but not fatal
+
+    model = "%s_%s" % (model.VENDOR, model.MODEL)
+    data = "%s,%s,%s" % (model, direction, success)
+
+    debug("Reporting model usage: %s" % data)
+
+    proxy = xmlrpclib.ServerProxy(REPORT_URL)
+    id = proxy.report_stats(CHIRP_VERSION,
+                            platform.get_platform().os_version_string(),
+                            "model_use",
+                            data)
+
+    # If the server returns zero, it wants us to shut up
+    return id != 0
+
+def _report_exception(stack):
+    global ENABLED
+
+    debug("Reporting exception")
+
+    proxy = xmlrpclib.ServerProxy(REPORT_URL)
+    id = proxy.report_exception(CHIRP_VERSION,
+                                platform.get_platform().os_version_string(),
+                                "exception",
+                                stack)
+
+    # If the server returns zero, it wants us to shut up
+    return id != 0
+
+def _report_misc_error(module, data):
+    global ENABLED
+
+    debug("Reporting misc error with %s" % module)
+
+    proxy = xmlrpclib.ServerProxy(REPORT_URL)
+    id = proxy.report_misc_error(CHIRP_VERSION,
+                                 platform.get_platform().os_version_string(),
+                                 module, data)
+
+    # If the server returns zero, it wants us to shut up
+    return id != 0
+
+def _check_for_updates(callback):
+    debug("Checking for updates")
+    proxy = xmlrpclib.ServerProxy(REPORT_URL)
+    ver = proxy.check_for_updates(CHIRP_VERSION,
+                                  platform.get_platform().os_version_string())
+
+    debug("Server reports version %s is latest" % ver)
+    callback(ver)
+    return True
+
+class ReportThread(threading.Thread):
+    def __init__(self, func, *args):
+        threading.Thread.__init__(self)
+        self.__func = func
+        self.__args = args
+
+    def _run(self):
+        try:
+            return self.__func(*self.__args)
+        except Exception, e:
+            debug("Failed to report: %s" % e)
+            return False
+        
+    def run(self):
+        start = time.time()
+        result = self._run()
+        if not result:
+            # Reporting failed
+            ENABLED = False
+        elif (time.time() - start) > 15:
+            # Reporting took too long
+            debug("Time to report was %.2f sec -- Disabling" % \
+                      (time.time()-start))
+            ENABLED = False
+
+        THREAD_SEM.release()
+
+def dispatch_thread(func, *args):
+    global LAST
+    global LAST_TYPE
+
+    # If reporting is disabled or failing, bail
+    if not should_report():
+        debug("Reporting is disabled")
+        return
+
+    # If the time between now and the last report is less than 5 seconds, bail
+    delta = time.time() - LAST
+    if delta < 5 and func == LAST_TYPE:
+        debug("Throttling...")
+        return
+
+    LAST = time.time()
+    LAST_TYPE = func
+
+    # If there are already too many threads running, bail
+    if not THREAD_SEM.acquire(False):
+        debug("Too many threads already running")
+        return
+
+    t = ReportThread(func, *args)
+    t.start()
+
+def report_model_usage(model, direction, success):
+    dispatch_thread(_report_model_usage, model, direction, success)
+
+def report_exception(stack):
+    dispatch_thread(_report_exception, stack)
+
+def report_misc_error(module, data):
+    dispatch_thread(_report_misc_error, module, data)
+
+# Calls callback with the latest version
+def check_for_updates(callback):
+    dispatch_thread(_check_for_updates, callback)
diff --git a/chirpui/settingsedit.py b/chirpui/settingsedit.py
new file mode 100644
index 0000000..b67982f
--- /dev/null
+++ b/chirpui/settingsedit.py
@@ -0,0 +1,209 @@
+# Copyright 2012 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 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 gtk
+import gobject
+
+from chirp import chirp_common, settings
+from chirpui import common, miscwidgets
+
+class RadioSettingProxy(settings.RadioSetting):
+    def __init__(self, setting, editor):
+        self._setting = setting
+        self._editor = editor
+
+class SettingsEditor(common.Editor):
+    def __init__(self, rthread):
+        common.Editor.__init__(self)
+        self._rthread = rthread
+
+        self.root = gtk.HBox(False, 10)
+        self._store = gtk.TreeStore(gobject.TYPE_STRING,
+                                    gobject.TYPE_PYOBJECT)
+        self._view = gtk.TreeView(self._store)
+        self._view.set_size_request(150, -1)
+        self._view.connect("button-press-event", self._group_selected)
+        self._view.show()
+        self.root.pack_start(self._view, 0, 0, 0)
+
+        col = gtk.TreeViewColumn("", gtk.CellRendererText(), text=0)
+        self._view.append_column(col)
+
+        self._table = gtk.Table(20, 3)
+        self._table.set_col_spacings(10)
+        self._table.show()
+
+        sw = gtk.ScrolledWindow()
+        sw.add_with_viewport(self._table)
+        sw.show()
+
+        self.root.pack_start(sw, 1, 1, 1)
+
+        self._index = 0
+
+        self._top_setting_group = None
+
+        job = common.RadioJob(self._build_ui, "get_settings")
+        job.set_desc("Getting radio settings")
+        self._rthread.submit(job)
+
+    def _save_settings(self):
+        if self._top_setting_group is None:
+            return
+
+        job = common.RadioJob(None, "set_settings", self._top_setting_group)
+        job.set_desc("Setting radio settings")
+        self._rthread.submit(job)
+
+    def _load_setting(self, value, widget):
+        if isinstance(value, settings.RadioSettingValueInteger):
+            adj = widget.get_adjustment()
+            adj.configure(value.get_value(),
+                          value.get_min(), value.get_max(),
+                          value.get_step(), 1, 0)
+        elif isinstance(value, settings.RadioSettingValueBoolean):
+            widget.set_active(value.get_value())
+        elif isinstance(value, settings.RadioSettingValueList):
+            model = widget.get_model()
+            model.clear()
+            for option in value.get_options():
+                widget.append_text(option)
+            current = value.get_value()
+            index = value.get_options().index(current)
+            widget.set_active(index)
+        elif isinstance(value, settings.RadioSettingValueString):
+            widget.set_text(str(value).rstrip())
+        else:
+            print "Unsupported widget type %s for %s" % (value.__class__,
+                                                         element.get_name())
+            
+    def _save_setting(self, widget, value):
+        if isinstance(value, settings.RadioSettingValueInteger):
+            value.set_value(widget.get_adjustment().get_value())
+        elif isinstance(value, settings.RadioSettingValueBoolean):
+            value.set_value(widget.get_active())
+        elif isinstance(value, settings.RadioSettingValueList):
+            value.set_value(widget.get_active_text())
+        elif isinstance(value, settings.RadioSettingValueString):
+            value.set_value(widget.get_text())
+        else:
+            print "Unsupported widget type %s for %s" % (\
+                element.value.__class__,
+                element.get_name())
+
+        self._save_settings()
+
+    def _build_ui_group(self, group):
+        def pack(widget, pos):
+            self._table.attach(widget, pos, pos+1, self._index, self._index+1,
+                               xoptions=gtk.FILL, yoptions=0)
+
+        def abandon(child):
+            self._table.remove(child)
+        self._table.foreach(abandon)
+
+        self._index = 0
+        for element in group:
+            if not isinstance(element, settings.RadioSetting):
+                continue
+            label = gtk.Label(element.get_shortname())
+            label.set_alignment(1.0, 0.5)
+            label.show()
+            pack(label, 0)
+
+            if isinstance(element.value, list) and \
+                    isinstance(element.value[0],
+                               settings.RadioSettingValueInteger):
+                arraybox = gtk.HBox(3, True)
+            else:
+                arraybox = gtk.VBox(3, True)
+            pack(arraybox, 1)
+            arraybox.show()
+
+            widgets = []
+            for index in element.keys():
+                value = element[index]
+                if isinstance(value, settings.RadioSettingValueInteger):
+                    widget = gtk.SpinButton()
+                    print "Digits: %i" % widget.get_digits()
+                    signal = "value-changed"
+                elif isinstance(value, settings.RadioSettingValueBoolean):
+                    widget = gtk.CheckButton(_("Enabled"))
+                    signal = "toggled"
+                elif isinstance(value, settings.RadioSettingValueList):
+                    widget = miscwidgets.make_choice([], editable=False)
+                    signal = "changed"
+                elif isinstance(value, settings.RadioSettingValueString):
+                    widget = gtk.Entry()
+                    signal = "changed"
+                else:
+                    print "Unsupported widget type: %s" % value.__class__
+
+                # Make sure the widget gets left-aligned to match up
+                # with its label
+                lalign = gtk.Alignment(0, 0, 0, 0)
+                lalign.add(widget)
+                lalign.show()
+
+                arraybox.pack_start(lalign, 1, 1, 1)
+                widget.show()
+                self._load_setting(value, widget)
+                widget.connect(signal, self._save_setting, value)
+
+            self._index += 1
+
+    def _build_tree(self, group, parent):
+        iter = self._store.append(parent)
+        self._store.set(iter, 0, group.get_shortname(), 1, group)
+        
+        if self._set_default is None:
+            # If we haven't found the first page with actual settings on it
+            # yet, then look for one here
+            for element in group:
+                if isinstance(element, settings.RadioSetting):
+                    self._set_default = self._store.get_path(iter), group
+                    break
+
+        for element in group:
+            if not isinstance(element, settings.RadioSetting):
+                self._build_tree(element, iter)
+        self._view.expand_all()
+
+    def _build_ui_real(self, group):
+        if not isinstance(group, settings.RadioSettingGroup):
+            print "Toplevel is not a group"
+            return
+
+        self._set_default = None
+        self._top_setting_group = group
+        self._build_tree(group, None)
+        self._view.set_cursor(self._set_default[0])
+        self._build_ui_group(self._set_default[1])
+
+    def _build_ui(self, group):
+        gobject.idle_add(self._build_ui_real, group)
+
+    def _group_selected(self, view, event):
+        if event.button != 1:
+            return
+
+        try:
+            path, col, x, y = view.get_path_at_pos(int(event.x), int(event.y))
+        except TypeError:
+            return # Didn't click on an actual item
+
+        group, = self._store.get(self._store.get_iter(path), 1)
+        if group:
+            self._build_ui_group(group)
diff --git a/chirpui/shiftdialog.py b/chirpui/shiftdialog.py
new file mode 100644
index 0000000..10554d3
--- /dev/null
+++ b/chirpui/shiftdialog.py
@@ -0,0 +1,156 @@
+#
+# 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 gtk
+import gobject
+
+import threading
+
+from chirp import errors, chirp_common
+
+class ShiftDialog(gtk.Dialog):
+    def __init__(self, rthread, parent=None):
+        gtk.Dialog.__init__(self,
+                            title=_("Shift"),
+                            buttons=(gtk.STOCK_CLOSE, gtk.RESPONSE_OK))
+
+        self.set_position(gtk.WIN_POS_CENTER_ALWAYS)
+
+        self.rthread = rthread
+
+        self.__prog = gtk.ProgressBar()
+        self.__prog.show()
+
+        self.__labl = gtk.Label("")
+        self.__labl.show()
+
+        self.vbox.pack_start(self.__prog, 1, 1, 1)
+        self.vbox.pack_start(self.__labl, 0, 0, 0)
+
+        self.quiet = False
+
+        self.thread = None
+
+        self.set_response_sensitive(gtk.RESPONSE_OK, False)
+
+    def _status(self, msg, prog):
+        self.__labl.set_text(msg)
+        self.__prog.set_fraction(prog)
+
+    def status(self, msg, prog):
+        gobject.idle_add(self._status, msg, prog)
+
+    def _shift_memories(self, delta, memories):
+        count = 0.0
+        for i in memories:
+            src = i.number
+            dst = src + delta
+
+            print "Moving %i to %i" % (src, dst)
+            self.status(_("Moving {src} to {dst}").format(src=src,
+                                                          dst=dst),
+                        count / len(memories))
+
+            i.number = dst
+            self.rthread.radio.set_memory(i)
+            count += 1.0
+
+        return int(count)
+
+    def _get_mems_until_hole(self, start):
+        mems = []
+
+        llimit, ulimit = self.rthread.radio.get_features().memory_bounds
+
+        pos = start
+        while pos <= ulimit:
+            self.status(_("Looking for a free spot ({number})").format(\
+                        number=pos), 0)
+            try:
+                mem = self.rthread.radio.get_memory(pos)
+                if mem.empty:
+                    break
+            except errors.InvalidMemoryLocation:
+                break
+
+            mems.append(mem)
+            pos += 1
+
+        if pos > ulimit:
+            raise errors.InvalidMemoryLocation(_("No space to insert a row"))
+
+        print "Found a hole: %i" % pos
+
+        return mems
+
+    def _insert_hole(self, start):
+        mems = self._get_mems_until_hole(start)
+        mems.reverse()
+        if mems:
+            ret = self._shift_memories(1, mems)
+            if ret:
+                # Clear the hole we made
+                self.rthread.radio.erase_memory(start)
+            return ret
+        else:
+            print "No memory list?"
+            return 0
+
+    def _delete_hole(self, start):
+        mems = self._get_mems_until_hole(start+1)
+        if mems:
+            count = self._shift_memories(-1, mems)
+            self.rthread.radio.erase_memory(count+start)
+            return count
+        else:
+            print "No memory list?"
+            return 0
+
+    def finished(self):
+        if self.quiet:
+            gobject.idle_add(self.response, gtk.RESPONSE_OK)
+        else:
+            gobject.idle_add(self.set_response_sensitive, gtk.RESPONSE_OK, True)
+
+    def threadfn(self, newhole, func):
+        self.status("Waiting for radio to become available", 0)
+        self.rthread.lock()
+
+        try:
+            count = func(newhole)
+        except errors.InvalidMemoryLocation, e:
+            self.status(str(e), 0)
+            self.finished()
+            return
+
+        self.rthread.unlock()
+        self.status(_("Moved {count} memories").format(count=count), 1)
+
+        self.finished()
+
+    def insert(self, newhole, quiet=False):
+        self.quiet = quiet
+        self.thread = threading.Thread(target=self.threadfn,
+                                       args=(newhole,self._insert_hole))
+        self.thread.start()
+        gtk.Dialog.run(self)
+
+    def delete(self, newhole, quiet=False):
+        self.quiet = quiet
+        self.thread = threading.Thread(target=self.threadfn,
+                                       args=(newhole,self._delete_hole))
+        self.thread.start()
+        gtk.Dialog.run(self)
diff --git a/chirpw b/chirpw
new file mode 100755
index 0000000..cd97918
--- /dev/null
+++ b/chirpw
@@ -0,0 +1,142 @@
+#!/usr/bin/python
+#
+# 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/>.
+
+
+from chirp import platform, CHIRP_VERSION
+from chirpui import config
+
+# Hack to setup environment
+platform.get_platform()
+
+import gtk
+import sys
+import os
+import locale
+import gettext
+
+p = platform.get_platform()
+if hasattr(sys, "frozen"):
+    log = file(p.config_file("debug.log"), "w", 0)
+    sys.stderr = log
+    sys.stdout = log
+elif not os.isatty(0):
+    log = file(p.config_file("debug.log"), "w", 0)
+    sys.stdout = log
+    sys.stderr = log
+
+print "CHIRP %s on %s (Python %s)" % (CHIRP_VERSION,
+                                      platform.get_platform().os_version_string(),
+                                      sys.version.split()[0])
+
+execpath = platform.get_platform().executable_path()
+localepath = os.path.abspath(os.path.join(execpath, "locale"))
+if not os.path.exists(localepath):
+    localepath = "/usr/share/chirp/locale"
+
+conf = config.get()
+manual_language = conf.get("language", "state")
+langs = []
+if manual_language and manual_language != "Auto":
+    lang_codes = { "English"   : "en_US",
+                   "Polish"    : "pl",
+                   "Italian"   : "it",
+                   "Dutch"     : "nl",
+                   "German"    : "de",
+                   "Hungarian" : "hu",
+                   }
+    try:
+        print lang_codes[manual_language]
+        langs = [lang_codes[manual_language]]
+    except KeyError:
+        print "Unsupported language `%s'" % manual_language
+else:
+    lc, encoding = locale.getdefaultlocale()
+    if (lc):
+	langs = [lc]
+    try:
+        langs += os.getenv("LANG").split(":")
+    except:
+        pass
+
+path = "locale"
+gettext.bindtextdomain("CHIRP", localepath)
+gettext.textdomain("CHIRP")
+lang = gettext.translation("CHIRP", localepath, languages=langs,
+                           fallback=True)
+
+# Python <2.6 does not have str.format(), which chirp uses to make translation
+# strings nicer. So, instead of installing the gettext standard "_()" function,
+# we can install our own, which returns a string of the following class,
+# which emulates the new behavior, thus allowing us to run on older Python
+# versions.
+class CompatStr(str):
+    def format(self, **kwargs):
+        base = lang.gettext(self)
+        for k,v in kwargs.items():
+            base = base.replace("{%s}" % k, str(v))
+        return base
+
+pyver = sys.version.split()[0]
+vmaj, vmin, vrel = pyver.split(".", 3)
+if int(vmaj) < 2 or int(vmin) < 6:
+    # Python <2.6, emulate str.format()
+    import __builtin__
+    def lang_with_format(string):
+        return CompatStr(string)
+    __builtin__._ = lang_with_format
+else:
+    # Python >=2.6, use normal gettext behavior
+    lang.install()
+
+from chirp import *
+from chirpui import mainapp, config
+
+a = mainapp.ChirpMain()
+
+profile = False
+if len(sys.argv) > 1:
+    arg = sys.argv[1]
+    index = 2
+    if arg == "--profile":
+        profile = True
+    elif arg == "--help":
+        print "Usage: %s [file...]" % sys.argv[0]
+        sys.exit(0)
+    else:
+        index = 1
+else:
+    index = 1
+
+for i in sys.argv[index:]:
+    print "Opening %s" % i
+    a.do_open(i)
+
+a.show()
+
+if profile:
+    import cProfile, pstats
+    cProfile.run("gtk.main()", "chirpw.stats")
+    p = pstats.Stats("chirpw.stats")
+    p.sort_stats("cumulative").print_stats(10)
+else:
+    gtk.main()
+
+if config._CONFIG:
+    config._CONFIG.set("last_dir",
+                       platform.get_platform().get_last_dir(),
+                       "state")
+    config._CONFIG.save()
diff --git a/setup.cfg b/setup.cfg
new file mode 100644
index 0000000..ba216f1
--- /dev/null
+++ b/setup.cfg
@@ -0,0 +1,5 @@
+[bdist_rpm]
+requires = pyserial
+packager = Dan Smith <kk7ds at danplanet.com>
+description = A frequency tool for Icom D-STAR Repeaters
+vendor = KK7DS
diff --git a/setup.py b/setup.py
new file mode 100644
index 0000000..28c4eb3
--- /dev/null
+++ b/setup.py
@@ -0,0 +1,160 @@
+import sys
+
+import os
+
+from chirp import CHIRP_VERSION
+from chirp import *
+import chirp
+
+def staticify_chirp_module():
+    import chirp
+
+    init = file("chirp/__init__.py", "w")
+    print >>init, "CHIRP_VERSION = \"%s\"" % CHIRP_VERSION
+    print >>init, "__all__ = %s\n" % str(chirp.__all__)
+    init.close()
+
+    print "Set chirp.py::__all__ = %s" % str(chirp.__all__)
+
+def win32_build():
+    from distutils.core import setup
+    import py2exe
+
+    try:
+        # if this doesn't work, try import modulefinder
+        import py2exe.mf as modulefinder
+        import win32com
+        for p in win32com.__path__[1:]:
+            modulefinder.AddPackagePath("win32com", p)
+        for extra in ["win32com.shell"]: #,"win32com.mapi"
+            __import__(extra)
+            m = sys.modules[extra]
+            for p in m.__path__[1:]:
+                modulefinder.AddPackagePath(extra, p)
+    except ImportError:
+        # no build path setup, no worries.
+        pass
+
+    staticify_chirp_module()
+
+    opts = {
+        "py2exe" : {
+            "includes" : "pango,atk,gobject,cairo,pangocairo,win32gui,win32com,win32com.shell,email.iterators,email.generator,gio",
+
+            "compressed" : 1,
+            "optimize" : 2,
+            "bundle_files" : 3,
+            #        "packages" : ""
+            }
+        }
+
+    mods = []
+    for mod in chirp.__all__:
+        mods.append("chirp.%s" % mod)
+    opts["py2exe"]["includes"] += ("," + ",".join(mods))
+
+    setup(
+        zipfile=None,
+        windows=[{'script'        : "chirpw",
+                  'icon_resources': [(0x0004, 'share/chirp.ico')],
+		 }],
+        options=opts)
+
+def macos_build():
+    from setuptools import setup
+    import shutil
+
+    APP = ['chirp-%s.py' % CHIRP_VERSION]
+    shutil.copy("chirpw", APP[0])
+    DATA_FILES = [('../Frameworks',
+                   ['/opt/local/lib/libpangox-1.0.dylib']),
+		  ('../Resources/', ['/opt/local/lib/pango']),
+                  ]
+    OPTIONS = {'argv_emulation': True, "includes" : "gtk,atk,pangocairo,cairo"}
+
+    setup(
+        app=APP,
+        data_files=DATA_FILES,
+        options={'py2app': OPTIONS},
+        setup_requires=['py2app'],
+        )
+
+    EXEC = 'bash ./build/macos/make_pango.sh /opt/local dist/chirp-%s.app' % CHIRP_VERSION
+    #print "exec string: %s" % EXEC
+    os.system(EXEC) 
+
+def default_build():
+    from distutils.core import setup
+    from glob import glob
+
+    os.system("make -C locale clean all")
+
+    desktop_files = glob("share/*.desktop")
+    #form_files = glob("forms/*.x?l")
+    image_files = glob("images/*")
+    _locale_files = glob("locale/*/LC_MESSAGES/CHIRP.mo")
+    stock_configs = glob("stock_configs/*")
+
+    locale_files = []
+    for f in _locale_files:
+        locale_files.append(("share/chirp/%s" % os.path.dirname(f), [f]))
+
+    print "LOC: %s" % str(locale_files)
+
+    xsd_files = glob("chirp*.xsd")
+
+    setup(
+        name="chirp",
+        packages=["chirp", "chirpui"],
+        version=CHIRP_VERSION,
+        scripts=["chirpw"],
+        data_files=[('share/applications', desktop_files),
+                    ('share/chirp/images', image_files),
+                    ('share/chirp', xsd_files),
+                    ('share/doc/chirp', ['COPYING']),
+                    ('share/pixmaps', ['share/chirp.png']),
+                    ('share/man/man1', ["share/chirpw.1"]),
+                    ('share/chirp/stock_configs', stock_configs),
+                    ] + locale_files)
+
+def rpttool_build():
+    from distutils.core import setup
+    
+    setup(name="rpttool",
+          packages=["chirp"],
+          version="0.3",
+          scripts=["rpttool"],
+          description="A frequency tool for ICOM D-STAR Repeaters",
+          data_files=[('/usr/sbin', ["tools/icomsio.sh"])],
+          )
+
+def nuke_manifest(*files):
+    for i in ["MANIFEST", "MANIFEST.in"]:
+        if os.path.exists(i):
+            os.remove(i)
+
+    if not files:
+        return
+
+    f = file("MANIFEST.in", "w")
+    for fn in files:
+        print >>f, fn
+    f.close()
+                    
+if sys.platform == "darwin":
+    macos_build()
+elif sys.platform == "win32":
+    win32_build()
+else:
+    if os.path.exists("rpttool"):
+        nuke_manifest("include tools/icomsio.sh", "include README.rpttool")
+        rpttool_build()
+    if os.path.exists("chirpui"):
+        nuke_manifest("include *.xsd",
+                      "include share/*.desktop",
+                      "include share/chirp.png",
+                      "include share/*.1",
+                      "include stock_configs/*",
+                      "include COPYING")
+        default_build()
+
diff --git a/share/chirp.desktop b/share/chirp.desktop
new file mode 100644
index 0000000..a65edee
--- /dev/null
+++ b/share/chirp.desktop
@@ -0,0 +1,13 @@
+[Desktop Entry]
+Version=0.1
+Encoding=UTF-8
+Type=Application
+Exec=chirpw
+Icon=chirp.png
+StartupNotify=true
+Terminal=false
+Categories=Application;HamRadio
+MimeType=inode/directory
+Name=CHIRP
+Comment=CHIRP Radio Programming Tool
+GenericName=CHIRP Radio Programming Tool
diff --git a/share/chirp.png b/share/chirp.png
new file mode 100644
index 0000000..fe57c1a
Binary files /dev/null and b/share/chirp.png differ
diff --git a/share/chirpw.1 b/share/chirpw.1
new file mode 100644
index 0000000..ba9b9e8
--- /dev/null
+++ b/share/chirpw.1
@@ -0,0 +1,46 @@
+.\"                                      Hey, EMACS: -*- nroff -*-
+.\" First parameter, NAME, should be all caps
+.\" Second parameter, SECTION, should be 1-8, maybe w/ subsection
+.\" other parameters are allowed: see man(7), man(1)
+.TH CHIRP 1 "February 12, 2011"
+.\" Please adjust this date whenever revising the manpage.
+.\"
+.\" Some roff macros, for reference:
+.\" .nh        disable hyphenation
+.\" .hy        enable hyphenation
+.\" .ad l      left justify
+.\" .ad b      justify to both left and right margins
+.\" .nf        disable filling
+.\" .fi        enable filling
+.\" .br        insert line break
+.\" .sp <n>    insert n+1 empty lines
+.\" for manpage-specific macros, see man(7)
+.SH NAME
+chirpw \- A tool for programming two-way radio equipment
+.SH SYNOPSIS
+.B chirpw
+.RI [ options ]
+.br
+.SH DESCRIPTION
+This manual page documents briefly the
+.B chirpw
+command.
+.PP
+\fBchirpw\fP is a tool for programming two-way radio equipment
+It provides a generic user interface to the programming data and
+process that can drive many radio models under the hood.
+.SH OPTIONS
+This program follows the usual GNU command line syntax, with long
+options starting with two dashes (`--').
+A summary of options is included below.
+.TP
+.B \-\-help
+Show summary of options.
+.TP
+.B \-\-profile
+Enable Profiling.
+.SH AUTHOR
+chirpw was written by Dan Smith.
+.PP
+This manual page was written by Dan Smith (with help from Steve Conklin),
+for the Debian project (and may be used by others).
diff --git a/stock_configs/EU LPD and PMR Channels.csv b/stock_configs/EU LPD and PMR Channels.csv
new file mode 100644
index 0000000..0595902
--- /dev/null
+++ b/stock_configs/EU LPD and PMR Channels.csv	
@@ -0,0 +1,78 @@
+Location,Name,Frequency,Duplex,Offset,Tone,rToneFreq,cToneFreq,DtcsCode,DtcsPolarity,Mode,TStep,Skip,Comment,URCALL,RPT1CALL,RPT2CALL
+1,LPD 01,433.075000,,0.600000,,88.5,88.5,023,NN,FM,5.00,,,,,,
+2,LPD 02,433.100000,,0.600000,,88.5,88.5,023,NN,FM,5.00,,,,,,
+3,LPD 03,433.125000,,0.600000,,88.5,88.5,023,NN,FM,5.00,,,,,,
+4,LPD 04,433.150000,,0.600000,,88.5,88.5,023,NN,FM,5.00,,,,,,
+5,LPD 05,433.175000,,0.600000,,88.5,88.5,023,NN,FM,5.00,,,,,,
+6,LPD 06,433.200000,,0.600000,,88.5,88.5,023,NN,FM,5.00,,,,,,
+7,LPD 07,433.225000,,0.600000,,88.5,88.5,023,NN,FM,5.00,,,,,,
+8,LPD 08,433.250000,,0.600000,,88.5,88.5,023,NN,FM,5.00,,,,,,
+9,LPD 09,433.275000,,0.600000,,88.5,88.5,023,NN,FM,5.00,,,,,,
+10,LPD 10,433.300000,,0.600000,,88.5,88.5,023,NN,FM,5.00,,,,,,
+11,LPD 11,433.325000,,0.600000,,88.5,88.5,023,NN,FM,5.00,,,,,,
+12,LPD 12,433.350000,,0.600000,,88.5,88.5,023,NN,FM,5.00,,,,,,
+13,LPD 13,433.375000,,0.600000,,88.5,88.5,023,NN,FM,5.00,,,,,,
+14,LPD 14,433.400000,,0.600000,,88.5,88.5,023,NN,FM,5.00,,,,,,
+15,LPD 15,433.425000,,0.600000,,88.5,88.5,023,NN,FM,5.00,,,,,,
+16,LPD 16,433.450000,,0.600000,,88.5,88.5,023,NN,FM,5.00,,,,,,
+17,LPD 17,433.475000,,0.600000,,88.5,88.5,023,NN,FM,5.00,,,,,,
+18,LPD 18,433.500000,,0.600000,,88.5,88.5,023,NN,FM,5.00,,,,,,
+19,LPD 19,433.525000,,0.600000,,88.5,88.5,023,NN,FM,5.00,,,,,,
+20,LPD 20,433.550000,,0.600000,,88.5,88.5,023,NN,FM,5.00,,,,,,
+21,LPD 21,433.575000,,0.600000,,88.5,88.5,023,NN,FM,5.00,,,,,,
+22,LPD 22,433.600000,,0.600000,,88.5,88.5,023,NN,FM,5.00,,,,,,
+23,LPD 23,433.625000,,0.600000,,88.5,88.5,023,NN,FM,5.00,,,,,,
+24,LPD 24,433.650000,,0.600000,,88.5,88.5,023,NN,FM,5.00,,,,,,
+25,LPD 25,433.675000,,0.600000,,88.5,88.5,023,NN,FM,5.00,,,,,,
+26,LPD 26,433.700000,,0.600000,,88.5,88.5,023,NN,FM,5.00,,,,,,
+27,LPD 27,433.725000,,0.600000,,88.5,88.5,023,NN,FM,5.00,,,,,,
+28,LPD 28,433.750000,,0.600000,,88.5,88.5,023,NN,FM,5.00,,,,,,
+29,LPD 29,433.775000,,0.600000,,88.5,88.5,023,NN,FM,5.00,,,,,,
+30,LPD 30,433.800000,,0.600000,,88.5,88.5,023,NN,FM,5.00,,,,,,
+31,LPD 31,433.825000,,0.600000,,88.5,88.5,023,NN,FM,5.00,,,,,,
+32,LPD 32,433.850000,,0.600000,,88.5,88.5,023,NN,FM,5.00,,,,,,
+33,LPD 33,433.875000,,0.600000,,88.5,88.5,023,NN,FM,5.00,,,,,,
+34,LPD 34,433.900000,,0.600000,,88.5,88.5,023,NN,FM,5.00,,,,,,
+35,LPD 35,433.925000,,0.600000,,88.5,88.5,023,NN,FM,5.00,,,,,,
+36,LPD 36,433.950000,,0.600000,,88.5,88.5,023,NN,FM,5.00,,,,,,
+37,LPD 37,433.975000,,0.600000,,88.5,88.5,023,NN,FM,5.00,,,,,,
+38,LPD 38,434.000000,,0.600000,,88.5,88.5,023,NN,FM,5.00,,,,,,
+39,LPD 39,434.025000,,0.600000,,88.5,88.5,023,NN,FM,5.00,,,,,,
+40,LPD 40,434.050000,,0.600000,,88.5,88.5,023,NN,FM,5.00,,,,,,
+41,LPD 41,434.075000,,0.600000,,88.5,88.5,023,NN,FM,5.00,,,,,,
+42,LPD 42,434.100000,,0.600000,,88.5,88.5,023,NN,FM,5.00,,,,,,
+43,LPD 43,434.125000,,0.600000,,88.5,88.5,023,NN,FM,5.00,,,,,,
+44,LPD 44,434.150000,,0.600000,,88.5,88.5,023,NN,FM,5.00,,,,,,
+45,LPD 45,434.175000,,0.600000,,88.5,88.5,023,NN,FM,5.00,,,,,,
+46,LPD 46,434.200000,,0.600000,,88.5,88.5,023,NN,FM,5.00,,,,,,
+47,LPD 47,434.225000,,0.600000,,88.5,88.5,023,NN,FM,5.00,,,,,,
+48,LPD 48,434.250000,,0.600000,,88.5,88.5,023,NN,FM,5.00,,,,,,
+49,LPD 49,434.275000,,0.600000,,88.5,88.5,023,NN,FM,5.00,,,,,,
+50,LPD 50,434.300000,,0.600000,,88.5,88.5,023,NN,FM,5.00,,,,,,
+51,LPD 51,434.325000,,0.600000,,88.5,88.5,023,NN,FM,5.00,,,,,,
+52,LPD 52,434.350000,,0.600000,,88.5,88.5,023,NN,FM,5.00,,,,,,
+53,LPD 53,434.375000,,0.600000,,88.5,88.5,023,NN,FM,5.00,,,,,,
+54,LPD 54,434.400000,,0.600000,,88.5,88.5,023,NN,FM,5.00,,,,,,
+55,LPD 55,434.425000,,0.600000,,88.5,88.5,023,NN,FM,5.00,,,,,,
+56,LPD 56,434.450000,,0.600000,,88.5,88.5,023,NN,FM,5.00,,,,,,
+57,LPD 57,434.475000,,0.600000,,88.5,88.5,023,NN,FM,5.00,,,,,,
+58,LPD 58,434.500000,,0.600000,,88.5,88.5,023,NN,FM,5.00,,,,,,
+59,LPD 59,434.525000,,0.600000,,88.5,88.5,023,NN,FM,5.00,,,,,,
+60,LPD 60,434.550000,,0.600000,,88.5,88.5,023,NN,FM,5.00,,,,,,
+61,LPD 61,434.575000,,0.600000,,88.5,88.5,023,NN,FM,5.00,,,,,,
+62,LPD 62,434.600000,,0.600000,,88.5,88.5,023,NN,FM,5.00,,,,,,
+63,LPD 63,434.625000,,0.600000,,88.5,88.5,023,NN,FM,5.00,,,,,,
+64,LPD 64,434.650000,,0.600000,,88.5,88.5,023,NN,FM,5.00,,,,,,
+65,LPD 65,434.675000,,0.600000,,88.5,88.5,023,NN,FM,5.00,,,,,,
+66,LPD 66,434.700000,,0.600000,,88.5,88.5,023,NN,FM,5.00,,,,,,
+67,LPD 67,434.725000,,0.600000,,88.5,88.5,023,NN,FM,5.00,,,,,,
+68,LPD 68,434.750000,,0.600000,,88.5,88.5,023,NN,FM,5.00,,,,,,
+69,LPD 69,434.775000,,0.600000,,88.5,88.5,023,NN,FM,5.00,,,,,,
+71,PMR 1,446.006250,,0.600000,,88.5,88.5,023,NN,NFM,6.25,,,,,,
+72,PMR 2,446.018750,,0.600000,,88.5,88.5,023,NN,NFM,6.25,,,,,,
+73,PMR 3,446.031250,,0.600000,,88.5,88.5,023,NN,NFM,6.25,,,,,,
+74,PMR 4,446.043750,,0.600000,,88.5,88.5,023,NN,NFM,6.25,,,,,,
+75,PMR 5,446.056250,,0.600000,,88.5,88.5,023,NN,NFM,6.25,,,,,,
+76,PMR 6,446.068750,,0.600000,,88.5,88.5,023,NN,NFM,6.25,,,,,,
+77,PMR 7,446.081250,,0.600000,,88.5,88.5,023,NN,NFM,6.25,,,,,,
+78,PMR 8,446.093750,,0.600000,,88.5,88.5,023,NN,NFM,6.25,,,,,,
diff --git a/stock_configs/Marine VHF Channels.csv b/stock_configs/Marine VHF Channels.csv
new file mode 100644
index 0000000..82c6cc2
--- /dev/null
+++ b/stock_configs/Marine VHF Channels.csv	
@@ -0,0 +1,61 @@
+Location,Name,Frequency,Duplex,Offset,Tone,rToneFreq,cToneFreq,DtcsCode,DtcsPolarity,Mode,TStep,Skip,Comment,URCALL,RPT1CALL,RPT2CALL
+0,MVHF  L1,155.500000,,0.000000,,88.5,88.5,023,NN,FM,25.00,,,,,,
+1,MVHF  L2,155.525000,,0.000000,,88.5,88.5,023,NN,FM,25.00,,,,,,
+2,MVHF  F1,155.625000,,0.000000,,88.5,88.5,023,NN,FM,25.00,,,,,,
+3,MVHF  F2,155.775000,,0.000000,,88.5,88.5,023,NN,FM,25.00,,,,,,
+4,MVHF  F3,155.825000,,0.000000,,88.5,88.5,023,NN,FM,25.00,,,,,,
+5,MVHF K06,156.300000,,0.000000,,88.5,88.5,023,NN,FM,25.00,,,,,,
+6,MVHF K67,156.375000,,0.000000,,88.5,88.5,023,NN,FM,25.00,,,,,,
+7,MVHF K08,156.400000,,0.000000,,88.5,88.5,023,NN,FM,25.00,,,,,,
+8,MVHF K68,156.425000,,0.000000,,88.5,88.5,023,NN,FM,25.00,,,,,,
+9,MVHF K09,156.450000,,0.000000,,88.5,88.5,023,NN,FM,25.00,,,,,,
+10,MVHF K69,156.475000,,0.000000,,88.5,88.5,023,NN,FM,25.00,,,,,,
+11,MVHF K10,156.500000,,0.000000,,88.5,88.5,023,NN,FM,25.00,,,,,,
+12,MDSC K70,156.525000,,0.000000,,88.5,88.5,023,NN,FM,25.00,,,,,,
+13,MVHF K11,156.550000,,0.000000,,88.5,88.5,023,NN,FM,25.00,,,,,,
+14,MVHF K71,156.575000,,0.000000,,88.5,88.5,023,NN,FM,25.00,,,,,,
+15,MVHF K12,156.600000,,0.000000,,88.5,88.5,023,NN,FM,25.00,,,,,,
+16,MVHF K72,156.625000,,0.000000,,88.5,88.5,023,NN,FM,25.00,,,,,,
+17,MVHF K13,156.650000,,0.000000,,88.5,88.5,023,NN,FM,25.00,,,,,,
+18,MVHF K73,156.675000,,0.000000,,88.5,88.5,023,NN,FM,25.00,,,,,,
+19,MVHF K14,156.700000,,0.000000,,88.5,88.5,023,NN,FM,25.00,,,,,,
+20,MVHF K74,156.725000,,0.000000,,88.5,88.5,023,NN,FM,25.00,,,,,,
+21,MVHF K15,156.750000,,0.000000,,88.5,88.5,023,NN,FM,25.00,,,,,,
+22,MVHF K16,156.800000,,0.000000,,88.5,88.5,023,NN,FM,25.00,,,,,,
+23,MVHF K17,156.850000,,0.000000,,88.5,88.5,023,NN,FM,25.00,,,,,,
+24,MVHF K77,156.875000,,0.000000,,88.5,88.5,023,NN,FM,25.00,,,,,,
+25,MVHF K60,160.625000,-,4.600000,,88.5,88.5,023,NN,FM,25.00,S,,,,,
+26,MVHF K01,160.650000,-,4.600000,,88.5,88.5,023,NN,FM,25.00,S,,,,,
+27,MVHF K61,160.675000,-,4.600000,,88.5,88.5,023,NN,FM,25.00,S,,,,,
+28,MVHF K02,160.700000,-,4.600000,,88.5,88.5,023,NN,FM,25.00,S,,,,,
+29,MVHF K62,160.725000,-,4.600000,,88.5,88.5,023,NN,FM,25.00,S,,,,,
+30,MVHF K03,160.750000,-,4.600000,,88.5,88.5,023,NN,FM,25.00,S,,,,,
+31,MVHF K63,160.775000,-,4.600000,,88.5,88.5,023,NN,FM,25.00,S,,,,,
+32,MVHF K04,160.800000,-,4.600000,,88.5,88.5,023,NN,FM,25.00,S,,,,,
+33,MVHF K64,160.825000,-,4.600000,,88.5,88.5,023,NN,FM,25.00,S,,,,,
+34,MVHF K05,160.850000,-,4.600000,,88.5,88.5,023,NN,FM,25.00,S,,,,,
+35,MVHF K65,160.875000,-,4.600000,,88.5,88.5,023,NN,FM,25.00,S,,,,,
+36,MVHF K66,160.925000,-,4.600000,,88.5,88.5,023,NN,FM,25.00,S,,,,,
+37,MVHF K07,160.950000,-,4.600000,,88.5,88.5,023,NN,FM,25.00,S,,,,,
+38,MVHF K18,161.500000,-,4.600000,,88.5,88.5,023,NN,FM,25.00,S,,,,,
+39,MVHF K78,161.525000,-,4.600000,,88.5,88.5,023,NN,FM,25.00,S,,,,,
+40,MVHF K19,161.550000,-,4.600000,,88.5,88.5,023,NN,FM,25.00,S,,,,,
+41,MVHF K79,161.575000,-,4.600000,,88.5,88.5,023,NN,FM,25.00,S,,,,,
+42,MVHF K20,161.600000,-,4.600000,,88.5,88.5,023,NN,FM,25.00,,,,,,
+43,MVHF K80,161.625000,-,4.600000,,88.5,88.5,023,NN,FM,25.00,S,,,,,
+44,MVHF K21,161.650000,-,4.600000,,88.5,88.5,023,NN,FM,25.00,S,,,,,
+45,MVHF K81,161.675000,-,4.600000,,88.5,88.5,023,NN,FM,25.00,S,,,,,
+46,MVHF K22,161.700000,-,4.600000,,88.5,88.5,023,NN,FM,25.00,S,,,,,
+47,MVHF K82,161.725000,-,4.600000,,88.5,88.5,023,NN,FM,25.00,S,,,,,
+48,MVHF K23,161.750000,-,4.600000,,88.5,88.5,023,NN,FM,25.00,S,,,,,
+49,MVHF K83,161.775000,-,4.600000,,88.5,88.5,023,NN,FM,25.00,S,,,,,
+50,MVHF K24,161.800000,-,4.600000,,88.5,88.5,023,NN,FM,25.00,S,,,,,
+51,MVHF K84,161.825000,-,4.600000,,88.5,88.5,023,NN,FM,25.00,S,,,,,
+52,MVHF K25,161.850000,-,4.600000,,88.5,88.5,023,NN,FM,25.00,S,,,,,
+53,MVHF K85,161.875000,-,4.600000,,88.5,88.5,023,NN,FM,25.00,S,,,,,
+54,MVHF K26,161.900000,-,4.600000,,88.5,88.5,023,NN,FM,25.00,S,,,,,
+55,MVHF K86,161.925000,-,4.600000,,88.5,88.5,023,NN,FM,25.00,S,,,,,
+56,MVHF K27,161.950000,-,4.600000,,88.5,88.5,023,NN,FM,25.00,S,,,,,
+57,MAIS K87,161.975000,-,4.600000,,88.5,88.5,023,NN,FM,25.00,S,,,,,
+58,MVHF K28,162.000000,-,4.600000,,88.5,88.5,023,NN,FM,25.00,S,,,,,
+59,MAIS K88,162.025000,-,4.600000,,88.5,88.5,023,NN,FM,25.00,S,,,,,
diff --git a/stock_configs/NOAA Weather Alert.csv b/stock_configs/NOAA Weather Alert.csv
new file mode 100644
index 0000000..ae1564a
--- /dev/null
+++ b/stock_configs/NOAA Weather Alert.csv	
@@ -0,0 +1,8 @@
+Location,Name,Frequency,Duplex,Offset,Tone,rToneFreq,cToneFreq,DtcsCode,DtcsPolarity,Mode,TStep,Skip,Comment,URCALL,RPT1CALL,RPT2CALL
+1,NOAA1,162.4,,0,,88.5,88.5,23,NN,FM,5,,,,,
+2,NOAA2,162.425,,0,,88.5,88.5,23,NN,FM,5,,,,,
+3,NOAA3,162.45,,0,,88.5,88.5,23,NN,FM,5,,,,,
+4,NOAA4,162.475,,0,,88.5,88.5,23,NN,FM,5,,,,,
+5,NOAA5,162.5,,0,,88.5,88.5,23,NN,FM,5,,,,,
+6,NOAA6,162.525,,0,,88.5,88.5,23,NN,FM,5,,,,,
+7,NOAA7,162.55,,0,,88.5,88.5,23,NN,FM,5,,,,,
diff --git a/stock_configs/US 60 meter channels (Center).csv b/stock_configs/US 60 meter channels (Center).csv
new file mode 100644
index 0000000..c650a3b
--- /dev/null
+++ b/stock_configs/US 60 meter channels (Center).csv	
@@ -0,0 +1,6 @@
+Location,Name,Frequency,Duplex,Offset,Tone,rToneFreq,cToneFreq,DtcsCode,DtcsPolarity,Mode,TStep,Skip,URCALL,RPT1CALL,RPT2CALL
+1,60m CH1,5.332000,,0.600000,,88.5,88.5,023,NN,USB,5.00,,,,,
+2,60m CH2,5.348000,,0.600000,,88.5,88.5,023,NN,USB,5.00,,,,,
+3,60m CH3,5.358500,,0.600000,,88.5,88.5,023,NN,USB,5.00,,,,,
+4,60m CH4,5.373000,,0.600000,,88.5,88.5,023,NN,USB,5.00,,,,,
+5,60m CH5,5.405000,,0.600000,,88.5,88.5,023,NN,USB,5.00,,,,,
diff --git a/stock_configs/US 60 meter channels (Dial).csv b/stock_configs/US 60 meter channels (Dial).csv
new file mode 100644
index 0000000..37c5eda
--- /dev/null
+++ b/stock_configs/US 60 meter channels (Dial).csv	
@@ -0,0 +1,6 @@
+Location,Name,Frequency,Duplex,Offset,Tone,rToneFreq,cToneFreq,DtcsCode,DtcsPolarity,Mode,TStep,Skip,URCALL,RPT1CALL,RPT2CALL
+1,60m CH1,5.330500,,0.600000,,88.5,88.5,023,NN,USB,5.00,,,,,
+2,60m CH2,3.346500,,0.600000,,88.5,88.5,023,NN,USB,5.00,,,,,
+3,60m CH3,5.357000,,0.600000,,88.5,88.5,023,NN,USB,5.00,,,,,
+4,60m CH4,5.371500,,0.600000,,88.5,88.5,023,NN,USB,5.00,,,,,
+5,60m CH5,5.403500,,0.600000,,88.5,88.5,023,NN,USB,5.00,,,,,
diff --git a/stock_configs/US Calling Frequencies.csv b/stock_configs/US Calling Frequencies.csv
new file mode 100644
index 0000000..2138a05
--- /dev/null
+++ b/stock_configs/US Calling Frequencies.csv	
@@ -0,0 +1,5 @@
+Location,Name,Frequency,Duplex,Offset,Tone,rToneFreq,cToneFreq,DtcsCode,DtcsPolarity,Mode,TStep,Skip,URCALL,RPT1CALL,RPT2CALL
+1,6m Call,52.525000,-,0.500000,,88.5,88.5,023,NN,FM,5.00,,,,,
+2,2m Call,146.520000,,0.000000,,88.5,88.5,023,NN,FM,5.00,,,,,
+3,220 Call,223.500000,,0.000000,,88.5,88.5,023,NN,FM,5.00,,,,,
+4,70cm Call,446.000000,,0.000000,,88.5,88.5,023,NN,FM,5.00,,,,,
diff --git a/stock_configs/US FRS and GMRS Channels.csv b/stock_configs/US FRS and GMRS Channels.csv
new file mode 100644
index 0000000..bfa250a
--- /dev/null
+++ b/stock_configs/US FRS and GMRS Channels.csv	
@@ -0,0 +1,23 @@
+Location,Name,Frequency,Duplex,Offset,Tone,rToneFreq,cToneFreq,DtcsCode,DtcsPolarity,Mode,TStep,Skip,Comment,URCALL,RPT1CALL,RPT2CALL
+1,FRS1,462.562500,,5.000000,,88.5,88.5,023,NN,NFM,12.50,,,,,,
+2,FRS2,462.587500,,5.000000,,88.5,88.5,023,NN,NFM,12.50,,,,,,
+3,FRS3,462.612500,,5.000000,,88.5,88.5,023,NN,NFM,12.50,,,,,,
+4,FRS4,462.637500,,5.000000,,88.5,88.5,023,NN,NFM,12.50,,,,,,
+5,FRS5,462.662500,,5.000000,,88.5,88.5,023,NN,NFM,12.50,,,,,,
+6,FRS6,462.687500,,5.000000,,88.5,88.5,023,NN,NFM,12.50,,,,,,
+7,FRS7,462.712500,,5.000000,,88.5,88.5,023,NN,NFM,12.50,,,,,,
+8,FRS8,467.562500,,5.000000,,88.5,88.5,023,NN,NFM,12.50,,,,,,
+9,FRS9,467.587500,,5.000000,,88.5,88.5,023,NN,NFM,12.50,,,,,,
+10,FRS10,467.612500,,5.000000,,88.5,88.5,023,NN,NFM,12.50,,,,,,
+11,FRS11,467.637500,,5.000000,,88.5,88.5,023,NN,NFM,12.50,,,,,,
+12,FRS12,467.662500,,5.000000,,88.5,88.5,023,NN,NFM,12.50,,,,,,
+13,FRS13,467.687500,,5.000000,,88.5,88.5,023,NN,NFM,12.50,,,,,,
+14,FRS14,467.712500,,5.000000,,88.5,88.5,023,NN,NFM,12.50,,,,,,
+15,GMRS1,462.550000,,5.000000,,88.5,88.5,023,NN,FM,25.00,,,,,,
+16,GMRS2,462.575000,,5.000000,,88.5,88.5,023,NN,FM,25.00,,,,,,
+17,GMRS3,462.600000,,5.000000,,88.5,88.5,023,NN,FM,25.00,,,,,,
+18,GMRS4,462.625000,,5.000000,,88.5,88.5,023,NN,FM,25.00,,,,,,
+19,GMRS5,462.650000,,5.000000,,88.5,88.5,023,NN,FM,25.00,,,,,,
+20,GMRS6,462.675000,,5.000000,,88.5,88.5,023,NN,FM,25.00,,,,,,
+21,GMRS7,462.700000,,5.000000,,88.5,88.5,023,NN,FM,25.00,,,,,,
+22,GMRS8,462.725000,,5.000000,,88.5,88.5,023,NN,FM,25.00,,,,,,
diff --git a/stock_configs/US MURS Channels.csv b/stock_configs/US MURS Channels.csv
new file mode 100644
index 0000000..4e5384c
--- /dev/null
+++ b/stock_configs/US MURS Channels.csv	
@@ -0,0 +1,6 @@
+Location,Name,Frequency,Duplex,Offset,Tone,rToneFreq,cToneFreq,DtcsCode,DtcsPolarity,Mode,TStep,Skip,Comment,URCALL,RPT1CALL,RPT2CALL
+1,MURS 1,151.820000,,0.000000,,88.5,88.5,023,NN,NFM,5.00,,,,,,
+2,MURS 2,151.880000,,0.000000,,88.5,88.5,023,NN,NFM,5.00,,,,,,
+3,MURS 3,151.940000,,0.000000,,88.5,88.5,023,NN,NFM,5.00,,,,,,
+4,Blue Dot,154.570000,,0.000000,,88.5,88.5,023,NN,FM,5.00,,,,,,
+5,Green Dot,154.600000,,0.000000,,88.5,88.5,023,NN,FM,5.00,,,,,,

-- 
Alioth's /usr/local/bin/git-commit-notice on /srv/git.debian.org/git/pkg-hamradio/chirp.git



More information about the pkg-hamradio-commits mailing list