[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