chirp-20261030/0000755000175000017500000000000015271057530012037 5ustar hibbyhibbychirp-20261030/COPYING0000644000175000017500000010451314411666130013073 0ustar hibbyhibby GNU GENERAL PUBLIC LICENSE Version 3, 29 June 2007 Copyright (C) 2007 Free Software Foundation, Inc. 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. Copyright (C) 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 . 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: Copyright (C) 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 . 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 . chirp-20261030/MANIFEST.in0000644000175000017500000000050614665027664013611 0ustar hibbyhibbyinclude chirp/share/*.desktop include chirp/share/*.metainfo.xml include chirp/share/*.png include chirp/share/*.svg include chirp/share/*.ico include chirp/share/*.1 include chirp/stock_configs/* include chirp/locale/*/LC_MESSAGES/*.mo exclude chirp/locale/* include COPYING include *requirements.txt graft tests graft tools chirp-20261030/PKG-INFO0000644000175000017500000000075515271057530013143 0ustar hibbyhibbyMetadata-Version: 2.4 Name: chirp Version: 20261030 Summary: A cross-platform cross-radio programming tool Home-page: https://chirp.danplanet.com Requires-Python: >=3.10,<4 License-File: COPYING Requires-Dist: pyserial Requires-Dist: requests Requires-Dist: yattag Requires-Dist: suds Requires-Dist: lark Provides-Extra: wx Requires-Dist: wxPython; extra == "wx" Dynamic: home-page Dynamic: license-file Dynamic: provides-extra Dynamic: requires-dist Dynamic: requires-python Dynamic: summary chirp-20261030/README.md0000644000175000017500000000033114665027664013326 0ustar hibbyhibby# CHIRP Project This is the official git repository for the __[CHIRP](https://www.chirpmyradio.com)__ project. When submitting PRs, please see [this file](.github/pull_request_template.md) for rules and guidelines. chirp-20261030/chirp/0000755000175000017500000000000015271057527013152 5ustar hibbyhibbychirp-20261030/chirp/__init__.py0000644000175000017500000000004015271057526015254 0ustar hibbyhibbyCHIRP_VERSION = "next-20261030" chirp-20261030/chirp/bandplan.py0000644000175000017500000001665614767271777015337 0ustar hibbyhibby# Copyright 2013 Sean Burford # # 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 . import logging from chirp import chirp_common LOG = logging.getLogger(__name__) class Band(object): def __init__(self, limits, name, mode=None, step_khz=None, input_offset=None, output_offset=None, tones=None, duplex=None): # Apply semantic and chirp limitations to settings. # memedit applies radio limitations when settings are applied. try: assert limits[0] <= limits[1], "Lower freq > upper freq" if mode is not None: assert mode in chirp_common.MODES, "Mode %s not one of %s" % ( mode, chirp_common.MODES) if step_khz is not None: assert step_khz in chirp_common.TUNING_STEPS, ( "step_khz %s not one of %s" % (step_khz, chirp_common.TUNING_STEPS)) if tones: for tone in tones: assert tone in chirp_common.TONES, ( "tone %s not one of %s" % (tone, chirp_common.TONES)) except AssertionError as e: raise ValueError("%s %s: %s" % (name, limits, e)) self.name = name self.mode = mode self.step_khz = step_khz self.tones = tones self.limits = limits self.offset = input_offset self.duplex = duplex if duplex is None and self.offset: self.duplex = '+' if self.offset > 0 else '-' def __eq__(self, other): return (other.limits[0] == self.limits[0] and other.limits[1] == self.limits[1]) def contains(self, other): return (other.limits[0] >= self.limits[0] and other.limits[1] <= self.limits[1]) def width(self): return self.limits[1] - self.limits[0] def inverse(self): """Create an RX/TX shadow of this band using the offset.""" if not self.offset: return self limits = (self.limits[0] + self.offset, self.limits[1] + self.offset) offset = -1 * self.offset if self.duplex and self.duplex in '+-': duplex = '+' if self.duplex == '-' else '-' return Band(limits, self.name, self.mode, self.step_khz, input_offset=offset, tones=self.tones, duplex=duplex) return Band(limits, self.name, self.mode, self.step_khz, output_offset=offset, tones=self.tones) def __repr__(self): desc = '%s%s%s%s' % ( self.mode and 'mode: %s ' % (self.mode,) or '', self.step_khz and 'step_khz: %s ' % (self.step_khz,) or '', self.offset and 'offset: %s ' % (self.offset,) or '', self.tones and 'tones: %s ' % (self.tones,) or '') return "%s-%s %s %s %s" % ( self.limits[0], self.limits[1], self.name, self.duplex, desc) class BandPlans(object): def __init__(self, config): self._config = config self.plans = {} # Migrate old "automatic repeater offset" setting to # "North American Amateur Band Plan" ro = self._config.get("autorpt", "memedit") if ro is not None: self._config.set_bool("north_america", ro, "bandplan") self._config.remove_option("autorpt", "memedit") # And default new users to North America. if not self._config.is_defined("north_america", "bandplan"): self._config.set_bool("north_america", True, "bandplan") from chirp import bandplan_na, bandplan_au from chirp import bandplan_iaru_r1, bandplan_iaru_r2, bandplan_iaru_r3 for plan in (bandplan_na, bandplan_au, bandplan_iaru_r1, bandplan_iaru_r2, bandplan_iaru_r3): name = plan.DESC.get("name", plan.SHORTNAME) self.plans[plan.SHORTNAME] = (name, plan) rpt_inputs = [] for band in plan.BANDS: # Add repeater inputs. rpt_input = band.inverse() if rpt_input not in plan.BANDS: rpt_inputs.append(band.inverse()) plan.bands = list(plan.BANDS) plan.bands.extend(rpt_inputs) def get_defaults_for_frequency(self, freq): freq = int(freq) result = Band((freq, freq), repr(freq)) for shortname, details in self.plans.items(): if self._config.get_bool(shortname, "bandplan"): matches = [x for x in details[1].bands if x.contains(result)] # Add matches to defaults, favoring more specific matches. matches = sorted(matches, key=lambda x: x.width(), reverse=True) for match in matches: result.mode = match.mode or result.mode result.step_khz = match.step_khz or result.step_khz result.offset = match.offset or result.offset result.duplex = (match.duplex if match.duplex is not None else result.duplex) result.tones = match.tones or result.tones if match.name: result.name = '/'.join((result.name or '', match.name)) # Limit ourselves to one band plan match for simplicity. # Note that if the user selects multiple band plans by editing # the config file it will work as expected (except where plans # conflict). if matches: break return result def get_enabled_plan(self): for shortname, details in self.plans.items(): if self._config.get_bool(shortname, "bandplan"): return details[1] def get_repeater_bands(self): bands_with_repeaters = [] current_plan = self.get_enabled_plan() # For now, assume anything above 28 MHz could have a repeater. # Alternately, we could scan for bands with repeater sub-bands and only # include those. min_freq = chirp_common.to_MHz(28) def add_nodupes(b): for existing in bands_with_repeaters: if b.name == existing.name: if sum(b.limits) < sum(existing.limits): # Don't add, this is smaller return else: bands_with_repeaters.remove(existing) break bands_with_repeaters.append(b) for band in current_plan.bands: if (band.limits[0] >= min_freq and (band.name.lower().endswith('meter band') or band.name.lower().endswith('cm band'))): add_nodupes(band) return sorted(bands_with_repeaters, key=lambda b: b.limits[0]) BANDS_AIR = ( Band((118000000, 136975000), "Aviation", mode="AM"), ) chirp-20261030/chirp/bandplan_au.py0000644000175000017500000001121114720054223015750 0ustar hibbyhibby# Copyright 2013 Sean Burford # # 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 . from chirp import bandplan, bandplan_iaru_r3 SHORTNAME = "australia" DESC = { "name": "Australian Amateur Band Plan", "updated": "April 2010", "url": "http://www.wia.org.au/members/bandplans/data" "/documents/Australian%20Band%20Plans%20100404.pdf", } BANDS_10M = ( # bandplan.Band((28000000, 29700000), "10 Meter Band"), bandplan.Band((29520000, 29680000), "FM Simplex and Repeaters", mode="FM", step_khz=20), bandplan.Band((29620000, 29680000), "FM Repeaters", input_offset=-100000), ) BANDS_6M = ( # bandplan.Band((50000000, 54000000), "6 Meter Band"), bandplan.Band((52525000, 53975000), "FM Simplex and Repeaters", mode="FM", step_khz=25), bandplan.Band((53550000, 53975000), "FM Repeaters", input_offset=-1000000), ) BANDS_2M = ( bandplan.Band((144000000, 148000000), "2 Meter Band", tones=(91.5, 123.0, 141.3, 146.2, 85.4)), bandplan.Band((144400000, 144600000), "Beacons", step_khz=1), bandplan.Band((146025000, 147975000), "FM Simplex and Repeaters", mode="FM", step_khz=25), bandplan.Band((146625000, 147000000), "FM Repeaters Group A", input_offset=-600000), bandplan.Band((147025000, 147375000), "FM Repeaters Group B", input_offset=600000), ) BANDS_70CM = ( bandplan.Band((420000000, 450000000), "70cm Band", tones=(91.5, 123.0, 141.3, 146.2, 85.4)), bandplan.Band((432400000, 432600000), "Beacons", step_khz=1), bandplan.Band((438025000, 439975000), "FM Simplex and Repeaters", mode="FM", step_khz=25), bandplan.Band((438025000, 438725000), "FM Repeaters Group A", input_offset=-5000000), bandplan.Band((439275000, 439975000), "FM Repeaters Group B", input_offset=-5000000), ) BANDS_23CM = ( # bandplan.Band((1240000000, 1300000000), "23cm Band"), bandplan.Band((1273025000, 1273975000), "FM Repeaters", mode="FM", step_khz=25, input_offset=20000000), bandplan.Band((1296400000, 1296600000), "Beacons", step_khz=1), bandplan.Band((1297025000, 1300400000), "General FM Simplex Data", mode="FM", step_khz=25), ) BANDS_13CM = ( bandplan.Band((2300000000, 2450000000), "13cm Band"), bandplan.Band((2403400000, 2403600000), "Beacons", step_khz=1), bandplan.Band((2425000000, 2428000000), "FM Simplex", mode="FM", step_khz=25), bandplan.Band((2428025000, 2429000000), "FM Duplex (Voice)", mode="FM", step_khz=25, input_offset=20000000), bandplan.Band((2429000000, 2429975000), "FM Duplex (Data)", mode="FM", step_khz=100, input_offset=20000000), ) BANDS_9CM = ( bandplan.Band((3300000000, 3600000000), "9cm Band"), bandplan.Band((3320000000, 3340000000), "WB Channel 2: Voice/Data", step_khz=100), bandplan.Band((3400400000, 3400600000), "Beacons", step_khz=1), bandplan.Band((3402000000, 3403000000), "FM Simplex (Voice)", mode="FM", step_khz=100), bandplan.Band((3403000000, 3405000000), "FM Simplex (Data)", mode="FM", step_khz=100), ) BANDS_6CM = ( bandplan.Band((5650000000, 5850000000), "6cm Band"), bandplan.Band((5760400000, 5760600000), "Beacons", step_khz=1), bandplan.Band((5700000000, 5720000000), "WB Channel 2: Data", step_khz=100, input_offset=70000000), bandplan.Band((5720000000, 5740000000), "WB Channel 3: Voice", step_khz=100, input_offset=70000000), bandplan.Band((5762000000, 5763000000), "FM Simplex (Voice)", mode="FM", step_khz=100), bandplan.Band((5763000000, 5765000000), "FM Simplex (Data)", mode="FM", step_khz=100), ) BANDS = bandplan_iaru_r3.BANDS_20M + bandplan_iaru_r3.BANDS_17M + \ bandplan_iaru_r3.BANDS_15M + bandplan_iaru_r3.BANDS_12M + \ bandplan_iaru_r3.BANDS_10M + bandplan_iaru_r3.BANDS_6M + \ BANDS_10M + BANDS_6M + BANDS_2M + BANDS_70CM + BANDS_23CM + \ BANDS_13CM + BANDS_9CM + BANDS_6CM + bandplan.BANDS_AIR chirp-20261030/chirp/bandplan_iaru_r1.py0000644000175000017500000002002515141336500016707 0ustar hibbyhibby# Copyright 2013 Sean Burford # # 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 . from chirp import bandplan SHORTNAME = "iaru_r1" DESC = { "name": "IARU Region 1 (Europe, Africa, Middle East and Northern Asia)", "url": "http://iaru-r1.org/index.php?option=com_content" "&view=article&id=175&Itemid=127", "updated": "General Conference Sun City 2011", } # Bands are broken up like this so that other plans can import bits. BANDS_2100M = ( bandplan.Band((135700, 137800), "137 kHz Band", mode="CW"), ) BANDS_160M = ( bandplan.Band((1810000, 2000000), "160 Meter Band"), bandplan.Band((1810000, 1838000), "CW", mode="CW"), bandplan.Band((1838000, 1840000), "All narrow band modes"), bandplan.Band((1840000, 1843000), "All modes, digimodes", mode="RTTY"), ) BANDS_80M = ( bandplan.Band((3500000, 3800000), "80 Meter Band"), bandplan.Band((3500000, 3510000), "CW, priority for DX", mode="CW"), bandplan.Band((3510000, 3560000), "CW, contest preferred", mode="CW"), bandplan.Band((3560000, 3580000), "CW", mode="CW"), bandplan.Band((3580000, 3600000), "All narrow band modes, digimodes"), bandplan.Band((3590000, 3600000), "All narrow band, digimodes, unattended"), bandplan.Band((3600000, 3650000), "All modes, SSB contest preferred", mode="LSB"), bandplan.Band((3600000, 3700000), "All modes, SSB QRP", mode="LSB"), bandplan.Band((3700000, 3800000), "All modes, SSB contest preferred", mode="LSB"), bandplan.Band((3775000, 3800000), "All modes, SSB DX preferred", mode="LSB"), ) BANDS_40M = ( bandplan.Band((7000000, 7200000), "40 Meter Band"), bandplan.Band((7000000, 7040000), "CW", mode="CW"), bandplan.Band((7040000, 7047000), "All narrow band modes, digimodes"), bandplan.Band((7047000, 7050000), "All narrow band, digimodes, unattended"), bandplan.Band((7050000, 7053000), "All modes, digimodes, unattended"), bandplan.Band((7053000, 7060000), "All modes, digimodes"), bandplan.Band((7060000, 7100000), "All modes, SSB contest preferred", mode="LSB"), bandplan.Band((7100000, 7130000), "All modes, R1 Emergency Center Of Activity", mode="LSB"), bandplan.Band((7130000, 7200000), "All modes, SSB contest preferred", mode="LSB"), bandplan.Band((7175000, 7200000), "All modes, SSB DX preferred", mode="LSB"), ) BANDS_30M = ( bandplan.Band((10100000, 10150000), "30 Meter Band"), bandplan.Band((10100000, 10140000), "CW", mode="CW"), bandplan.Band((10140000, 10150000), "All narrow band digimodes"), ) BANDS_20M = ( bandplan.Band((14000000, 14350000), "20 Meter Band"), bandplan.Band((14000000, 14070000), "CW", mode="CW"), bandplan.Band((14070000, 14099000), "All narrow band modes, digimodes"), bandplan.Band((14099000, 14101000), "IBP, exclusively for beacons", mode="CW"), bandplan.Band((14101000, 14112000), "All narrow band modes, digimodes"), bandplan.Band((14125000, 14350000), "All modes, SSB contest preferred", mode="USB"), bandplan.Band((14300000, 14350000), "All modes, Global Emergency center of activity", mode="USB"), ) BANDS_17M = ( bandplan.Band((18068000, 18168000), "17 Meter Band"), bandplan.Band((18068000, 18095000), "CW", mode="CW"), bandplan.Band((18095000, 18109000), "All narrow band modes, digimodes"), bandplan.Band((18109000, 18111000), "IBP, exclusively for beacons", mode="CW"), bandplan.Band((18111000, 18168000), "All modes, digimodes"), ) BANDS_15M = ( bandplan.Band((21000000, 21450000), "15 Meter Band"), bandplan.Band((21000000, 21070000), "CW", mode="CW"), bandplan.Band((21070000, 21090000), "All narrow band modes, digimodes"), bandplan.Band((21090000, 21110000), "All narrow band, digimodes, unattended"), bandplan.Band((21110000, 21120000), "All modes, digimodes, unattended"), bandplan.Band((21120000, 21149000), "All narrow band modes"), bandplan.Band((21149000, 21151000), "IBP, exclusively for beacons", mode="CW"), bandplan.Band((21151000, 21450000), "All modes", mode="USB"), ) BANDS_12M = ( bandplan.Band((24890000, 24990000), "12 Meter Band"), bandplan.Band((24890000, 24915000), "CW", mode="CW"), bandplan.Band((24915000, 24929000), "All narrow band modes, digimodes"), bandplan.Band((24929000, 24931000), "IBP, exclusively for beacons", mode="CW"), bandplan.Band((24931000, 24990000), "All modes, digimodes", mode="USB"), ) BANDS_10M = ( bandplan.Band((28000000, 29700000), "10 Meter Band"), bandplan.Band((28000000, 28070000), "CW", mode="CW"), bandplan.Band((28070000, 28120000), "All narrow band modes, digimodes"), bandplan.Band((28120000, 28150000), "All narrow band, digimodes, unattended"), bandplan.Band((28150000, 28190000), "All narrow band modes"), bandplan.Band((28190000, 28199000), "Beacons", mode="CW"), bandplan.Band((28199000, 28201000), "IBP, exclusively for beacons", mode="CW"), bandplan.Band((28201000, 28300000), "Beacons", mode="CW"), bandplan.Band((28300000, 28320000), "All modes, digimodes, unattended"), bandplan.Band((28320000, 29100000), "All modes"), bandplan.Band((29100000, 29200000), "FM simplex", mode="NFM", step_khz=10), bandplan.Band((29200000, 29300000), "All modes, digimodes, unattended"), bandplan.Band((29300000, 29510000), "Satellite downlink"), bandplan.Band((29510000, 29520000), "Guard band, no transmission allowed"), bandplan.Band((29520000, 29590000), "All modes, FM repeater inputs", step_khz=10, mode="NFM"), bandplan.Band((29600000, 29610000), "FM simplex", step_khz=10, mode="NFM"), bandplan.Band((29620000, 29700000), "All modes, FM repeater outputs", step_khz=10, mode="NFM", input_offset=-100000), bandplan.Band((29520000, 29700000), "Wide band", step_khz=10, mode="NFM"), ) BANDS_6M = ( bandplan.Band((50000000, 54000000), "6 Meter Band"), bandplan.Band((50000000, 50300000), "SSB", mode="USB"), bandplan.Band((50300000, 50500000), "Narrow band modes, beacons"), bandplan.Band((50500000, 52000000), "All modes"), bandplan.Band((50500000, 50700000), "Digital communications", mode="DV"), bandplan.Band((50700000, 50900000), "FM/DV repeater outputs", input_offset=+500000, mode="FM"), bandplan.Band((52000000, 54000000), "All modes"), ) BANDS_4M = ( bandplan.Band((70000000, 70500000), "4 Meter Band"), ) BANDS_2M = ( bandplan.Band((144000000, 146000000), "2 Meter Band"), bandplan.Band((144100000, 144150000), "CW", mode="CW"), bandplan.Band((144150000, 144400000), "SSB", mode="USB"), bandplan.Band((144975000, 145806000), "FM", mode="FM"), bandplan.Band((144975000, 145194000), "Repeater inputs", mode="FM"), bandplan.Band((144575000, 145793500), "Repeater outputs", mode="FM", input_offset=-600000), ) BANDS_70CM = ( bandplan.Band((430000000, 440000000), "70 Centimeter Band"), bandplan.Band((434594000, 434981000), "Repeater outputs", mode="FM", input_offset=-16000000), ) BANDS = BANDS_2100M + BANDS_160M + BANDS_80M + BANDS_40M + BANDS_30M + \ BANDS_20M + BANDS_17M + BANDS_15M + BANDS_12M + BANDS_10M + \ BANDS_6M + BANDS_4M + BANDS_2M + BANDS_70CM + bandplan.BANDS_AIR # EU Analogue/DMR PMR446 Frequencies PMR446_FREQS = [446006250, 446018750, 446031250, 446043750, 446056250, 446068750, 446081250, 446093750, 446106250, 446118750, 446131250, 446143750, 446156250, 446168750, 446181250, 446193750] chirp-20261030/chirp/bandplan_iaru_r2.py0000644000175000017500000001504414720054223016716 0ustar hibbyhibby# Copyright 2013 Sean Burford # # 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 . from chirp import bandplan SHORTNAME = "iaru_r2" DESC = { "name": "IARU Region 2 (The Americas)", "updated": "October 8, 2010", "url": "http://www.iaru.org/uploads/1/3/0/7/13073366/r2_band_plan.pdf" } # Bands are broken up like this so that other plans can import bits. BANDS_160M = ( bandplan.Band((1800000, 2000000), "160 Meter Band"), bandplan.Band((1800000, 1810000), "Digimodes"), bandplan.Band((1810000, 1830000), "CW", mode="CW"), bandplan.Band((1830000, 1840000), "CW, priority for DX", mode="CW"), bandplan.Band((1840000, 1850000), "SSB, priority for DX", mode="LSB"), bandplan.Band((1850000, 1999000), "All modes", mode="LSB"), bandplan.Band((1999000, 2000000), "Beacons", mode="CW"), ) BANDS_80M = ( bandplan.Band((3500000, 4000000), "80 Meter Band"), bandplan.Band((3500000, 3510000), "CW, priority for DX", mode="CW"), bandplan.Band((3510000, 3560000), "CW, contest preferred", mode="CW"), bandplan.Band((3560000, 3580000), "CW", mode="CW"), bandplan.Band((3580000, 3590000), "All narrow band modes, digimodes"), bandplan.Band((3590000, 3600000), "All modes"), bandplan.Band((3600000, 3650000), "All modes, SSB contest preferred", mode="LSB"), bandplan.Band((3650000, 3700000), "All modes", mode="LSB"), bandplan.Band((3700000, 3775000), "All modes, SSB contest preferred", mode="LSB"), bandplan.Band((3775000, 3800000), "All modes, SSB DX preferred", mode="LSB"), bandplan.Band((3800000, 4000000), "All modes"), ) BANDS_40M = ( bandplan.Band((7000000, 7300000), "40 Meter Band"), bandplan.Band((7000000, 7025000), "CW, priority for DX", mode="CW"), bandplan.Band((7025000, 7035000), "CW", mode="CW"), bandplan.Band((7035000, 7038000), "All narrow band modes, digimodes"), bandplan.Band((7038000, 7040000), "All narrow band modes, digimodes"), bandplan.Band((7040000, 7043000), "All modes, digimodes"), bandplan.Band((7043000, 7300000), "All modes"), ) BANDS_30M = ( bandplan.Band((10100000, 10150000), "30 Meter Band"), bandplan.Band((10100000, 10130000), "CW", mode="CW"), bandplan.Band((10130000, 10140000), "All narrow band digimodes"), bandplan.Band((10140000, 10150000), "All modes, digimodes, no phone"), ) BANDS_20M = ( bandplan.Band((14000000, 14350000), "20 Meter Band"), bandplan.Band((14000000, 14025000), "CW, priority for DX", mode="CW"), bandplan.Band((14025000, 14060000), "CW, contest preferred", mode="CW"), bandplan.Band((14060000, 14070000), "CW", mode="CW"), bandplan.Band((14070000, 14089000), "All narrow band modes, digimodes"), bandplan.Band((14089000, 14099000), "All modes, digimodes"), bandplan.Band((14099000, 14101000), "IBP, exclusively for beacons", mode="CW"), bandplan.Band((14101000, 14112000), "All modes, digimodes"), bandplan.Band((14112000, 14285000), "All modes, SSB contest preferred", mode="USB"), bandplan.Band((14285000, 14300000), "All modes", mode="AM"), bandplan.Band((14300000, 14350000), "All modes"), ) BANDS_17M = ( bandplan.Band((18068000, 18168000), "17 Meter Band"), bandplan.Band((18068000, 18095000), "CW", mode="CW"), bandplan.Band((18095000, 18105000), "All narrow band modes, digimodes"), bandplan.Band((18105000, 18109000), "All narrow band modes, digimodes"), bandplan.Band((18109000, 18111000), "IBP, exclusively for beacons", mode="CW"), bandplan.Band((18111000, 18120000), "All modes, digimodes"), bandplan.Band((18120000, 18168000), "All modes"), ) BANDS_15M = ( bandplan.Band((21000000, 21450000), "15 Meter Band"), bandplan.Band((21000000, 21070000), "CW", mode="CW"), bandplan.Band((21070000, 21090000), "All narrow band modes, digimodes"), bandplan.Band((21090000, 21110000), "All narrow band modes, digimodes"), bandplan.Band((21110000, 21120000), "All modes (exc SSB), digimodes"), bandplan.Band((21120000, 21149000), "All narrow band modes"), bandplan.Band((21149000, 21151000), "IBP, exclusively for beacons", mode="CW"), bandplan.Band((21151000, 21450000), "All modes", mode="USB"), ) BANDS_12M = ( bandplan.Band((24890000, 24990000), "12 Meter Band"), bandplan.Band((24890000, 24915000), "CW", mode="CW"), bandplan.Band((24915000, 24925000), "All narrow band modes, digimodes"), bandplan.Band((24925000, 24929000), "All narrow band modes, digimodes"), bandplan.Band((24929000, 24931000), "IBP, exclusively for beacons", mode="CW"), bandplan.Band((24931000, 24940000), "All modes, digimodes"), bandplan.Band((24940000, 24990000), "All modes", mode="USB"), ) BANDS_10M = ( bandplan.Band((28000000, 29520000), "10 Meter Band"), bandplan.Band((28000000, 28070000), "CW", mode="CW"), bandplan.Band((28070000, 28120000), "All narrow band modes, digimodes"), bandplan.Band((28120000, 28150000), "All narrow band modes, digimodes"), bandplan.Band((28150000, 28190000), "All narrow band modes, digimodes"), bandplan.Band((28190000, 28199000), "Regional time shared beacons", mode="CW"), bandplan.Band((28199000, 28201000), "IBP, exclusively for beacons", mode="CW"), bandplan.Band((28201000, 28225000), "Continuous duty beacons", mode="CW"), bandplan.Band((28225000, 28300000), "All modes, beacons"), bandplan.Band((28300000, 28320000), "All modes, digimodes"), bandplan.Band((28320000, 29000000), "All modes"), bandplan.Band((29000000, 29200000), "All modes, AM preferred", mode="AM"), bandplan.Band((29200000, 29300000), "All modes including FM, digimodes"), bandplan.Band((29300000, 29510000), "Satellite downlink"), bandplan.Band((29510000, 29520000), "Guard band, no transmission allowed"), bandplan.Band((29520000, 29700000), "FM", step_khz=10, mode="NFM"), bandplan.Band((29620000, 29690000), "FM Repeaters", input_offset=-100000), ) BANDS = BANDS_160M + BANDS_80M + BANDS_40M + BANDS_30M + BANDS_20M + \ BANDS_17M + BANDS_15M + BANDS_12M + BANDS_10M + bandplan.BANDS_AIR chirp-20261030/chirp/bandplan_iaru_r3.py0000644000175000017500000001253214720054223016716 0ustar hibbyhibby# Copyright 2013 Sean Burford # # 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 . from chirp import bandplan SHORTNAME = "iaru_r3" DESC = { "name": "IARU Region 3 (Asia Pacific)", "updated": "16 October 2009", "url": "http://www.iaru.org/uploads/1/3/0/7/13073366/r3_band_plan.pdf" } # Bands are broken up like this so that other plans can import bits. BANDS_2100M = ( bandplan.Band((135700, 137800), "137 kHz Band", mode="CW"), ) BANDS_160M = ( bandplan.Band((1800000, 2000000), "160 Meter Band"), bandplan.Band((1830000, 1840000), "Digimodes", mode="RTTY"), bandplan.Band((1840000, 2000000), "Phone"), ) BANDS_80M = ( bandplan.Band((3500000, 3900000), "80 Meter Band"), bandplan.Band((3500000, 3510000), "CW, priority for DX", mode="CW"), bandplan.Band((3535000, 3900000), "Phone"), bandplan.Band((3775000, 3800000), "All modes, SSB DX preferred", mode="LSB"), ) BANDS_40M = ( bandplan.Band((7000000, 7300000), "40 Meter Band"), bandplan.Band((7000000, 7025000), "CW, priority for DX", mode="CW"), bandplan.Band((7025000, 7035000), "All narrow band modes, cw", mode="CW"), bandplan.Band((7035000, 7040000), "All narrow band modes, phone"), bandplan.Band((7040000, 7300000), "All modes, digimodes"), ) BANDS_30M = ( bandplan.Band((10100000, 10150000), "30 Meter Band"), bandplan.Band((10100000, 10130000), "CW", mode="CW"), bandplan.Band((10130000, 10150000), "All narrow band digimodes"), ) BANDS_20M = ( bandplan.Band((14000000, 14350000), "20 Meter Band"), bandplan.Band((14000000, 14070000), "CW", mode="CW"), bandplan.Band((14070000, 14099000), "All narrow band modes, digimodes"), bandplan.Band((14099000, 14101000), "IBP, exclusively for beacons", mode="CW"), bandplan.Band((14101000, 14112000), "All narrow band modes, digimodes"), bandplan.Band((14101000, 14350000), "All modes, digimodes"), ) BANDS_17M = ( bandplan.Band((18068000, 18168000), "17 Meter Band"), bandplan.Band((18068000, 18100000), "CW", mode="CW"), bandplan.Band((18100000, 18109000), "All narrow band modes, digimodes"), bandplan.Band((18109000, 18111000), "IBP, exclusively for beacons", mode="CW"), bandplan.Band((18111000, 18168000), "All modes, digimodes"), ) BANDS_15M = ( bandplan.Band((21000000, 21450000), "15 Meter Band"), bandplan.Band((21000000, 21070000), "CW", mode="CW"), bandplan.Band((21070000, 21125000), "All narrow band modes, digimodes"), bandplan.Band((21125000, 21149000), "All narrow band modes, digimodes"), bandplan.Band((21149000, 21151000), "IBP, exclusively for beacons", mode="CW"), bandplan.Band((21151000, 21450000), "All modes", mode="USB"), ) BANDS_12M = ( bandplan.Band((24890000, 24990000), "12 Meter Band"), bandplan.Band((24890000, 24920000), "CW", mode="CW"), bandplan.Band((24920000, 24929000), "All narrow band modes, digimodes"), bandplan.Band((24929000, 24931000), "IBP, exclusively for beacons", mode="CW"), bandplan.Band((24931000, 24990000), "All modes, digimodes", mode="USB"), ) BANDS_10M = ( bandplan.Band((28000000, 29700000), "10 Meter Band"), bandplan.Band((28000000, 28050000), "CW", mode="CW"), bandplan.Band((28050000, 28150000), "All narrow band modes, digimodes"), bandplan.Band((28150000, 28190000), "All narrow band modes, digimodes"), bandplan.Band((28190000, 28199000), "Beacons", mode="CW"), bandplan.Band((28199000, 28201000), "IBP, exclusively for beacons", mode="CW"), bandplan.Band((28201000, 28300000), "Beacons", mode="CW"), bandplan.Band((28300000, 29300000), "Phone"), bandplan.Band((29300000, 29510000), "Satellite downlink"), bandplan.Band((29510000, 29520000), "Guard band, no transmission allowed"), bandplan.Band((29520000, 29700000), "Wide band", step_khz=10, mode="NFM"), ) BANDS_6M = ( bandplan.Band((50000000, 54000000), "6 Meter Band"), bandplan.Band((50000000, 50100000), "Beacons", mode="CW"), bandplan.Band((50100000, 50500000), "Phone and narrow band"), bandplan.Band((50500000, 54000000), "Wide band"), ) BANDS_2M = ( bandplan.Band((144000000, 148000000), "2 Meter Band"), bandplan.Band((144000000, 144035000), "Earth Moon Earth"), bandplan.Band((145800000, 146000000), "Satellite"), ) BANDS_70CM = ( bandplan.Band((430000000, 450000000), "70cm Band"), bandplan.Band((431900000, 432240000), "Earth Moon Earth"), bandplan.Band((435000000, 438000000), "Satellite"), ) BANDS_23CM = ( bandplan.Band((1240000000, 1300000000), "23cm Band"), bandplan.Band((1260000000, 1270000000), "Satellite"), bandplan.Band((1296000000, 1297000000), "Earth Moon Earth"), ) BANDS = BANDS_2100M + BANDS_160M + BANDS_80M + BANDS_40M + BANDS_30M + \ BANDS_20M + BANDS_17M + BANDS_15M + BANDS_12M + BANDS_10M + \ BANDS_6M + BANDS_2M + BANDS_70CM + BANDS_23CM + bandplan.BANDS_AIR chirp-20261030/chirp/bandplan_na.py0000644000175000017500000003256214720054223015755 0ustar hibbyhibby# Copyright 2013 Dan Smith # # 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 . from chirp import bandplan, bandplan_iaru_r2 SHORTNAME = "north_america" DESC = { "name": "North American Band Plan", "url": "http://www.arrl.org/band-plan" } BANDS_160M = ( bandplan.Band((1800000, 2000000), "160 Meter Band", mode="CW"), bandplan.Band((1800000, 1810000), "Digital Modes"), bandplan.Band((1843000, 2000000), "SSB, SSTV and other wideband modes"), bandplan.Band((1995000, 2000000), "Experimental"), bandplan.Band((1999000, 2000000), "Beacons"), ) BANDS_80M = ( bandplan.Band((3500000, 4000000), "80 Meter Band"), bandplan.Band((3570000, 3600000), "RTTY/Data", mode="RTTY"), bandplan.Band((3790000, 3800000), "DX window"), ) BANDS_40M = ( bandplan.Band((7000000, 7300000), "40 Meter Band"), bandplan.Band((7080000, 7125000), "RTTY/Data", mode="RTTY"), ) BANDS_30M = ( bandplan.Band((10100000, 10150000), "30 Meter Band"), bandplan.Band((10130000, 10140000), "RTTY", mode="RTTY"), bandplan.Band((10140000, 10150000), "Packet"), ) BANDS_20M = ( bandplan.Band((14000000, 14350000), "20 Meter Band"), bandplan.Band((14070000, 14095000), "RTTY", mode="RTTY"), bandplan.Band((14095000, 14099500), "Packet"), bandplan.Band((14100500, 14112000), "Packet"), ) BANDS_17M = ( bandplan.Band((18068000, 18168000), "17 Meter Band"), bandplan.Band((18100000, 18105000), "RTTY", mode="RTTY"), bandplan.Band((18105000, 18110000), "Packet"), ) BANDS_15M = ( bandplan.Band((21000000, 21450000), "15 Meter Band"), bandplan.Band((21070000, 21110000), "RTTY/Data", mode="RTTY"), ) BANDS_12M = ( bandplan.Band((24890000, 24990000), "12 Meter Band"), bandplan.Band((24920000, 24925000), "RTTY", mode="RTTY"), bandplan.Band((24925000, 24930000), "Packet"), ) BANDS_10M = ( bandplan.Band((28000000, 29700000), "10 Meter Band"), bandplan.Band((28000000, 28070000), "CW", mode="CW"), bandplan.Band((28070000, 28150000), "RTTY", mode="RTTY"), bandplan.Band((28150000, 28190000), "CW", mode="CW"), bandplan.Band((28201000, 28300000), "Beacons", mode="CW"), bandplan.Band((28300000, 29300000), "Phone"), bandplan.Band((29000000, 29200000), "AM", mode="AM"), bandplan.Band((29300000, 29510000), "Satellite Downlinks"), bandplan.Band((29520000, 29590000), "Repeater Inputs", step_khz=10, mode="FM"), bandplan.Band((29610000, 29700000), "Repeater Outputs", step_khz=10, mode="FM", input_offset=-890000), ) BANDS_6M = ( bandplan.Band((50000000, 54000000), "6 Meter Band"), bandplan.Band((50000000, 50100000), "CW, beacons", mode="CW"), bandplan.Band((50060000, 50080000), "beacon subband"), bandplan.Band((50100000, 50300000), "SSB, CW", mode="USB"), bandplan.Band((50100000, 50125000), "DX window", mode="USB"), bandplan.Band((50300000, 50600000), "All modes"), bandplan.Band((50600000, 50800000), "Nonvoice communications"), bandplan.Band((50800000, 51000000), "Radio remote control", step_khz=20), bandplan.Band((51000000, 51100000), "Pacific DX window"), bandplan.Band((51120000, 51180000), "Digital repeater inputs", step_khz=10), bandplan.Band((51500000, 51600000), "Simplex"), bandplan.Band((51620000, 51980000), "Repeater outputs A", input_offset=-500000), bandplan.Band((51620000, 51680000), "Digital repeater outputs", input_offset=-500000), bandplan.Band((52020000, 52040000), "FM simplex", mode="FM"), bandplan.Band((52500000, 52980000), "Repeater outputs B", input_offset=-500000, step_khz=20, mode="FM"), bandplan.Band((53000000, 53100000), "FM simplex", mode="FM"), bandplan.Band((53100000, 53400000), "Radio remote control", step_khz=100), bandplan.Band((53500000, 53980000), "Repeater outputs C", input_offset=-500000), bandplan.Band((53500000, 53800000), "Radio remote control", step_khz=100), bandplan.Band((53520000, 53900000), "Simplex"), ) BANDS_2M = ( bandplan.Band((144000000, 148000000), "2 Meter Band"), bandplan.Band((144000000, 144050000), "EME (CW)", mode="CW"), bandplan.Band((144050000, 144100000), "General CW and weak signals", mode="CW"), bandplan.Band((144100000, 144200000), "EME and weak-signal SSB", mode="USB"), bandplan.Band((144200000, 144275000), "General SSB operation", mode="USB"), bandplan.Band((144275000, 144300000), "Propagation beacons", mode="CW"), bandplan.Band((144300000, 144500000), "OSCAR subband"), bandplan.Band((144600000, 144900000), "FM repeater inputs", mode="FM"), bandplan.Band((144900000, 145100000), "Weak signal and FM simplex", mode="FM", step_khz=10), bandplan.Band((145100000, 145200000), "Linear translator outputs", input_offset=-600000), bandplan.Band((145200000, 145500000), "FM repeater outputs", input_offset=-600000, mode="FM",), bandplan.Band((145500000, 145800000), "Misc and experimental modes"), bandplan.Band((145800000, 146000000), "OSCAR subband"), bandplan.Band((146400000, 146580000), "Simplex"), bandplan.Band((146610000, 146970000), "Repeater outputs", input_offset=-600000), bandplan.Band((147000000, 147390000), "Repeater outputs", input_offset=600000), bandplan.Band((147420000, 147570000), "Simplex"), ) BANDS_1_25M = ( bandplan.Band((222000000, 225000000), "1.25 Meter Band"), bandplan.Band((222000000, 222150000), "Weak-signal modes"), bandplan.Band((222000000, 222025000), "EME"), bandplan.Band((222050000, 222060000), "Propagation beacons"), bandplan.Band((222100000, 222150000), "Weak-signal CW & SSB"), bandplan.Band((222150000, 222250000), "Local coordinator's option"), bandplan.Band((223400000, 223520000), "FM simplex", mode="FM"), bandplan.Band((223520000, 223640000), "Digital, packet"), bandplan.Band((223640000, 223700000), "Links, control"), bandplan.Band((223710000, 223850000), "Local coordinator's option"), bandplan.Band((223850000, 224980000), "Repeater outputs only", mode="FM", input_offset=-1600000), ) BANDS_70CM = ( bandplan.Band((420000000, 450000000), "70 Centimeter Band"), bandplan.Band((420000000, 426000000), "ATV repeater or simplex"), bandplan.Band((426000000, 432000000), "ATV simplex"), bandplan.Band((432000000, 432070000), "EME (Earth-Moon-Earth)"), bandplan.Band((432070000, 432100000), "Weak-signal CW", mode="CW"), bandplan.Band((432100000, 432300000), "Mixed-mode and weak-signal work"), bandplan.Band((432300000, 432400000), "Propagation beacons"), bandplan.Band((432400000, 433000000), "Mixed-mode and weak-signal work"), bandplan.Band((433000000, 435000000), "Auxiliary/repeater links"), bandplan.Band((435000000, 438000000), "Satellite only (internationally)"), bandplan.Band((438000000, 444000000), "ATV repeater input/repeater links", input_offset=5000000), bandplan.Band((442000000, 445000000), "Repeater input/output (local option)", input_offset=5000000), bandplan.Band((445000000, 447000000), "Shared by aux and control links, " "repeaters, simplex (local option)"), bandplan.Band((447000000, 450000000), "Repeater inputs and outputs " "(local option)", input_offset=-5000000), ) BANDS_US_GMRS = ( bandplan.Band((462550000, 462725000), "GMRS Repeaters", input_offset=5000000, duplex=''), ) BANDS_33CM = ( bandplan.Band((902000000, 928000000), "33 Centimeter Band"), bandplan.Band((902075000, 902100000), "CW/SSB, Weak signal"), bandplan.Band((902100000, 902125000), "CW/SSB, Weak signal"), bandplan.Band((903000000, 903100000), "CW/SSB, Beacons and weak signal"), bandplan.Band((903100000, 903400000), "CW/SSB, Weak signal"), bandplan.Band((903400000, 909000000), "Mixed modes, Mixed operations " "including control links"), bandplan.Band((909000000, 915000000), "Analog/digital Broadband multimedia " "including ATV, DATV and SS"), bandplan.Band((915000000, 921000000), "Analog/digital Broadband multimedia " "including ATV, DATV and SS"), bandplan.Band((921000000, 927000000), "Analog/digital Broadband multimedia " "including ATV, DATV and SS"), bandplan.Band((927000000, 927075000), "FM / other including DV or CW/SSB", input_offset=-25000000, step_khz=12.5), bandplan.Band((927075000, 927125000), "FM / other including DV. Simplex"), bandplan.Band((927125000, 928000000), "FM / other including DV", input_offset=-25000000, step_khz=12.5), ) BANDS_23CM = ( bandplan.Band((1240000000, 1300000000), "23 Centimeter Band"), bandplan.Band((1240000000, 1246000000), "ATV Channel #1"), bandplan.Band((1246000000, 1248000000), "Point-to-point links paired " "with 1258.000-1260.000", mode="FM"), bandplan.Band((1248000000, 1252000000), "Digital"), bandplan.Band((1252000000, 1258000000), "ATV Channel #2"), bandplan.Band((1258000000, 1260000000), "Point-to-point links paired with 1246.000-1248.000", mode="FM"), bandplan.Band((1240000000, 1260000000), "Regional option, FM ATV"), bandplan.Band((1260000000, 1270000000), "Satellite uplinks, Experimental, " "Simplex ATV"), bandplan.Band((1270000000, 1276000000), "FM, digital Repeater inputs " "(Regional option)", step_khz=25), bandplan.Band((1276000000, 1282000000), "ATV Channel #3"), bandplan.Band((1282000000, 1288000000), "FM, digital repeater outputs", step_khz=25, input_offset=-12000000), bandplan.Band((1288000000, 1294000000), "Various Broadband Experimental, " "Simplex ATV"), bandplan.Band((1290000000, 1294000000), "FM, digital Repeater outputs " "(Regional option)", step_khz=25, input_offset=-20000000), bandplan.Band((1294000000, 1295000000), "FM simplex", mode="FM"), bandplan.Band((1295000000, 1297000000), "Narrow Band Segment"), bandplan.Band((1295000000, 1295800000), "Narrow Band Image, Experimental"), bandplan.Band((1295800000, 1296080000), "CW, SSB, digital EME"), bandplan.Band((1296080000, 1296200000), "CW, SSB Weak Signal"), bandplan.Band((1296200000, 1296400000), "CW, digital Beacons"), bandplan.Band((1296400000, 1297000000), "General Narrow Band"), bandplan.Band((1297000000, 1300000000), "Digital"), ) BANDS_13CM = ( bandplan.Band((2300000000, 2450000000), "13 Centimeter Band"), bandplan.Band((2300000000, 2303000000), "Analog & Digital 0.05-1.0 MHz, " "including full duplex; paired with 2390-2393"), bandplan.Band((2303000000, 2303750000), "Analog & Digital <50 kHz; " "paired with 2393 - 2393.750"), bandplan.Band((2303750000, 2304000000), "SSB, CW, digital weak-signal"), bandplan.Band((2304000000, 2304100000), "Weak Signal EME Band, <3 kHz"), bandplan.Band((2304100000, 2304300000), "SSB, CW, digital weak-signal, <3 kHz"), bandplan.Band((2304300000, 2304400000), "Beacons, <3 kHz"), bandplan.Band((2304400000, 2304750000), "SSB, CW, digital weak-signal and " "NBFM, <6 kHz"), bandplan.Band((2304750000, 2305000000), "Analog & Digital; paired with " "2394.750-2395, <50 kHz"), bandplan.Band((2305000000, 2310000000), "Analog & Digital, paired with " "2395-2400, 0.05 - 1.0 MHz"), bandplan.Band((2310000000, 2390000000), "NON-AMATEUR"), bandplan.Band((2390000000, 2393000000), "Analog & Digital, including full " "duplex; paired with 2300-2303, 0.05 - 1.0 MHz"), bandplan.Band((2393000000, 2393750000), "Analog & Digital; paired with " "2303-2303.750, < 50 kHz"), bandplan.Band((2393750000, 2394750000), "Experimental"), bandplan.Band((2394750000, 2395000000), "Analog & Digital; paired with " "2304.750-2305, < 50 kHz"), bandplan.Band((2395000000, 2400000000), "Analog & Digital, including full " "duplex; paired with 2305-2310, 0.05-1.0 MHz"), bandplan.Band((2400000000, 2410000000), "Amateur Satellite Communications, " "<6 kHz"), bandplan.Band((2410000000, 2450000000), "Broadband Modes, 22 MHz max."), ) BANDS = bandplan_iaru_r2.BANDS + \ BANDS_160M + BANDS_80M + BANDS_40M + BANDS_30M + BANDS_20M + \ BANDS_17M + BANDS_15M + BANDS_12M + BANDS_10M + BANDS_6M + \ BANDS_2M + BANDS_1_25M + BANDS_70CM + BANDS_33CM + BANDS_23CM + \ BANDS_13CM + BANDS_US_GMRS # US 2017+ GMRS/FRS Frequencies GMRS_LOW = [462562500, 462587500, 462612500, 462637500, 462662500, 462687500, 462712500] GMRS_HHONLY = [467562500, 467587500, 467612500, 467637500, 467662500, 467687500, 467712500] GMRS_HIRPT = [462550000, 462575000, 462600000, 462625000, 462650000, 462675000, 462700000, 462725000] ALL_GMRS_FREQS = GMRS_LOW + GMRS_HHONLY + GMRS_HIRPT # US MURS Frequencies MURS_NFM = [151820000, 151880000, 151940000] MURS_FM = [154570000, 154600000] ALL_MURS_FREQS = MURS_NFM + MURS_FM chirp-20261030/chirp/bitwise.py0000644000175000017500000011361115242013013015151 0ustar hibbyhibby# Copyright 2010 Dan Smith # # 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 . # Language: # # Example definitions: # # bit foo[8]; /* Eight single bit values */ # lbit foo[16]; /* Sixteen single-bit values, little-endian*/ # 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) */ # i8 foo; /* Signed 8-bit value */ # i16 foo; /* Signed 16-bit value */ # il16 foo; /* Signed 16-bit value (LE) */ # i24 foo; /* Signed 24-bit value */ # il24 foo; /* Signed 24-bit value (LE) */ # i32 foo; /* Signed 32-bit value */ # il32 foo; /* Signed 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 */ # union { # u16 whole; # bbcd digits[2]; # }; /* Union where all elements occupy the same memory */ # # 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 import logging import re import warnings from chirp import bitwise_grammar from chirp import util LOG = logging.getLogger(__name__) class ParseError(Exception): """Indicates an error parsing a definition""" pass def string_straight_encode(string): """str -> bytes""" # So, there are a lot of py2-thinking chirp drivers that # will do something like this: # # memobj.name = 'foo\xff' # # because they need the string to be stored in radio memory # with a 0xFF byte terminating the string. If they do that in # py3 we will get the unicode equivalent, which is a non-ASCII # character. Conventional unicode wisdom would tell us we need # to encode() the string to get UTF-8 bytes, but that's not # what we want here (the radio doesn't support UTF-8 and we need # specific binary values in memory). Ideally we would have # written all of chirp with bytes() for these values, but alas. # We can get the intended string here by doing bytes([ord(char)]). return bytes(b''.join(bytes([ord(b)]) for b in string)) def string_straight_decode(string): """bytes -> str""" # Normally, we would want to decode bytes() to str() for py3. # However...chirp drivers are currently using strings with # hex escapes for setting binary byte values in memories. # Technically, this is char, which is a little more like bytes() # in py3, but until every driver has converted its strings to # bytes(), this is massively simpler. Since set_value() is # doing the inverse, we can do this here. If something sets # a char to '\xFF' it will arrive below as the unicode character, # and be converted to the integer/bytes value. When it is read out # here, we will return that same unicode character and the driver # will detect '\xFF' properly. # FIXMEPY3: Remove this and the hack below when drivers convert to # bytestrings. return ''.join(chr(b) for b in string) def format_binary(nbits, value, width=8, shift=0, padchar='.'): padleft = width - shift padright = width - padleft - nbits bits = f'{value << shift:0{nbits}b}'[:nbits] return f'{padleft * padchar}{bits}{padright * padchar}' def bits_between(start, end): bits = (1 << (int(end) - int(start))) - 1 return bits << int(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 bbcd_to_dec(bbcd: int, packed=True) -> int: """Convert a BE BCD int to decimal int. Usually for getting user-friendly value from raw data. For packed, 0x243 (0d579) -> 243. For unpacked, 0x020403 (0d132099) -> 243.""" result = 0 bit_shift = 4 if packed else 8 dec_shift = 0 while bbcd > 0: result += (bbcd & 0xF) * (10**dec_shift) bbcd >>= bit_shift dec_shift += 1 return result def dec_to_bbcd(dec: int, packed=True) -> int: """Convert a decimal int to BE BCD int. Usually to set raw data from user-friendly value. For packed, 243 -> 0x243 (0d579). For unpacked, 243 -> 0x020403 (0d132099).""" result = 0 bit_shift_amount = 4 if packed else 8 bcd_shift = 0 while dec > 0: result += (dec % 10) << bcd_shift dec //= 10 bcd_shift += bit_shift_amount return result def numeric_str_to_bcd(bcdel, string, pad='0'): """Convert a string of digits into BCD-like bytes. This should only be used for the quasi-BCD encodings of DTMF characters that some radios use (i.e. 0x12 == "12" and 0x1D == "1D") """ string = string.ljust(2 * bcdel.size() // 8, pad) for i in range(0, bcdel.size() // 8): bcdel[i] = int(string[i*2:i*2+2], 16) def bcd_to_numeric_str(bcdel, pad='0'): """Convert a list of BCD-like bytes into a string. See numeric_str_to_bcd above. """ string = '' for i in range(0, bcdel.size() // 8): string += '%02X' % bcdel[i] return string.ljust(2 * bcdel.size() // 8, pad) 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 = int(offset) self._count = count def _compat_bytes(self, bs, asbytes): if asbytes: return bytes(bs) else: warnings.warn('Driver is using non-byte-native get_raw()', DeprecationWarning, stacklevel=3) return string_straight_decode(bs) def size(self): return int(self._size * 8) def get_offset(self): return int(self._offset) def _get_value(self, data): raise Exception("Not implemented") def get_value(self): value = self._data[self._offset:self._offset + self._size] return self._get_value(value) def set_value(self, value): raise Exception("Not implemented for %s" % self.__class__) def get_raw(self, asbytes=True): raw = self._data[self._offset:self._offset+self._size] return self._compat_bytes(raw, asbytes) def set_raw(self, data): if isinstance(data, str): warnings.warn('Driver is using non-byte-native set_raw()', DeprecationWarning, stacklevel=2) data = string_straight_encode(data) self._data[self._offset] = data[:self._size] def get_path(self, path): """Retrieve a child element starting from here by symbolic path. Examples: .mystruct.foo[2].field1 [3] .bar """ tok = re.search('[.[]', path) if tok: tok = tok.group(0) if path.startswith('.'): return self.get_path(path[1:]) elif path.startswith('['): index, rest = path.split(']', 1) index = int(index[1:]) return self[index].get_path(rest) elif tok == '.': field, rest = path.split('.', 1) return getattr(self, field).get_path(rest) elif tok == '[': field, rest = path.split('[', 1) return getattr(self, field).get_path('[' + rest) elif path: return getattr(self, path) else: return self def fill_raw(self, byteval): """Fill object with bytes :param byteval: A bytes of length 1 to fill the object's memory with """ assert isinstance(byteval, bytes) and len(byteval) == 1 self.set_raw(byteval * (self.size() // 8)) 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): hex = self.get_raw().hex().upper() return "%i:[0x%s (%i)]" % (len(self.__items), hex, int(self)) if isinstance(self.__items[0], charDataElement): return "%i:[(%s)]" % (len(self.__items), repr(str(self))[1:-1]) 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, asbytes=True): raw = [item.get_raw(asbytes=True) for item in self.__items] return self._compat_bytes(bytes(b''.join(raw)), asbytes) def set_raw(self, data): item_size = self.__items[0].size() // 8 assert len(data) / item_size == len(self), 'Invalid raw data length' for i in range(len(self)): self[i].set_raw(data[i * item_size:(i + 1) * item_size]) def __setitem__(self, index, val): self.__items[index].set_value(val) def __getitem__(self, index): if isinstance(index, slice): return self.__items[int(index.start):int(index.stop)] else: return self.__items[int(index)] def __len__(self): return len(self.__items) def __str__(self): if isinstance(self.__items[0], charDataElement): # NOTE: All the values should be strings coming out of py3. # and on py2 they could be a mix of unicode and str, the latter # for non-ASCII values. On py2 we can just coerce all of these # types to a string for compatibility. return "".join([str(x.get_value()) for x in self.__items]) elif isinstance(self.__items[0], bcdDataElement): return str(int(self)) else: return str(self.__items) def __int__(self): if isinstance(self.__items[0], bcdDataElement): val = 0 if isinstance(self.__items[0], bbcdDataElement): items = self.__items else: items = reversed(self.__items) for i in items: val = (val * 100) + int(i) 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): if len(value) != len(self.__items): raise ValueError("String expects exactly %i characters, not %i" % ( len(self.__items), len(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(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 items(self): index = 0 for item in self.__items: yield (str(index), item) index += 1 def size(self): size = 0 for i in self.__items: size += i.size() return int(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 __nonzero__(self): return int(self) != 0 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 __truediv__(self, val): return self.get_value() / val def __div__(self, val): return self.get_value() / val def __floordiv__(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 __xor__(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 __rxor__(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 __ixor__(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 __bool__(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 b"\x00" or b"" post = self._endianess == "<" and b"\x00" or b"" 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 packed = struct.pack(self._endianess + "I", int(value) & 0xFFFFFFFF) self._data[self._offset] = packed[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 i8DataElement(u8DataElement): _size = 1 def _get_value(self, data): return struct.unpack("b", data)[0] def set_value(self, value): self._data[self._offset] = struct.pack("b", int(value)) class i16DataElement(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)) class il16DataElement(i16DataElement): _endianess = "<" class i24DataElement(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))[start:end] class il24DataElement(i24DataElement): _endianess = "<" class i32DataElement(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)) class il32DataElement(i32DataElement): _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 string_straight_decode(data) def set_value(self, value): if isinstance(value, int): # This is the case if bytes() are passed in to arrayDataElement # to set a string self._data[self._offset] = value else: self._data[self._offset] = string_straight_encode(value) class bcdDataElement(DataElement): def __init__(self, *a, **k): super().__init__(*a, **k) self._ignoremask = 0x00 def __int__(self): return bbcd_to_dec(self.get_value()) 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 ignore_bits(self, mask): self._ignoremask = mask def set_raw(self, data): if isinstance(data, int): self._data[self._offset] = data & 0xFF elif isinstance(data, str): warnings.warn('Driver is using non-byte-native set_raw()', DeprecationWarning, stacklevel=2) self._data[self._offset] = string_straight_encode(data[0]) elif isinstance(data, bytes) and len(data) == 1: self._data[self._offset] = int(data[0]) & 0xFF else: raise TypeError("Unable to set bcdDataElement from type %s" % type(data)) def set_value(self, value): preserve = self._data[self._offset][0] & self._ignoremask self._data[self._offset] = ( dec_to_bbcd(int(value) & 0xFF) & ~self._ignoremask) | preserve def _get_value(self, data): return data[0] & ~self._ignoremask def __str__(self): return str(int(self)) def __repr__(self): hex = self.get_raw().hex().upper() return f'0x{hex} ({int(self)})' class lbcdDataElement(bcdDataElement): _size = 1 class bbcdDataElement(bcdDataElement): _size = 1 class bitDataElement(intDataElement): _nbits = 0 _shift = 0 _subgen = u8DataElement # Default to a byte def __repr__(self): size = self._subgen._size * 8 hex_len = size // 4 bits = format_binary(self._nbits, self.get_value(), size, self._shift) return f'0x{int(self):0{hex_len}X} ({bits}b)' 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) >> int(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 value = ((int(value) << int(self._shift-self._nbits)) & mask) | data self._subgen(self._data, self._offset).set_value(value) def get_bbcd(self) -> int: """Get BE BCD as a user-friendly decimal. e.g. 0x243 (0d579) -> 243.""" return bbcd_to_dec(int(self)) def set_bbcd(self, decimal: int): """Set decimal as stored BE BCD. e.g. 243 -> 0x243 (0d579).""" self.set_value(dec_to_bbcd(int(decimal))) def size(self): return int(self._nbits) class structDataElement(DataElement): def __repr__(self): return self._make_repr('struct') def _make_repr(self, typename, verbose=False): s = typename + " {" + os.linesep pos = 0 for prop in self._keys: cur = self._generators[prop].get_offset() if cur != pos and verbose: s += os.linesep + ( 'Gap of %i at 0x%04x' % (cur - pos, pos)) + os.linesep s += util.hexprint(self._data[pos:cur], pos) + os.linesep s += " %15s: %s%s" % (prop, repr(self._generators[prop]), os.linesep) pos = cur + self._generators[prop].size() // 8 s += "} %s (%i bytes at 0x%04X)%s" % (self._name, self.size() // 8, self._offset, os.linesep) return s def verbose_repr(self): return self._make_repr('struct', verbose=True) 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 __contains__(self, key): return key in self._generators.keys() 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 __dir__(self): return list(super().__dir__()) + list(self._generators.keys()) def __getattr__(self, name): try: return self._generators[name] except KeyError: raise AttributeError("No attribute %s in struct %s" % ( name, self._name)) def __setattr__(self, name, value): if "_structDataElement__init" not in self.__dict__: 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() return int(size) def get_raw(self, asbytes=True): size = self.size() // 8 raw = self._data[self._offset:self._offset+size] return self._compat_bytes(raw, asbytes) def set_raw(self, buffer): if len(buffer) != (self.size() // 8): raise ValueError("Struct size mismatch during set_raw()") if isinstance(buffer, str): warnings.warn('Driver is using non-byte-native set_raw()', DeprecationWarning, stacklevel=2) buffer = string_straight_encode(buffer) self._data[self._offset] = buffer def __iter__(self): for item in self._generators.values(): yield item def items(self): for key in self._keys: yield key, self._generators[key] class unionDataElement(structDataElement): def __repr__(self): return self._make_repr('union') def set_union_size(self, size): # We don't know the size of the union until we have processed the first # member, so allow this to be set after creation. assert (size % 8 == 0) self.__dict__['_size'] = size def size(self): return self._size def parse_count(string): if string.startswith('0x'): return int(string, 16) else: return int(string) class Processor: _types = { "u8": u8DataElement, "u16": u16DataElement, "ul16": ul16DataElement, "u24": u24DataElement, "ul24": ul24DataElement, "u32": u32DataElement, "ul32": ul32DataElement, "i8": i8DataElement, "i16": i16DataElement, "il16": il16DataElement, "i24": i24DataElement, "il24": il24DataElement, "i32": i32DataElement, "char": charDataElement, "lbcd": lbcdDataElement, "bbcd": bbcdDataElement, } def __init__(self, data, offset, input): if hasattr(data, 'get_byte_compatible'): # bitwise uses the byte-compatible interface of MemoryMap, # if that is what was passed in data = data.get_byte_compatible() self._data = data self._offset = offset self._obj = None self._user_types = {} self._input = input.split('\n') def do_symbol(self, symdef, gen): name = symdef[1] self._generators[name] = gen def get_source_line(self, lineno): return self._input[lineno] def get_line_from_sym(self, sym): name = sym.__name__ if ':' in name.line: _, num = name.line.split(':') else: num = 0 return int(num) def get_line_sym(self, sym): num = self.get_line_from_sym(sym) return '%i: %s' % (num, self.get_source_line(num)) 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] if name in self._generators: newname = '%s_%06x' % (name, self._offset) prevline = self._lines.get(name, 'unknown') LOG.error('Duplicate definition for %s on line %s; ' 'renaming to %s (previous definition line %s)', name, self.get_line_sym(defn[0]), newname, prevline) name = newname 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) self._lines[name] = self.get_line_from_sym(defn[0]) bitsleft -= bits if bitsleft: LOG.warn("WARNING: %i trailing bits unaccounted for in %s" % (bitsleft, name)) return bytes def do_bitarray(self, i, count, bigendian=True): if count % 8 != 0: raise ValueError("bit array must be divisible by 8.") class bitDE(bitDataElement): _nbits = 1 _shift = (8 - i % 8) if bigendian else (i % 8) + 1 return bitDE(self._data, self._offset) 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 = parse_count(defn[1][1][1][1]) else: count = 1 sym = defn[1] name = sym[1] if name in self._generators: newname = '%s_%06x' % (name, self._offset) prevline = self._lines.get(name, 'unknown') LOG.error('Duplicate definition for %s on line %s; ' 'renaming to %s (previous definition line %s)', name, self.get_line_sym(sym), newname, prevline) name = newname res = arrayDataElement(self._offset) size = 0 for i in range(0, count): if dtype == "bit": gen = self.do_bitarray(i, count) self._offset += int((i+1) % 8 == 0) elif dtype == "lbit": gen = self.do_bitarray(i, count, bigendian=False) self._offset += int((i+1) % 8 == 0) else: gen = self._types[dtype](self._data, self._offset) self._offset += (gen.size() // 8) res.append(gen) if defn[1][0] != "array": self._generators[name] = res[0] else: self._generators[name] = res self._lines[name] = self.get_line_from_sym(sym) 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 = parse_count(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 tmp_lines = self._lines self._generators = element self._lines = {} self.parse_block(block) self._generators = tmp self._lines = tmp_lines if deftype[0] != "array": 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_union(self, union): deftype = union[-1] block = union[:-1] union_size = 0 tmp = self._generators if deftype[0] == "array": name = deftype[1][0][1] count = parse_count(deftype[1][1][1]) elif deftype[0] == "symbol": name = deftype[1] count = 1 result = arrayDataElement(self._offset) for i in range(0, count): start_of_union_offset = self._offset self._generators = element = unionDataElement( self._data, start_of_union_offset, name=name) for t, d in block: self._offset = start_of_union_offset if t not in ("definition", "struct", "union"): raise ParseError("Not supported in union: %s" % t) self.parse_statement(t, d) size = self._offset - start_of_union_offset if union_size == 0: union_size = size elif union_size != size: LOG.error('Union member size mismatch parsing %s', name) raise ParseError('Union members must be the same size ' '(found %i, expected %i)' % (size, union_size)) element.set_union_size(union_size * 8) if union_size == 0: raise ParseError('Empty union %r is not valid' % name) # Increment the offset only past the end of the union self._offset = start_of_union_offset + union_size result.append(element) self._generators = tmp if count == 1: result = result[0] self._generators[name] = result def assert_negative_seek(self, message): warnings.warn(message, DeprecationWarning, stacklevel=6) def assert_unnecessary_seek(self, message): warnings.warn(message, DeprecationWarning, stacklevel=6) def parse_directive(self, directive): name = directive[0][0] value = directive[0][1][0][1] if name == "seekto": target = int(value, 0) if self._offset == target: self.assert_unnecessary_seek('Unnecessary #seekto %s' % value) elif target < self._offset: self.assert_negative_seek( 'Invalid negative seek from 0x%04x to 0x%04x' % ( self._offset, target)) self._offset = target elif name == "seek": self._offset += int(value, 0) elif name == "printoffset": LOG.debug("%s: %i (0x%08X)" % (value[1:-1], self._offset, self._offset)) def parse_statement(self, type_, data): if type_ == "struct": self.parse_struct(data) elif type_ == "definition": self.parse_defn(data) elif type_ == "directive": self.parse_directive(data) elif type_ == "union": self.parse_union(data) def parse_block(self, lang): for t, d in lang: self.parse_statement(t, d) def parse(self, lang): self._lines = {} self._generators = structDataElement(self._data, self._offset) self.parse_block(lang) return self._generators def parse(spec, data, offset=0): ast = bitwise_grammar.parse(spec) p = Processor(data, offset, spec) return p.parse(ast) chirp-20261030/chirp/bitwise_grammar.py0000644000175000017500000000557515067711504016707 0ustar hibbyhibby# Copyright 2010 Dan Smith # # 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 . import re from chirp.pyPEG import keyword, parse as pypeg_parse TYPES = ["bit", "lbit", "u8", "u16", "ul16", "u24", "ul24", "u32", "ul32", "i8", "i16", "il16", "i24", "il24", "i32", "il32", "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 def _bitdeflist(): return bitdef, -1, (",", bitdef) def bitfield(): return _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, union, 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 union(): return keyword("union"), _block, [array, symbol], ";" def _language(): return _block_inner def parse(data): lines = data.split("\n") for index, line in enumerate(lines): if '//' in line: lines[index] = line[:line.index('//')] class FakeFileInput(object): """Simulate line-by-line file reading from @data""" line = -1 def isfirstline(self): return self.line == 0 def filename(self): return "input" def lineno(self): return self.line def __iter__(self): return self def __next__(self): self.line += 1 try: # Note, FileInput objects keep the newlines return lines[self.line] + "\n" except IndexError: raise StopIteration return pypeg_parse(_language, FakeFileInput()) chirp-20261030/chirp/checksum.py0000644000175000017500000000351415161444633015325 0ustar hibbyhibby# Copyright 2008 Dan Smith # Copyright 2023 Jacek Lipkowski # Copyright 2025 Thibaut Berg # # 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 . # This is commonly called "xmodem CRC16" CRC_POLY_CCITT = 0x1021 CRC_POLY_IBM_REV = 0xA001 def crc16(data, poly, reverse=False): crc = 0x0 for byte in data: if reverse: crc ^= byte else: crc ^= (byte << 8) for _ in range(8): if reverse: if crc & 0x0001: crc = (crc >> 1) ^ poly else: crc >>= 1 else: crc = crc << 1 if crc & 0x10000: crc = (crc ^ poly) & 0xFFFF return crc & 0xFFFF def crc16_xmodem(data: bytes): return crc16(data, CRC_POLY_CCITT) def crc16_ibm_rev(data): return crc16(data, CRC_POLY_IBM_REV, reverse=True) def checksum_8bit(data, modulo=256): """Calculate 8-bit checksum as sum of all bytes""" cs = 0 for byte in data: cs += byte return cs % modulo def checksum_xor(data): """Calculate checksum using XOR of all bytes""" cs = 0 for byte in data: cs ^= byte return cs & 0xFF chirp-20261030/chirp/chirp_common.py0000644000175000017500000022022315257672455016211 0ustar hibbyhibby# Copyright 2008 Dan Smith # # 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 . import base64 import json import inspect import logging import math import re import sys from chirp import errors, memmap, CHIRP_VERSION LOG = logging.getLogger(__name__) SEPCHAR = "," # 50 Tones TONES = ( 67.0, 69.3, 71.9, 74.4, 77.0, 79.7, 82.5, 85.4, 88.5, 91.5, 94.8, 97.4, 100.0, 103.5, 107.2, 110.9, 114.8, 118.8, 123.0, 127.3, 131.8, 136.5, 141.3, 146.2, 151.4, 156.7, 159.8, 162.2, 165.5, 167.9, 171.3, 173.8, 177.3, 179.9, 183.5, 186.2, 189.9, 192.8, 196.6, 199.5, 203.5, 206.5, 210.7, 218.1, 225.7, 229.1, 233.6, 241.8, 250.3, 254.1, ) OLD_TONES = tuple(x for x in sorted( set(TONES) - set([159.8, 165.5, 171.3, 177.3, 183.5, 189.9, 196.6, 199.5, 206.5, 229.1, 254.1]))) def VALIDTONE(v): return isinstance(v, float) and 50 < v < 300 # 104 DTCS Codes DTCS_CODES = ( 23, 25, 26, 31, 32, 36, 43, 47, 51, 53, 54, 65, 71, 72, 73, 74, 114, 115, 116, 122, 125, 131, 132, 134, 143, 145, 152, 155, 156, 162, 165, 172, 174, 205, 212, 223, 225, 226, 243, 244, 245, 246, 251, 252, 255, 261, 263, 265, 266, 271, 274, 306, 311, 315, 325, 331, 332, 343, 346, 351, 356, 364, 365, 371, 411, 412, 413, 423, 431, 432, 445, 446, 452, 454, 455, 462, 464, 465, 466, 503, 506, 516, 523, 526, 532, 546, 565, 606, 612, 624, 627, 631, 632, 654, 662, 664, 703, 712, 723, 731, 732, 734, 743, 754, ) # 512 Possible DTCS Codes ALL_DTCS_CODES = tuple([((a * 100) + (b * 10) + c) for a in range(0, 8) for b in range(0, 8) for c in range(0, 8)]) CROSS_MODES = ( "Tone->Tone", "DTCS->", "->DTCS", "Tone->DTCS", "DTCS->Tone", "->Tone", "DTCS->DTCS", "Tone->" ) # This is the "master" list of modes, and in general things should not be # added here without significant consideration. These must remain stable and # universal to allow importing memories between different radio vendors and # models. MODES = ("WFM", "FM", "NFM", "AM", "NAM", "DV", "USB", "LSB", "CW", "RTTY", "DIG", "PKT", "NCW", "NCWR", "CWR", "P25", "Auto", "RTTYR", "FSK", "FSKR", "DMR", "DN", "NXDN") 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, ) # These are the default for RadioFeatures.valid_tuning_steps COMMON_TUNING_STEPS = (5.0, 10.0, 15.0, 20.0, 25.0, 30.0, 50.0, 100.0) 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)]) CHARSET_1252 = bytes( [x for x in range(0x20, 0x100) if x not in [0x7F, 0x81, 0x8D, 0x8F, 0x90, 0x9D, 0xA0, 0xAD]] ).decode('cp1252') # http://aprs.org/aprs11/SSIDs.txt APRS_SSID = ( "0 Your primary station usually fixed and message capable", "1 generic additional station, digi, mobile, wx, etc", "2 generic additional station, digi, mobile, wx, etc", "3 generic additional station, digi, mobile, wx, etc", "4 generic additional station, digi, mobile, wx, etc", "5 Other networks (Dstar, Iphones, Androids, Blackberry's etc)", "6 Special activity, Satellite ops, camping or 6 meters, etc", "7 walkie talkies, HT's or other human portable", "8 boats, sailboats, RV's or second main mobile", "9 Primary Mobile (usually message capable)", "10 internet, Igates, echolink, winlink, AVRS, APRN, etc", "11 balloons, aircraft, spacecraft, etc", "12 APRStt, DTMF, RFID, devices, one-way trackers*, etc", "13 Weather stations", "14 Truckers or generally full time drivers", "15 generic additional station, digi, mobile, wx, etc") APRS_POSITION_COMMENT = ( "off duty", "en route", "in service", "returning", "committed", "special", "priority", "custom 0", "custom 1", "custom 2", "custom 3", "custom 4", "custom 5", "custom 6", "EMERGENCY") # http://aprs.org/symbols/symbolsX.txt APRS_SYMBOLS = ( "Police/Sheriff", "[reserved]", "Digi", "Phone", "DX Cluster", "HF Gateway", "Small Aircraft", "Mobile Satellite Groundstation", "Wheelchair", "Snowmobile", "Red Cross", "Boy Scouts", "House QTH (VHF)", "X", "Red Dot", "0 in Circle", "1 in Circle", "2 in Circle", "3 in Circle", "4 in Circle", "5 in Circle", "6 in Circle", "7 in Circle", "8 in Circle", "9 in Circle", "Fire", "Campground", "Motorcycle", "Railroad Engine", "Car", "File Server", "Hurricane Future Prediction", "Aid Station", "BBS or PBBS", "Canoe", "[reserved]", "Eyeball", "Tractor/Farm Vehicle", "Grid Square", "Hotel", "TCP/IP", "[reserved]", "School", "PC User", "MacAPRS", "NTS Station", "Balloon", "Police", "TBD", "Recreational Vehicle", "Space Shuttle", "SSTV", "Bus", "ATV", "National WX Service Site", "Helicopter", "Yacht/Sail Boat", "WinAPRS", "Human/Person", "Triangle", "Mail/Postoffice", "Large Aircraft", "WX Station", "Dish Antenna", "Ambulance", "Bicycle", "Incident Command Post", "Dual Garage/Fire Dept", "Horse/Equestrian", "Fire Truck", "Glider", "Hospital", "IOTA", "Jeep", "Truck", "Laptop", "Mic-Repeater", "Node", "Emergency Operations Center", "Rover (dog)", "Grid Square above 128m", "Repeater", "Ship/Power Boat", "Truck Stop", "Truck (18 wheeler)", "Van", "Water Station", "X-APRS", "Yagi at QTH", "TDB", "[reserved]" ) def watts_to_dBm(watts): """Converts @watts in watts to dBm""" return 10 * math.log10(watts) + 30 def dBm_to_watts(dBm): """Converts @dBm from dBm to watts""" return round(math.pow(10, dBm / 10) / 1000, 1) 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 = float(dBm) self._label = label def __str__(self): return str(self._label) def __int__(self): return int(self._power) def __float__(self): return self._power def __sub__(self, val): return float(self) - float(val) def __add__(self, val): return float(self) + float(val) def __eq__(self, val): if val is not None: return float(self) == float(val) return False def __lt__(self, val): return float(self) < float(val) def __gt__(self, val): return float(self) > float(val) def __bool__(self): return int(self) != 0 def __repr__(self): return "%s (%i dBm)" % (self._label, self._power) class AutoNamedPowerLevel(PowerLevel): """A power level that is simply named by its value in watts""" def __init__(self, watts): fmt = ('%iW' if watts >= 10 else '%.1fW') super().__init__(fmt % watts, watts=watts) def parse_power(powerstr): if powerstr.isdigit(): # All digits means watts watts = int(powerstr) else: match = re.match(r'^\s*([0-9.]+)\s*([Ww]?)\s*$', powerstr) if not match: raise ValueError('Invalid power specification: %r' % powerstr) if match.group(2).lower() in ('', 'w'): watts = float(match.group(1)) else: raise ValueError('Unknown power units in %r' % powerstr) return AutoNamedPowerLevel(watts) def parse_freq(freqstr: str) -> int: """Parse a frequency string and return the value in integral Hz""" freqstr = freqstr.strip() if freqstr == "": return 0 elif freqstr.endswith(" MHz"): return parse_freq(freqstr.split(" ")[0]) elif freqstr.endswith(" kHz"): return int(freqstr.split(" ")[0]) * 1000 if "." in freqstr: _mhz, _khz = freqstr.split(".") if _mhz == "": _mhz = "0" _khz = _khz.ljust(6, "0") if len(_khz) > 6: raise ValueError("Invalid kHz value: %s", _khz) mhz = int(_mhz) * 1000000 khz = int(_khz) else: mhz = int(freqstr) * 1000000 khz = 0 return mhz + khz def format_freq(freq: int) -> str: """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: int = 0 number: int = 0 extd_number: str = "" name: str = "" vfo: int = 0 rtone: float = 88.5 ctone: float = 88.5 dtcs: int = 23 rx_dtcs: int = 23 tmode: str = "" cross_mode: str = "Tone->Tone" dtcs_polarity: str = "NN" skip: str = "" power: PowerLevel | None = None duplex: str = "" offset: int = 600000 mode: str = "FM" tuning_step: float = 5.0 comment: str = "" empty: bool = False immutable: list[str] = [] # A RadioSettingGroup of additional settings supported by the radio, # or an empty list if none extra = [] def __init__(self, number=0, empty=False, name=""): self.freq = 0 self.number = number self.extd_number = "" self.name = 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 = empty self.immutable = [] _valid_map = { "rtone": VALIDTONE, "ctone": VALIDTONE, "dtcs": ALL_DTCS_CODES, "rx_dtcs": ALL_DTCS_CODES, "tmode": TONE_MODES, "dtcs_polarity": ["NN", "NR", "RN", "RR"], "cross_mode": CROSS_MODES, "mode": MODES, "duplex": ["", "+", "-", "split", "off"], "skip": SKIP_VALUES, "empty": [True, False], "dv_code": [x for x in range(0, 100)], } def __repr__(self): ident, vals = self.debug_dump() return '' % ( ident, ','.join('%s=%r' % item for item in vals)) def debug_diff(self, other, delim='/'): my_ident, my_vals = self.debug_dump() my_vals = dict(my_vals) om_ident, om_vals = other.debug_dump() om_vals = dict(om_vals) diffs = [] if my_ident != om_ident: diffs.append('ident=%s%s%s' % (my_ident, delim, om_ident)) for k in sorted(my_vals.keys() | om_vals.keys()): myval = my_vals.get(k, '') omval = om_vals.get(k, '') if myval != omval: diffs.append('%s=%r%s%r' % (k, myval, delim, omval)) return ','.join(diffs) def debug_dump(self): vals = [(k, v) for k, v in self.__dict__.items() if k not in ('extra', 'number', 'extd_number')] for extra in self.extra: vals.append(('extra.%s' % extra.get_name(), str(extra.value))) if self.extd_number: ident = '%s(%i)' % (self.extd_number, self.number) else: ident = str(self.number) return ident, vals def dupe(self): """Return a deep copy of @self""" mem = self.__class__() for k, v in list(self.__dict__.items()): mem.__dict__[k] = v return mem def clone(self, source): """Absorb all of the properties of @source""" for k, v in list(source.__dict__.items()): self.__dict__[k] = v CSV_FORMAT = ["Location", "Name", "Frequency", "Duplex", "Offset", "Tone", "rToneFreq", "cToneFreq", "DtcsCode", "DtcsPolarity", "RxDtcsCode", "CrossMode", "Mode", "TStep", "Skip", "Power", "Comment", "URCALL", "RPT1CALL", "RPT2CALL", "DVCODE"] def __setattr__(self, name, val): if not hasattr(self, name): raise ValueError("No such attribute `%s'" % name) if name in self.immutable: raise ImmutableValueError("Field %s is not " % name + "mutable on this memory") if name in self._valid_map: valid = self._valid_map[name] if callable(valid): if not valid(val): raise ValueError("`%s' is not a valid value for `%s'" % ( val, name)) elif val not in self._valid_map[name]: raise ValueError("`%s' is not in valid list: %s" % (val, self._valid_map[name])) self.__dict__[name] = val def format_freq(self): """Return a properly-formatted string of this memory's frequency""" return format_freq(self.freq) def parse_freq(self, freqstr): """Set the frequency from a string""" self.freq = parse_freq(freqstr) return self.freq def __str__(self): if self.tmode == "Tone": tenc = "*" else: tenc = " " if self.tmode == "TSQL": tsql = "*" else: tsql = " " if self.tmode == "DTCS": dtcs = "*" else: dtcs = " " if self.duplex == "": dup = "/" else: dup = self.duplex return \ "Memory %s: %s%s%s %s (%s) r%.1f%s c%.1f%s d%03i%s%s [%.2f]" % \ (self.number if self.extd_number == "" else self.extd_number, format_freq(self.freq), dup, format_freq(self.offset), self.mode, self.name, self.rtone, tenc, self.ctone, tsql, self.dtcs, dtcs, self.dtcs_polarity, self.tuning_step) def to_csv(self): """Return a CSV representation of this memory""" return [ "%i" % self.number, "%s" % self.name, format_freq(self.freq), "%s" % self.duplex, format_freq(self.offset), "%s" % self.tmode, "%.1f" % self.rtone, "%.1f" % self.ctone, "%03i" % self.dtcs, "%s" % self.dtcs_polarity, "%03i" % self.rx_dtcs, "%s" % self.cross_mode, "%s" % self.mode, "%.2f" % self.tuning_step, "%s" % self.skip, "%s" % self.power, "%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 Exception: raise errors.InvalidDataError( "Location '%s' is not a valid integer" % vals[0]) self.name = vals[1] try: self.freq = to_MHz(float(vals[2])) except Exception: 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 = to_MHz(float(vals[4])) except Exception: 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 Exception: 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 Exception: 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 Exception: 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") if vals[9] in ["NN", "NR", "RN", "RR"]: self.dtcs_polarity = vals[9] else: raise errors.InvalidDataError("DtcsPolarity is not valid") try: self.rx_dtcs = int(vals[10], 10) except Exception: 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") self.cross_mode = vals[11] if vals[12] in MODES: self.mode = vals[12] else: raise errors.InvalidDataError("Mode %r is not valid" % vals[10]) try: self.tuning_step = float(vals[13]) except Exception: raise errors.InvalidDataError("Tuning step is invalid") try: self.skip = vals[14] except Exception: raise errors.InvalidDataError("Skip value is not valid") return True class DVMemory(Memory): """A Memory with D-STAR attributes""" dv_urcall: str = "CQCQCQ" dv_rpt1call: str = "" dv_rpt2call: str = "" dv_code: int = 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 def FrozenMemory(source, strict=True): class _FrozenMemory(source.__class__): def __init__(self, source): self.__dict__['_frozen'] = False for k, v in source.__dict__.items(): if k == '_frozen': continue try: setattr(self, k, v) except ValueError: pass self.__dict__['_frozen'] = True for i in self.extra: i.set_frozen() def __setattr__(self, k, v): if self._frozen: # This should really be an error, but we have a number of # drivers that make modifications during set_memory(). If we # are not being strict, then we just log a warning. caller = inspect.getframeinfo(inspect.stack()[1][0]) LOG.warning( '%s@%i: Illegal set on attribute %s - Fix this driver!' % ( caller.filename, caller.lineno, k)) if strict: raise errors.FrozenMemoryError( _('Internal driver error: Attempt to modify ' 'frozen memory; Please report this as a bug using ' '"Report or update a bug" in the Help menu')) super().__setattr__(k, v) def dupe(self): m = Memory() m.clone(self) delattr(m, '_frozen') return m return _FrozenMemory(source) class MemoryMapping(object): """Base class for a memory mapping""" def __init__(self, model, index, name): self._model = model self._index = index self._name = name def __str__(self): return self.get_name() def __repr__(self): return "%s-%s" % (self.__class__.__name__, self._index) def get_name(self): """Returns the mapping name""" return self._name def get_index(self): """Returns the immutable index (string or int)""" return self._index def __eq__(self, other): return self.get_index() == other.get_index() class MappingModel(object): """Base class for a memory mapping model""" def __init__(self, radio, name): self._radio = radio self._name = name def get_name(self): return self._name def get_num_mappings(self): """Returns the number of mappings in the model (should be callable without consulting the radio""" raise NotImplementedError() def get_mappings(self): """Return a list of mappings""" raise NotImplementedError() def add_memory_to_mapping(self, memory, mapping): """Add @memory to @mapping.""" raise NotImplementedError() def remove_memory_from_mapping(self, memory, mapping): """Remove @memory from @mapping. Shall raise exception if @memory is not in @bank""" raise NotImplementedError() def get_mapping_memories(self, mapping): """Return a list of memories in @mapping""" raise NotImplementedError() def get_memory_mappings(self, memory): """Return a list of mappings that @memory is in""" raise NotImplementedError() class Bank(MemoryMapping): """Base class for a radio's Bank""" class NamedBank(Bank): """A bank that can have a name""" def set_name(self, name): """Changes the user-adjustable bank name""" self._name = name class BankModel(MappingModel): """A bank model where one memory is in zero or one banks at any point""" def __init__(self, radio, name='Banks'): super(BankModel, self).__init__(radio, name) class StaticBank(Bank): pass class StaticBankModel(BankModel): MSG = 'This radio has fixed banks and does not allow reassignment' channelAlwaysHasBank = True """A BankModel that shows a static mapping but does not allow changes.""" def __init__(self, radio, name='Banks', banks=10): super().__init__(radio, name=name) self._num_banks = banks self._rf = radio.get_features() self._banks = [] for i in range(self._num_banks): self._banks.append(StaticBank(self, i + 1, 'Bank')) def get_num_mappings(self): return self._num_banks def get_mappings(self): return self._banks def get_mapping_memories(self, bank): lo, hi = self._rf.memory_bounds count = (hi - lo + 1) / self._num_banks offset = lo + ((bank.get_index() - 1) * count) return [self._radio.get_memory(offset + i) for i in range(count)] def get_memory_mappings(self, memory): lo, hi = self._rf.memory_bounds mems = hi - lo + 1 count = mems // self._num_banks return [self._banks[(memory.number - lo) // count]] def remove_memory_from_mapping(self, memory, mapping): raise errors.RadioFixedBanks() def add_memory_to_mapping(self, memory, mapping): raise errors.RadioFixedBanks() class MappingModelIndexInterface: """Interface for mappings with index capabilities""" def get_index_bounds(self): """Returns a tuple (lo,hi) of the min and max mapping indices""" raise NotImplementedError() def get_memory_index(self, memory, mapping): """Returns the index of @memory in @mapping""" raise NotImplementedError() def set_memory_index(self, memory, mapping, index): """Sets the index of @memory in @mapping to @index""" raise NotImplementedError() def get_next_mapping_index(self, mapping): """Returns the next available mapping index in @mapping, or raises Exception if full""" raise NotImplementedError() class MTOBankModel(BankModel): """A bank model where one memory can be in multiple banks at once """ pass def console_status(status): """Write a status object to the console""" import logging from chirp import logger if not logger.is_visible(logging.WARN): return import sys import os sys.stdout.write("\r%s" % status) if status.cur == status.max: sys.stdout.write(os.linesep) class RadioPrompts: """Radio prompt strings""" info = None experimental = None pre_download = None pre_upload = None display_pre_upload_prompt_before_opening_port = True def BOOLEAN(v): assert v in (True, False) def LIST(v): assert hasattr(v, '__iter__') def LIST_NONZERO_INT(v): assert all(x > 0 for x in v) def INT(min=0, max=None): def checkint(v): assert isinstance(v, int) assert v >= min if max is not None: assert v <= max return checkint def STRING(v): assert isinstance(v, str) def NTUPLE(size): def checktuple(v): assert len(v) == size return checktuple def TONELIST(v): assert all(VALIDTONE(x) for x in v) 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, "has_variable_power": BOOLEAN, "has_dynamic_subdevices": BOOLEAN, # Attributes "valid_modes": LIST, "valid_tmodes": LIST, "valid_duplexes": LIST, "valid_tuning_steps": LIST_NONZERO_INT, "valid_bands": LIST, "valid_skips": LIST, "valid_power_levels": LIST, "valid_characters": STRING, "valid_name_length": INT(), "valid_cross_modes": LIST, "valid_tones": TONELIST, "valid_dtcs_pols": LIST, "valid_dtcs_codes": LIST, "valid_special_chans": LIST, "has_sub_devices": BOOLEAN, "memory_bounds": NTUPLE(2), "can_odd_split": BOOLEAN, "can_delete": BOOLEAN, # D-STAR "requires_call_lists": BOOLEAN, "has_implicit_calls": BOOLEAN, } def __setattr__(self, name, val): if name.startswith("_"): self.__dict__[name] = val return elif name not in list(self._valid_map.keys()): raise ValueError("No such attribute `%s'" % name) try: self._valid_map[name](val) except AssertionError: raise ValueError('Invalid value %r for attribute %r' % ( 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("has_variable_power", False, "Indicates the radio supports any power level between the " "min and max in valid_power_levels") self.init("has_dynamic_subdevices", False, "Indicates the radio has a non-static list of subdevices") 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(COMMON_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_tones", list(TONES), "Support Tones") self.init("valid_dtcs_pols", ["NN", "RN", "NR", "RR"], "Supported DTCS polarities") self.init("valid_dtcs_codes", list(DTCS_CODES), "Supported DTCS codes") self.init("valid_special_chans", [], "Supported special channel names") self.init("has_sub_devices", False, "Indicates that the radio behaves as two semi-independent " + "devices") self.init("memory_bounds", (0, 1), "The minimum and maximum channel numbers") self.init("can_odd_split", False, "Indicates that the radio can store an independent " + "transmit frequency") self.init("can_delete", True, "Indicates that the radio can delete memories") 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 list(self._valid_map.keys()) def __getitem__(self, name): return self.__dict__[name] @property def concise_bands(self): pp_range_strings = [ format_freq(lo).rstrip('0').rstrip('.') + '-' + format_freq(hi).rstrip('0').rstrip('.') + 'MHz' for lo, hi in self.valid_bands] return ', '.join(pp_range_strings) def validate_memory(self, mem): """Return a list of warnings and errors that will be encountered if trying to set @mem on the current radio. An Error will abort the edit, while a Warning will allow it. Both will show a message to the user. Use Errors to reject things that need to be. A Warning is useful to alert the user that something will be fixed/changed/coerced by the driver and that they should expect it to look different than what they entered. Use these sparingly as they can be very annoying to the user if they happen on every edit. NOTE: This method MAY NOT modify @mem in any way. It is not a hook to make changes, it is for _validation_ of the memory before it is sent to the driver's set_memory() method. """ 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 'mode' not in mem.immutable 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.valid_tones and mem.rtone not in self.valid_tones: msg = ValidationError("Tone %.1f not supported" % mem.rtone) msgs.append(msg) if self.valid_tones and mem.ctone not in self.valid_tones: msg = ValidationError("Tone %.1f not supported" % mem.ctone) msgs.append(msg) if self.has_dtcs_polarity and \ mem.dtcs_polarity not in self.valid_dtcs_pols: msg = ValidationError("DTCS Polarity %s not supported" % mem.dtcs_polarity) msgs.append(msg) if self.valid_dtcs_codes and \ mem.dtcs not in self.valid_dtcs_codes: msg = ValidationError("DTCS Code %03i not supported" % mem.dtcs) msgs.append(msg) if self.valid_dtcs_codes and \ mem.rx_dtcs not in self.valid_dtcs_codes: msg = ValidationError("DTCS Code %03i not supported" % mem.rx_dtcs) 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 ranges {ranges}").format( freq=format_freq(mem.freq), ranges=self.concise_bands)) msgs.append(msg) if self.valid_bands and \ self.valid_duplexes and \ mem.duplex in ["split", "-", "+"]: if mem.duplex == "split": freq = mem.offset elif mem.duplex == "-": freq = mem.freq - mem.offset elif mem.duplex == "+": freq = mem.freq + mem.offset valid = False for lo, hi in self.valid_bands: if lo <= freq < hi: valid = True break if not valid: msg = ValidationError( ("Tx freq {freq} is out " "of supported range").format(freq=format_freq(freq))) msgs.append(msg) if mem.power and self.valid_power_levels: if self.has_variable_power: if (mem.power < min(self.valid_power_levels) or mem.power > max(self.valid_power_levels)): msg = ValidationWarning( "Power level %s is out of radio's range" % mem.power) msgs.append(msg) else: if 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: required_step(mem.freq, self.valid_tuning_steps) except errors.InvalidDataError as e: msgs.append(ValidationError(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 if is_airband(mem.freq): try: fix_rounded_step(mem.freq) except errors.InvalidDataError as e: msgs.append(ValidationError( '%s: %s' % (format_freq(mem.freq), e))) 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 def split_validation_msgs(msgs): """Split a list of msgs into warnings,errors""" return ([x for x in msgs if isinstance(x, ValidationWarning)], [x for x in msgs if isinstance(x, ValidationError)]) class Alias(object): VENDOR = "Unknown" MODEL = "Unknown" VARIANT = "" class Radio(Alias): """Base class for all Radio drivers""" BAUD_RATE = 9600 # Whether or not we should use RTS/CTS flow control HARDWARE_FLOW = False # Whether or not we should assert DTR when opening the serial port WANTS_DTR = True # Whether or not we should assert RTS when opening the serial port WANTS_RTS = True ALIASES = [] NEEDS_COMPAT_SERIAL = False FORMATS: list[str] = [] 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) -> RadioFeatures: """Return a RadioFeatures object for this radio""" return RadioFeatures() @classmethod def get_name(cls) -> str: """Return a printable name for this radio""" return "%s %s" % (cls.VENDOR, cls.MODEL) @classmethod def get_prompts(cls) -> RadioPrompts: """Return a set of strings for use in prompts""" return RadioPrompts() def set_pipe(self, pipe) -> None: """Set the serial object to be used for communications""" self.pipe = pipe def get_memory(self, number: int | str) -> Memory: """Return a Memory object for the memory at location @number Constructs and returns a generic Memory object for the given location in the radio's memory. The memory should accurately represent what is actually stored in the radio as closely as possible. If the radio does not support changing some attributes of the location in question, the Memory.immutable list should be set appropriately. NB: No changes to the radio's memory should occur as a result of calling get_memory(). """ raise NotImplementedError() def erase_memory(self, number: int | str) -> None: """Erase memory at location @number""" mem = Memory() if isinstance(number, str): mem.extd_number = number else: 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: Memory) -> None: """Set the memory object @memory This method should copy generic attributes from @memory to the radio's memory. It should not modify @memory and it should reproduce the generic attributes on @memory in the radio's memory as faithfully as the radio allows. Attributes that can't be copied exactly should be warned in validate_memory() with ValidationWarnings if a substitution will be made, or ValidationError if truly incompatible. In the latter case, set_memory() will not be called. """ raise NotImplementedError() def get_mapping_models(self): """Returns a list of MappingModel objects (or an empty list)""" if hasattr(self, "get_bank_model"): # FIXME: Backwards compatibility for old bank models bank_model = self.get_bank_model() if bank_model: return [bank_model] return [] def get_raw_memory(self, number: int | str) -> str: """Return a raw string describing the memory at @number""" return 'Memory<%r>' % number def filter_name(self, name: str) -> str: """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) -> list[Alias]: """Return a list of sub-device Radio objects, if RadioFeatures.has_sub_devices is True""" return [] def validate_memory(self, mem: Memory) -> list[ValidationMessage]: """Return a list of warnings and errors that will be encountered if trying to set @mem on the current radio""" rf = self.get_features() return rf.validate_memory(mem) def get_settings(self): """Returns a RadioSettings list containing one or more RadioSettingGroup or RadioSetting objects. These represent general setting knobs and dials that can be adjusted on the radio. If this function is implemented, the has_settings RadioFeatures flag should be True and set_settings() must be implemented as well.""" pass def set_settings(self, settings): """Accepts the top-level RadioSettingGroup returned from get_settings() and adjusts the values in the radio accordingly. This function expects the entire RadioSettingGroup hierarchy returned from get_settings(). If this function is implemented, the has_settings RadioFeatures flag should be True and get_settings() must be implemented as well.""" pass @classmethod def supports_format(cls, fmt: str) -> bool: """Returns true if file format @fmt is supported by this radio. This really should not be overridden by implementations without a good reason (like excluding one). """ return fmt in cls.FORMATS def check_set_memory_immutable_policy(self, existing: Memory, new: Memory): """Checks whether or not a new memory will violate policy. Some radios have complex requirements for which fields of which memories can be modified at any given point. For the most part, radios require certain physical memory slots to have immutable fields (such as labels on call channels), and this default implementation checks that policy. However, other radios have more fine-grained rules in order to comply with FCC type acceptance, which requires overriding this behavior. ** This should almost never be overridden in your driver. ** This must not communicate with the radio, if implemented on a live- mode driver. """ for field in existing.immutable: if getattr(existing, field) != getattr(new, field): raise ImmutableValueError( 'Field %s is not mutable on this memory' % field) class ExternalMemoryProperties: """A mixin class that provides external memory property support. This is for use by drivers that have some way of storing additional memory properties externally (i.e. in metadata or a separate region) that cannot be loaded/updated during get_memory()/set_memory(). Implementing this is much less ideal than supporting those properties directly, so this should only be used when absolutely necessary. """ def get_memory_extra(self, memory): """Update @memory with extra fields. This is called after get_memory() and is passed the result for augmentation from external storage. """ return memory def set_memory_extra(self, memory): """Update external storage of properties from @memory. This is called after set_memory() with the same memory object to record additional properties in external storage. """ pass def erase_memory_extra(self, number): """Erase external storage for @memory. This is called after erase_memory() to clear external storage for memory @number. """ pass def link_device_metadata(self, devices): """Link sub-device metadata with parent. This is called after get_sub_devices() to make sure that the sub-device instances use a reference into the main radio's metadata. In most cases, this does not need to be overridden. """ # Link metadata of the sub-devices into the main by variant name # so that they are both included in a later save of the parent. for sub in devices: self._metadata.setdefault(sub.VARIANT, {}) sub._metadata = self._metadata[sub.VARIANT] class FileBackedRadio(Radio): """A file-backed radio stores its data in a file""" FILE_EXTENSION = 'dat' def save(self, filename): """Save the radio's memory map to @filename""" pass def load(self, filename): """Load the radio's memory map object from @filename""" pass def class_detected_models_attribute(cls): return 'DETECTED_MODELS_%s' % cls.__name__ class DetectableInterface: @classmethod def detect_from_serial(cls, pipe): """Communicate with the radio via serial to determine proper class Returns an in implementation of CloneModeRadio if detected, or raises RadioError if not. If NotImplemented is raised, we assume that no detection is possible or necessary. """ detected = getattr(cls, class_detected_models_attribute(cls), None) assert detected is None, ( 'Class has detected models but no detect_from_serial() ' 'implementation') raise NotImplementedError() @classmethod def detected_models(cls, include_self=True): detected = getattr(cls, class_detected_models_attribute(cls), []) # Only include this class if it is registered if include_self and hasattr(cls, '_DETECTED_BY'): extra = [cls] else: extra = [] return extra + list(detected) @classmethod def is_detected(cls): return getattr(cls, '_DETECTED_BY', None) is not None @classmethod def detect_model(cls, detected_cls): detected_attr = class_detected_models_attribute(cls) if getattr(cls, detected_attr, None) is None: setattr(cls, detected_attr, []) getattr(cls, detected_attr).append(detected_cls) @classmethod def is_minor_variant(cls): return getattr(cls, '_MINOR_VARIANT', False) class CloneModeRadio(FileBackedRadio, ExternalMemoryProperties, DetectableInterface): """A clone-mode radio does a full memory dump in and out and we store an image of the radio into an image file""" FILE_EXTENSION = "img" MAGIC = b'\x00\xffchirp\xeeimg\x00\x01' _memsize = 0 def __init__(self, pipe): self.errors = [] self._mmap = None self._memobj = None self._metadata = {} if isinstance(pipe, str): self.pipe = None self.load_mmap(pipe) elif isinstance(pipe, memmap.MemoryMapBytes): 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 NOTE: This should not be implemented by new drivers UNLESS you need to recognize a non-CHIRP-native file format, such as files from the OEM software or similar. Those must be registered via directory.register_format() in order to be properly recognized. """ return False def sync_in(self): """Initiate a radio-to-PC clone operation""" pass def sync_out(self): """Initiate a PC-to-radio clone operation""" pass 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 @classmethod def _strip_metadata(cls, raw_data): try: idx = raw_data.index(cls.MAGIC) except ValueError: LOG.debug('Image data has no metadata blob') return raw_data, {} # Find the beginning of the base64 blob raw_metadata = raw_data[idx + len(cls.MAGIC):] metadata = {} try: metadata = json.loads(base64.b64decode(raw_metadata).decode()) except ValueError as e: LOG.error('Failed to parse decoded metadata blob: %s' % e) except TypeError as e: LOG.error('Failed to decode metadata blob: %s' % e) if metadata: LOG.debug('Loaded metadata: %s' % metadata) return raw_data[:idx], metadata def _make_metadata(self): # Always generate these directly from our in-memory state base = { 'rclass': self.__class__.__name__, 'vendor': self.VENDOR, 'model': self.MODEL, 'variant': self.VARIANT, 'chirp_version': CHIRP_VERSION, } # Any other properties take a back seat to the above extra = {k: v for k, v in self._metadata.items() if k not in base} extra.update(base) return base64.b64encode(json.dumps(extra).encode()) def load_mmap(self, filename): """Load the radio's memory map from @filename""" mapfile = open(filename, "rb") data = mapfile.read() if self.MAGIC in data: data, self._metadata = self._strip_metadata(data) if ('chirp_version' in self._metadata and is_version_newer(self._metadata.get('chirp_version'))): LOG.warning('Image is from version %s but we are %s' % ( self._metadata.get('chirp_version'), CHIRP_VERSION)) if self.NEEDS_COMPAT_SERIAL: self._mmap = memmap.MemoryMap(data) else: self._mmap = memmap.MemoryMapBytes(bytes(data)) 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 = open(filename, "wb") mapfile.write(self._mmap.get_byte_compatible().get_packed()) if filename.lower().endswith(".img"): mapfile.write(self.MAGIC) mapfile.write(self._make_metadata()) mapfile.close() except IOError: raise Exception("File Access Error") def get_mmap(self): """Return the radio's memory map object""" return self._mmap @property def metadata(self): return dict(self._metadata) @metadata.setter def metadata(self, values): self._metadata.update(values) def get_memory_extra(self, memory): rf = self.get_features() if not rf.has_comment and isinstance(memory.number, int): self._metadata.setdefault('mem_extra', {}) try: memory.comment = self._metadata['mem_extra'].get( '%04i_comment' % memory.number, '') except ImmutableValueError: pass return memory def set_memory_extra(self, memory): rf = self.get_features() if not rf.has_comment and isinstance(memory.number, int): self._metadata.setdefault('mem_extra', {}) key = '%04i_comment' % memory.number if not memory.comment: self._metadata['mem_extra'].pop(key, None) else: self._metadata['mem_extra'][key] = memory.comment def erase_memory_extra(self, number): rf = self.get_features() if not rf.has_comment and isinstance(number, int): self._metadata.setdefault('mem_extra', {}) self._metadata['mem_extra'].pop('%04i_comment' % number, None) class LiveRadio(Radio, DetectableInterface): """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.5 kHz or 6.25 kHz 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 5 kHz step""" return (freq % 5000) == 0 def is_10_0(freq): """Returns True if @freq is reachable by a 10 kHz step""" return (freq % 10000) == 0 def is_12_5(freq): """Returns True if @freq is reachable by a 12.5 kHz step""" return (freq % 12500) == 0 def is_6_25(freq): """Returns True if @freq is reachable by a 6.25 kHz step""" return (freq % 6250) == 0 def is_2_5(freq): """Returns True if @freq is reachable by a 2.5 kHz step""" return (freq % 2500) == 0 def is_8_33(freq): """Returns True if @freq is reachable by a 8.33 kHz step""" return (freq % 25000) in [8330, 8333, 16660, 16666] def is_1_0(freq): """Returns True if @freq is reachable by a 1.0 kHz step""" return (freq % 1000) == 0 def is_0_5(freq): """Returns True if @freq is reachable by a 0.5 kHz step""" return (freq % 500) == 0 def make_is(stephz): def validator(freq): return freq % stephz == 0 return validator def required_step(freq, allowed=None): """Returns the simplest tuning step that is required to reach @freq""" if allowed is None: allowed = [5.0, 10.0, 12.5, 6.25, 2.5, 1.0, 0.5, 8.33, 0.25] special = {8.33: is_8_33} for step in allowed: if step in special: validate = special[step] else: validate = make_is(int(step * 1000)) if validate(freq): LOG.debug('Chose step %s for %s' % (step, format_freq(freq))) return step raise errors.InvalidDataError("Unable to find a supported " + "tuning step for %s" % format_freq(freq)) def is_airband(freq): """Returns True if @freq is in the airband range""" return in_range(freq, [(to_MHz(108), to_MHz(137))]) def fix_rounded_step(freq): """Some radios imply the last bit of 12.5 kHz and 6.25 kHz step frequencies. Take the base @freq and return the corrected one""" allowed = [12.5, 6.25] if is_airband(freq): # Airband can be 25kHz or 8.33kHz (25k / 3) steps # https://en.wikipedia.org/wiki/Airband if freq % 25000: # This must be 8.33kHz. The goal here is to find the closest # 8.33k-aligned channel and return that. # In the 8.33kHz channel scheme, there are "channel names" that # look like 5kHz-aligned frequencies within the 25kHz regular # channels, which don't match the actual frequency being used # (which is 8.33kHz-aligned). We need to be able to detect those # and return the correct actual frequency, as well as allow # matching the actual frequencies themselves. # The "base frequency" is the 25kHz-aligned channel frequency that # has been divided into three 8.33kHz channels. base = freq // 25000 * 25000 orig = freq # This is a channel name if it is 5kHz-aligned and equal-or-above # the base frequency. Adjust down for the lower channel and up for # the upper (no adjustment for the middle one) so that the channel # matching below will find the right closest option. ch_index = (freq - base) / 5000 if ch_index == 1.0: # Lower of the three, bump down freq -= (25000 // 3) elif ch_index == 3.0: # Upper of the three, bump up freq += (25000 // 3) elif ch_index >= 4.0 and ch_index == int(ch_index): # This is 5kHz-aligned but not one of the three sub-channels, # thus this is one of the gaps in the channel numbers. Refuse # it so that it's clear to the user. raise errors.InvalidDataError( _('Aircraft frequencies must be aligned to 25kHz, 8.33kHz' ', be or a valid channel')) # These are the three possible 8.33kHz-aligned channels within # this 25kHz block. channels = [base + (25000 // 3) * i for i in range(0, 3)] # Find the closest one to the original frequency best = min(channels, key=lambda x: abs(x - freq)) LOG.debug('833: Orig %s Channels %s best %s adjusted %s diffs %s ' 'chindex %s' % ( orig, channels, best, freq, [x - freq for x in channels], ch_index)) return best else: return freq try: required_step(freq + 500, allowed=allowed) return freq + 500 except errors.InvalidDataError: pass try: required_step(freq + 250, allowed=allowed) return freq + 250 except errors.InvalidDataError: pass try: required_step(freq + 750, allowed=allowed) return float(freq + 750) except errors.InvalidDataError: pass try: required_step(freq + 330, allowed=allowed) return float(freq + 330) except errors.InvalidDataError: pass try: required_step(freq + 660, allowed=allowed) return float(freq + 660) except errors.InvalidDataError: pass # These radios can all resolve 5kHz, so make sure what we are left with # is 5kHz-aligned, else we refuse below. try: required_step(freq, allowed=[5.0]) return freq 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 int(val * 1000000000) def to_MHz(val): """Convert @val in MHz to Hz""" return int(val * 1000000) def to_kHz(val): """Convert @val in kHz to Hz""" return int(val * 1000) def from_GHz(val): """Convert @val in Hz to GHz""" return val // 1000000000 def from_MHz(val): """Convert @val in Hz to MHz""" return val // 1000000 def from_kHz(val): """Convert @val in Hz to kHz""" return val // 1000 def split_to_offset(mem, rxfreq, txfreq): """Set the freq, offset, and duplex fields of a memory based on a separate rx/tx frequency. """ mem.freq = rxfreq if abs(txfreq - rxfreq) > to_MHz(70): mem.offset = txfreq mem.duplex = 'split' else: offset = txfreq - rxfreq if offset < 0: mem.duplex = '-' elif offset > 0: mem.duplex = '+' mem.offset = abs(offset) def split_tone_decode(mem, txtone, rxtone): """ Set tone mode and values on @mem based on txtone and rxtone specs like: None, None, None "Tone", 123.0, None "DTCS", 23, "N" """ txmode, txval, txpol = txtone rxmode, rxval, rxpol = rxtone mem.dtcs_polarity = "%s%s" % (txpol or "N", rxpol or "N") if not txmode and not rxmode: # No tone return if txmode == "Tone" and not rxmode: mem.tmode = "Tone" mem.rtone = txval return if txmode == rxmode == "Tone" and txval == rxval: # TX and RX same tone -> TSQL mem.tmode = "TSQL" mem.ctone = txval return if txmode == rxmode == "DTCS" and txval == rxval: mem.tmode = "DTCS" mem.dtcs = txval return mem.tmode = "Cross" mem.cross_mode = "%s->%s" % (txmode or "", rxmode or "") if txmode == "Tone": mem.rtone = txval elif txmode == "DTCS": mem.dtcs = txval if rxmode == "Tone": mem.ctone = rxval elif rxmode == "DTCS": mem.rx_dtcs = rxval def split_tone_encode(mem): """ Returns TX, RX tone specs based on @mem like: None, None, None "Tone", 123.0, None "DTCS", 23, "N" """ txmode = '' rxmode = '' txval = None rxval = None if mem.tmode == "Tone": txmode = "Tone" txval = mem.rtone elif mem.tmode == "TSQL": txmode = rxmode = "Tone" txval = rxval = mem.ctone elif mem.tmode == "DTCS": txmode = rxmode = "DTCS" txval = rxval = mem.dtcs elif mem.tmode == "Cross": txmode, rxmode = mem.cross_mode.split("->", 1) if txmode == "Tone": txval = mem.rtone elif txmode == "DTCS": txval = mem.dtcs if rxmode == "Tone": rxval = mem.ctone elif rxmode == "DTCS": rxval = mem.rx_dtcs if txmode == "DTCS": txpol = mem.dtcs_polarity[0] else: txpol = None if rxmode == "DTCS": rxpol = mem.dtcs_polarity[1] else: rxpol = None return ((txmode, txval, txpol), (rxmode, rxval, rxpol)) def sanitize_string(astring, validcharset=CHARSET_ASCII, replacechar='*'): myfilter = ''.join( [ [replacechar, chr(x)][chr(x) in validcharset] for x in range(256) ]) return astring.translate(myfilter) def is_version_newer(version): """Return True if version is newer than ours""" def get_version(v): if v.startswith('daily-'): _, stamp = v.split('-', 1) ver = (int(stamp),) elif '.' in v: ver = tuple(int(p) for p in v.split('.')) else: ver = (0,) LOG.debug('Parsed version %r to %r' % (v, ver)) return ver from chirp import CHIRP_VERSION try: version = get_version(version) except ValueError as e: LOG.error('Failed to parse version %r: %s' % (version, e)) version = (0,) try: my_version = get_version(CHIRP_VERSION) except ValueError as e: LOG.error('Failed to parse my version %r: %s' % (CHIRP_VERSION, e)) my_version = (0,) return version > my_version def http_user_agent(): ver = sys.version_info return 'chirp/%s (Python %i.%i.%i on %s)' % ( CHIRP_VERSION, ver.major, ver.minor, ver.micro, sys.platform) def urlretrieve(url, fn): """Grab an URL and save it in a specified file""" import urllib.request import urllib.error headers = { 'User-Agent': http_user_agent(), } req = urllib.request.Request(url, headers=headers) resp = urllib.request.urlopen(req) with open(fn, 'wb') as f: f.write(resp.read()) def mem_from_tsv(tsv_text): fields = tsv_text.split('\t') if len(fields) < 13: raise ValueError('Not enough fields to be a memory') mem = Memory() mem.really_from_csv(fields) print('Parsed %s from tsv' % mem) return mem def mem_from_text(text): if text.count('\t') > 10: # Seems like plausible TSV, return if it parses try: return mem_from_tsv(text) except Exception: pass m = Memory() freqs = re.findall(r'\b(\d{1,3}\.\d{2,6})\b', text) if not freqs: raise ValueError('Unable to find a frequency') m.freq = parse_freq(freqs[0]) offset = re.search(r'([+-])\s*(\d\.\d{1,3}|\d)\b', text) duplex = re.search(r'\W([+-])\W', text[text.index(freqs[0]):]) if len(freqs) > 1 and not offset: split_to_offset(m, m.freq, parse_freq(freqs[1])) else: if offset: m.duplex = offset.group(1) m.offset = parse_freq(offset.group(2)) # Only look for the first +/- after the frequency, which would be # by far the most common arrangement if offset is None and duplex: m.duplex = duplex.group(1) tones = re.findall(r'\b(\d{2,3}\.\d|D\d{3})\b', text) if tones and len(tones) <= 2: txrx = [] for val in tones: if '.' in val: mode = 'Tone' tone = float(val) elif 'D' in val: mode = 'DTCS' tone = int(val[1:]) else: continue txrx.append((mode, tone, 'N')) if len(txrx) == 1: txrx.append(('', 88.5, 'N')) split_tone_decode(m, txrx[0], txrx[1]) return m def mem_to_text(mem): pieces = [format_freq(mem.freq)] if mem.duplex == 'split': pieces.append(format_freq(mem.offset)) elif mem.duplex in ('-', '+'): pieces.append('%s%i.%3.3s' % (mem.duplex, mem.offset / 1000000, '%03i' % (mem.offset % 1000000))) txrx = split_tone_encode(mem) for mode, tone, pol in txrx: if mode == 'Tone': pieces.append('%.1f' % tone) elif mode == 'DTCS': pieces.append('D%03i' % tone) return '[%s]' % '/'.join(pieces) def in_range(freq, ranges): """Check if freq is in any of the provided ranges""" for lo, hi in ranges: if lo <= freq <= hi: return True return False def is_split(bands, freq1, freq2): """Check if two freqs are in the same band from a list of bands Returns False if the two freqs are in the same band (not split) or True if they are in separate bands (split)""" # determine if the two freqs are in the same band for low, high in bands: if freq1 >= low and freq1 <= high and \ freq2 >= low and freq2 <= high: # if the two freqs are on the same Band this is not a split return False # if you get here is because the freq pairs are split return True chirp-20261030/chirp/cli/0000755000175000017500000000000015271057527013721 5ustar hibbyhibbychirp-20261030/chirp/cli/__init__.py0000644000175000017500000000000014411666130016007 0ustar hibbyhibbychirp-20261030/chirp/cli/experttune.py0000644000175000017500000002221015017704131016457 0ustar hibbyhibby#!/usr/bin/env python3 import argparse import contextlib import logging import random import serial import time import sys from chirp import bitwise from chirp.drivers import icomciv from chirp import memmap LOG = logging.getLogger() # Note these are only the frequencies from the chart that fit into the # US band edges. BANDS = { 160: [i for i in range(1805, 2005, 20)], 80: [i for i in range(3510, 4010, 10)], 40: [i for i in range(7013, 7313, 25)], 30: [10125], 20: [i for i in range(14025, 14325, 50)], 17: [18075, 18125, 18165], 15: [i for i in range(21025, 21475, 50)], 12: [24891, 24963], 10: [i for i in range(28050, 29750, 100)], 6: [i for i in range(50125, 54125, 250)], } class BitwiseFrame(icomciv.Frame): _fmt = '' _datalen = 0 _querycmd: int | None = None def __init__(self): super().__init__() self._data = memmap.MemoryMapBytes(b'\x00' * self._datalen) self.parse() def parse(self): self._obj = bitwise.parse(self._fmt, self._data) @classmethod def get(cls, radio): f = icomciv.Frame() f.set_command(cls._querycmd or cls._cmd, cls._sub) radio._send_frame(f) r = radio._recv_frame(frame=cls()) # If we used a different command to query, reset the received frame # to the one we'd use for the setting r._cmd = cls._cmd return r class SetFreqFrame(BitwiseFrame): _datalen = 5 _cmd = 0x05 _querycmd = 0x03 _sub = None _fmt = 'lbcd freq[5];' @property def freq(self): return self._obj.freq // 1000 @freq.setter def freq(self, khz): self._obj.freq = khz * 1000 class SetModeFrame(BitwiseFrame): _datalen = 2 _cmd = 0x06 _querycmd = 0x04 _sub = None _fmt = 'u8 mode; u8 filter;' _modes = [ ('LSB', 0), ('USB', 1), ('AM', 2), ('CW', 3), ('RTTY', 4), ('FM', 5), ('CW-R', 7), ('RTTY-R', 8), ('PSK', 12), ('PSK-R', 17), ] @property def mode(self): for mode, index in self._modes: if index == self._obj.mode: return mode raise RuntimeError('Unsupported mode %i' % self._obj.mode) @mode.setter def mode(self, mode): self._obj.filter = 1 for m, index in self._modes: if mode == m: self._obj.mode = index return raise RuntimeError('Unsupported mode %s' % mode) @property def filter(self): return int(self._obj.filter) class BKINFrame(BitwiseFrame): _datalen = 1 _cmd = 0x16 _sub = 0x47 _fmt = 'u8 mode;' _modes = ('off', 'semi', 'full') @property def mode(self): return self._modes[self._obj.mode] @mode.setter def mode(self, mode): self._obj.mode = self._modes.index(mode) class CWSpeedFrame(BitwiseFrame): _datalen = 2 _cmd = 0x14 _sub = 0x0C _fmt = 'bbcd speed[2];' @property def speed(self): return 48 * self._obj.speed // 255 @speed.setter def speed(self, wpm): self._obj.speed = int((wpm / 48) * 255) class PowerFrame(BitwiseFrame): _datalen = 2 _cmd = 0x15 _sub = 0x11 _fmt = 'bbcd power[2];' @classmethod def get(cls, radio): f = icomciv.Frame() f.set_command(cls._cmd, cls._sub) radio._send_frame(f) f = radio._recv_frame(frame=cls()) f.parse() return f @property def power(self): return int(100 * int(self._obj.power) / 212) @power.setter def power(self, power): self._obj.power = 212 * (power / 100) class Demo: def __init__(self, s=None): self._last_command = 0 def _send_frame(self, f): self._last_command = f._cmd def _recv_frame(self, frame=None): if frame is None: frame = PowerFrame() if self._last_command == 0x15: frame.power = random.randint(0, 10) * 10 else: frame.set_command(0xFB, None) return frame def setup(port, rcls): LOG.debug('Connecting to %s' % port) s = serial.Serial(port, timeout=1) LOG.debug('Starting radio') radio = rcls(s) LOG.debug('Detecting baudrate') radio._detect_baudrate() LOG.debug('Connected') return radio def set_freq(radio, khz, mode): f = SetFreqFrame() f.freq = khz radio._send_frame(f) r = radio._recv_frame() assert r._cmd == 0xFB LOG.debug('Set frequency to %i kHz', khz) f = SetModeFrame() f.mode = mode radio._send_frame(f) r = radio._recv_frame() assert r._cmd == 0xFB LOG.debug('Set mode to %s', mode) def interactive_next(current): print() print('Options:') print('r: Retry this frequency') print('x: Exit immediately') print('[enter]: Continue to the next frequency') i = input('> ').lower() if i == '': return True elif i == 'r': return False elif i == 'x': raise RuntimeError('Abort') def auto_next(current): print() return True def send_id(radio, call): f = SetModeFrame() f.mode = 'CW' radio._send_frame(f) r = radio._recv_frame() assert r._cmd == 0xFB LOG.debug('Set mode to CW') f = BKINFrame() f.mode = 'full' radio._send_frame(f) r = radio._recv_frame() assert r._cmd == 0xFB LOG.debug('Set BK-IN to full') f = CWSpeedFrame() f.speed = 22 radio._send_frame(f) r = radio._recv_frame() assert r._cmd == 0xFB LOG.debug('Set key speed to 22WPM') f = icomciv.Frame() f.set_command(0x17, 0x40) f.set_data(b'DE %s' % call.upper().encode()) radio._send_frame(f) r = radio._recv_frame() if r._cmd != 0xFB: print('Radio does not support CWID - ID manually now') else: print('Sending id %s' % call) time.sleep(len(call) + 2) LOG.debug('Sent ID') def wait_for_power(radio, fn): po = None maxl = 0 while po is None or not fn(po): po = PowerFrame.get(radio).power line = sys.stdout.write('Po: |%-10s| %3iW\r' % ('=' * (po // 10), po)) maxl = max(maxl, line) time.sleep(0.5) sys.stdout.write('%s\r' % (' ' * maxl)) def wait_for_tune(radio): print('Transmit to tune now (waiting for key)') wait_for_power(radio, lambda po: po > 0) print('Keyed, waiting for completion') wait_for_power(radio, lambda po: po == 0) print('Done') @contextlib.contextmanager def restore_settings(radio): if not isinstance(radio, Demo): settings = [ SetModeFrame.get(radio), SetFreqFrame.get(radio), BKINFrame.get(radio), CWSpeedFrame.get(radio), ] else: settings = [] try: yield finally: LOG.debug('Restoring previous settings') for f in settings: radio._send_frame(f) radio._recv_frame() def tune_loop(radio, bands, call, next_fn): for band in bands: for freq in BANDS[band]: done = False while not done: print('Changing to %i kHz' % freq) set_freq(radio, freq, 'RTTY') time.sleep(1) wait_for_tune(radio) time.sleep(2) if call: send_id(radio, call) try: done = next_fn(freq) except RuntimeError: return 0 def main(): # More of these may work, but these are known to work radios = { '7200': icomciv.Icom7200Radio, '7300': icomciv.Icom7300Radio, '7610': icomciv.Icom7610Radio, 'Demo': Demo, } p = argparse.ArgumentParser(description=( 'A simple tool to automate running through the required frequencies ' 'to tune an SPE Expert linear.')) p.add_argument('radio', choices=radios, help='Radio model') p.add_argument('port', help='Serial port for CAT control') p.add_argument('--bands', help=('Comma-separated list of bands ' 'to tune (160, 80, etc)')) p.add_argument('--call', help='Callsign for CW ID after each step') p.add_argument('--next', choices=['interactive', 'auto'], default='interactive', help='Next step strategy') p.add_argument('--debug', action='store_true', help='Enable verbose debugging') args = p.parse_args() logging.basicConfig(level=logging.DEBUG if args.debug else logging.INFO) next_fn = globals()['%s_next' % args.next] try: bands = [int(i) for i in args.bands.split(',')] for band in bands: assert band in BANDS except (ValueError, AssertionError): print('Invalid band input. Must be one or more of %s' % ( ','.join(str(i) for i in BANDS.keys()))) return 1 if args.radio == 'Demo': radio = Demo() else: radio = setup(args.port, radios[args.radio]) with restore_settings(radio): tune_loop(radio, bands, args.call, next_fn) if __name__ == '__main__': try: sys.exit(main()) except KeyboardInterrupt: pass chirp-20261030/chirp/cli/main.py0000644000175000017500000003403315270732464015221 0ustar hibbyhibby#!/usr/bin/env python3 # # Copyright 2008 Dan Smith # # 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 . import serial import os import sys import argparse import logging from chirp import logger from chirp import chirp_common, errors, directory, util sys.modules['builtins']._ = lambda x: x # type: ignore[attr-defined] directory.import_drivers() LOG = logging.getLogger("chirpc") RADIOS = directory.DRV_TO_RADIO def fail_unsupported(): LOG.error("Operation not supported by selected radio") sys.exit(1) def fail_missing_mmap(): LOG.error("mmap-only operation requires specification of an mmap file") sys.exit(1) class ToneAction(argparse.Action): def __call__(self, parser, namespace, values, option_string=None): value = values[0] if value not in chirp_common.TONES: raise argparse.ArgumentError( self, "Invalid tone value: %.1f" % value) setattr(namespace, self.dest, value) class DTCSAction(argparse.Action): def __call__(self, parser, namespace, values, option_string=None): value = values[0] if value not in chirp_common.DTCS_CODES: raise argparse.ArgumentError( self, "Invalid DTCS value: %03i" % value) setattr(namespace, self.dest, value) class DTCSPolarityAction(argparse.Action): def __call__(self, parser, namespace, values, option_string=None): value = values[0] if value not in ["NN", "RN", "NR", "RR"]: raise argparse.ArgumentError( self, "Invalid DTCS polarity: %s" % value) setattr(namespace, self.dest, value) def parse_memory_number(radio, args): if len(args) < 1: LOG.error("You must provide an argument specifying the memory number.") sys.exit(1) try: memnum = int(args[0]) except ValueError: memnum = args[0] rf = radio.get_features() start, end = rf.memory_bounds if not (start <= memnum <= end or memnum in rf.valid_special_chans): if len(rf.valid_special_chans) > 0: LOG.error( "memory number must be between %d and %d or one of %s" " (got %s)", start, end, ", ".join(rf.valid_special_chans), memnum) else: LOG.error("memory number must be between %d and %d (got %s)", start, end, memnum) sys.exit(1) return memnum def main(args=None): parser = argparse.ArgumentParser() logger.add_version_argument(parser) parser.add_argument("-s", "--serial", dest="serial", default="mmap", help="Serial port (default: mmap)") parser.add_argument("--list-settings", action="store_true", help="List settings") parser.add_argument("-i", "--id", dest="id", default=False, action="store_true", help="Request radio ID string") memarg = parser.add_argument_group("Memory/Channel Options") memarg.add_argument("--list-mem", action="store_true", help="List all memory locations") memarg.add_argument("--list-special-mem", action="store_true", help="List all special memory locations") memarg.add_argument("--raw", action="store_true", help="Dump raw memory location") memarg.add_argument("--get-mem", action="store_true", help="Get and print memory location") memarg.add_argument("--copy-mem", action="store_true", help="Copy memory location") memarg.add_argument("--clear-mem", action="store_true", help="Clear memory location") memarg.add_argument("--set-mem-name", help="Set memory name") memarg.add_argument("--set-mem-freq", type=float, help="Set memory frequency") memarg.add_argument("--set-mem-tencon", action="store_true", help="Set tone encode enabled flag") memarg.add_argument("--set-mem-tencoff", action="store_true", help="Set tone decode disabled flag") memarg.add_argument("--set-mem-tsqlon", action="store_true", help="Set tone squelch enabled flag") memarg.add_argument("--set-mem-tsqloff", action="store_true", help="Set tone squelch disabled flag") memarg.add_argument("--set-mem-dtcson", action="store_true", help="Set DTCS enabled flag") memarg.add_argument("--set-mem-dtcsoff", action="store_true", help="Set DTCS disabled flag") memarg.add_argument("--set-mem-tenc", type=float, action=ToneAction, nargs=1, help="Set memory encode tone") memarg.add_argument("--set-mem-tsql", type=float, action=ToneAction, nargs=1, help="Set memory squelch tone") memarg.add_argument("--set-mem-dtcs", type=int, action=DTCSAction, nargs=1, help="Set memory DTCS code") memarg.add_argument("--set-mem-dtcspol", action=DTCSPolarityAction, nargs=1, help="Set memory DTCS polarity (NN, NR, RN, RR)") memarg.add_argument("--set-mem-dup", help="Set memory duplex (+,-, or blank)") memarg.add_argument("--set-mem-offset", type=float, help="Set memory duplex offset (in MHz)") memarg.add_argument("--set-mem-mode", help="Set mode (%s)" % ",".join(chirp_common.MODES)) parser.add_argument("-r", "--radio", dest="radio", default=None, help="Radio model (see --list-radios)") parser.add_argument("--list-radios", action="store_true", help="List radio models") parser.add_argument("--mmap", dest="mmap", default=None, help="Radio memory map file location") parser.add_argument("--download-mmap", dest="download_mmap", action="store_true", default=False, help="Download memory map from radio") parser.add_argument("--upload-mmap", dest="upload_mmap", action="store_true", default=False, help="Upload memory map to radio") logger.add_arguments(parser) parser.add_argument("args", metavar="arg", nargs='*', help="Some commands require additional arguments") if len(sys.argv) <= 1: parser.print_help() sys.exit(0) options = parser.parse_args(args) args = options.args logger.handle_options(options) if options.list_radios: print("Supported Radios:\n\t", "\n\t".join(sorted(RADIOS.keys()))) sys.exit(0) if options.id: from chirp import detect md = detect.detect_icom_radio(options.serial) print("Model:\n%s" % md.MODEL) sys.exit(0) if not options.radio: if options.mmap: rclass = directory.get_radio_by_image(options.mmap).__class__ else: print("You must specify a radio model. See --list-radios.") sys.exit(1) else: rclass = directory.get_radio(options.radio) if options.serial == "mmap": if options.mmap: s = options.mmap else: s = options.radio + ".img" if not os.path.exists(s): LOG.error("Image file '%s' does not exist" % s) sys.exit(1) else: LOG.info("opening %s at %i" % (options.serial, rclass.BAUD_RATE)) if '://' in options.serial: s = serial.serial_for_url(options.serial, do_not_open=True) s.timeout = 0.5 s.open() else: s = serial.Serial(port=options.serial, timeout=0.5) radio = rclass(s) if options.list_settings: print(radio.get_settings()) sys.exit(0) if options.list_mem: rf = radio.get_features() start, end = rf.memory_bounds for i in range(start, end + 1): mem = radio.get_memory(i) if mem.empty and not logger.is_visible(logging.INFO): continue print(mem) sys.exit(0) if options.list_special_mem: rf = radio.get_features() for i in sorted(rf.valid_special_chans): mem = radio.get_memory(i) if mem.empty and not logger.is_visible(logging.INFO): continue print(mem) sys.exit(0) if options.copy_mem: src = parse_memory_number(radio, args) dst = parse_memory_number(radio, args[1:]) try: mem = radio.get_memory(src) except errors.InvalidMemoryLocation as e: LOG.exception(e) sys.exit(1) LOG.info("copying memory %s to %s", src, dst) mem.number = dst radio.set_memory(mem) if options.clear_mem: memnum = parse_memory_number(radio, args) try: mem = radio.get_memory(memnum) except errors.InvalidMemoryLocation as e: LOG.exception(e) sys.exit(1) if mem.empty: LOG.warn("memory %s is already empty, deleting again", memnum) mem.empty = True radio.set_memory(mem) if options.raw: memnum = parse_memory_number(radio, args) data = radio.get_raw_memory(memnum) for i in data: if ord(i) > 0x7F: print("Memory location %s (%i):\n%s" % (memnum, len(data), util.hexprint(data))) sys.exit(0) print(data) sys.exit(0) if options.set_mem_dup is not None: if options.set_mem_dup != "+" and \ options.set_mem_dup != "-" and \ options.set_mem_dup != "": LOG.error("Invalid duplex value `%s'" % options.set_mem_dup) LOG.error("Valid values are: '+', '-', ''") sys.exit(1) else: _dup = options.set_mem_dup else: _dup = None if options.set_mem_mode: LOG.info("Set mode: %s" % options.set_mem_mode) if options.set_mem_mode not in chirp_common.MODES: LOG.error("Invalid mode `%s'") sys.exit(1) else: _mode = options.set_mem_mode else: _mode = None if options.set_mem_name or options.set_mem_freq or \ options.set_mem_tencon or options.set_mem_tencoff or \ options.set_mem_tsqlon or options.set_mem_tsqloff or \ options.set_mem_dtcson or options.set_mem_dtcsoff or \ options.set_mem_tenc or options.set_mem_tsql or \ options.set_mem_dtcs or options.set_mem_dup is not None or \ options.set_mem_mode or options.set_mem_dtcspol or\ options.set_mem_offset: memnum = parse_memory_number(radio, args) try: mem = radio.get_memory(memnum) except errors.InvalidMemoryLocation as e: LOG.exception(e) sys.exit(1) if mem.empty: LOG.info("creating new memory (#%s)", memnum) mem = chirp_common.Memory() mem.number = memnum mem.name = options.set_mem_name or mem.name mem.freq = options.set_mem_freq or mem.freq mem.rtone = options.set_mem_tenc or mem.rtone mem.ctone = options.set_mem_tsql or mem.ctone mem.dtcs = options.set_mem_dtcs or mem.dtcs mem.dtcs_polarity = options.set_mem_dtcspol or mem.dtcs_polarity if _dup is not None: mem.duplex = _dup mem.offset = options.set_mem_offset or mem.offset mem.mode = _mode or mem.mode if options.set_mem_tencon: mem.tmode = "Tone" elif options.set_mem_tencoff: mem.tmode = "" if options.set_mem_tsqlon: mem.tmode = "TSQL" elif options.set_mem_tsqloff: mem.tmode = "" if options.set_mem_dtcson: mem.tmode = "DTCS" elif options.set_mem_dtcsoff: mem.tmode = "" radio.set_memory(mem) if options.get_mem: pos = parse_memory_number(radio, args) try: mem = radio.get_memory(pos) except errors.InvalidMemoryLocation: mem = chirp_common.Memory() mem.number = pos print(mem) sys.exit(0) if options.download_mmap: if not issubclass(rclass, chirp_common.CloneModeRadio): LOG.error("%s is not a clone mode radio" % options.radio) sys.exit(1) if not options.mmap: LOG.error("You must specify the destination file name with --mmap") sys.exit(1) try: radio.sync_in() radio.save_mmap(options.mmap) except Exception as e: LOG.exception(e) sys.exit(1) if options.upload_mmap: if not issubclass(rclass, chirp_common.CloneModeRadio): LOG.error("%s is not a clone mode radio" % options.radio) sys.exit(1) if not options.mmap: LOG.error("You must specify the source file name with --mmap") sys.exit(1) try: radio.load_mmap(options.mmap) radio.sync_out() print("Upload successful") except Exception as e: LOG.exception(e) sys.exit(1) if options.mmap and isinstance(radio, chirp_common.CloneModeRadio): radio.save_mmap(options.mmap) chirp-20261030/chirp/directory.py0000644000175000017500000002103515146031702015515 0ustar hibbyhibby# Copyright 2010 Dan Smith # Copyright 2012 Tom Hayward # # 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 . import glob import os import inspect import logging import sys from chirp import chirp_common, errors LOG = logging.getLogger(__name__) 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 def registered_class(cls): """Find the first parent of @cls that is registered""" bases = inspect.getmro(cls) for base in bases: try: return get_driver(base) except Exception: pass else: raise KeyError('No parent radio class of %s is registered' % cls) 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: LOG.info("driver re-registration enabled") ALLOW_DUPS = True def register(cls): """Register radio @cls with the directory""" ident = radio_class_id(cls) if ident in list(DRV_TO_RADIO.keys()): if ALLOW_DUPS: LOG.warn("Replacing existing driver id `%s'" % ident) else: raise Exception("Duplicate radio driver id `%s'" % ident) elif ALLOW_DUPS: LOG.info("Registered %s = %s" % (ident, cls.__name__)) DRV_TO_RADIO[ident] = cls RADIO_TO_DRV[cls] = ident if not hasattr(cls, '_DETECTED_BY'): cls._DETECTED_BY = None if not hasattr(cls, '_MINOR_VARIANT'): cls._MINOR_VARIANT = False return cls def detected_by(manager_class): """Mark a class as detected by another class This means it may not be offered directly as a choice and instead is added to @manager_class's DETECTED_MODELS list and should be returned from detect_from_serial() when appropriate. """ assert issubclass(manager_class, chirp_common.CloneModeRadio) def wrapper(cls): assert issubclass(cls, chirp_common.CloneModeRadio) cls._DETECTED_BY = manager_class manager_class.detect_model(cls) return cls return wrapper def minorvariant(cls): """Mark a class as too minor to distinguish from its parent. Use this on registered classes that are very minor and used for organization, but need not be exposed to the user as a separate offering. Minor version differences of the same model, for example. """ assert issubclass(cls, chirp_common.CloneModeRadio) cls._MINOR_VARIANT = True return cls DRV_TO_RADIO = {} RADIO_TO_DRV = {} AUX_FORMATS = set() def register_format(name, pattern, readonly=False): """Register a named format and file pattern. The name and pattern must not exist in the directory already (except together). The name should be something like "CSV" or "Icom ICF" and the pattern should be a glob like "*.icf". Returns a unique name to go in Radio.FORMATS so the UI knows what additional formats a driver can read (and write unless readonly is set). """ if (name, pattern) not in [(n, p) for n, p, r in AUX_FORMATS]: if name in [x[0] for x in AUX_FORMATS]: raise Exception('Duplicate format name %r' % name) AUX_FORMATS.add((name, pattern, readonly)) return name def get_radio(driver): """Get radio driver class by identification string""" if driver in DRV_TO_RADIO: 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 rclass in RADIO_TO_DRV: return RADIO_TO_DRV[rclass] elif rclass.__bases__[0] in RADIO_TO_DRV: return RADIO_TO_DRV[rclass.__bases__[0]] else: raise Exception("Unknown radio type `%s'" % rclass) # This is a mapping table of radio models that have changed in the past. # ideally we would never do this, but in the case where a radio was added # as the wrong model name, or a model has to be split, we need to be able # to open older files and do something intelligent with them. MODEL_COMPAT = { ('Retevis', 'RT-5R'): ('Retevis', 'RT5R'), ('Retevis', 'RT-5RV'): ('Retevis', 'RT5RV'), ('Signus', 'XTR-5'): ('Cignus', 'XTR-5'), } def get_radio_by_image(image_file): """Attempt to get the radio class that owns @image_file""" if os.path.exists(image_file): with open(image_file, "rb") as f: filedata = f.read() else: filedata = b"" data, metadata = chirp_common.CloneModeRadio._strip_metadata(filedata) for rclass in list(DRV_TO_RADIO.values()): if not issubclass(rclass, chirp_common.FileBackedRadio): continue if not metadata: # If no metadata, we do the old thing try: if rclass.match_model(filedata, image_file): return rclass(image_file) except Exception as e: LOG.error('Radio class %s failed during detection: %s' % ( rclass.__name__, e)) pass meta_vendor = metadata.get('vendor') meta_model = metadata.get('model') meta_variant = metadata.get('variant') meta_vendor, meta_model = MODEL_COMPAT.get((meta_vendor, meta_model), (meta_vendor, meta_model)) # If metadata, then it has to match one of the aliases or the parent for alias in rclass.ALIASES + [rclass]: if (alias.VENDOR == meta_vendor and alias.MODEL == meta_model and (meta_variant is None or alias.VARIANT == meta_variant)): class DynamicRadioAlias(rclass): _orig_rclass = rclass VENDOR = meta_vendor MODEL = meta_model VARIANT = metadata.get('variant') def __repr__(self): return repr(self._orig_rclass) return DynamicRadioAlias(image_file) if metadata: ex = errors.ImageMetadataInvalidModel("Unsupported model %s %s" % ( metadata.get("vendor"), metadata.get("model"))) ex.metadata = metadata raise ex else: # If we don't find anything else and the file appears to be a CSV # file, then explicitly open it with the generic driver so we can # get relevant errors instead of just "Unknown file format". if image_file.lower().endswith('.csv'): rclass = get_radio('Generic_CSV') return rclass(image_file) raise errors.ImageDetectFailed("Unknown file format") def import_drivers(limit=None): frozen = getattr(sys, 'frozen', False) if sys.platform == 'win32' and frozen: # We are in a frozen win32 build, so we can not glob # the driver files, but we do not need to anyway import chirp.drivers for module in chirp.drivers.__all__: try: __import__('chirp.drivers.%s' % module) except Exception as e: print('Failed to import %s: %s' % (module, e)) return # Safe import of everything in chirp/drivers. We need to import them # to get them to register, but should not abort if one import fails chirp_module_base = os.path.dirname(os.path.abspath(__file__)) driver_files = glob.glob(os.path.join(chirp_module_base, 'drivers', '*.py')) for driver_file in driver_files: module, ext = os.path.splitext(driver_file) driver_module = os.path.basename(module) if limit and driver_module not in limit: continue __import__('chirp.drivers.%s' % driver_module) chirp-20261030/chirp/drivers/0000755000175000017500000000000015271057527014630 5ustar hibbyhibbychirp-20261030/chirp/drivers/__init__.py0000644000175000017500000002204615271057526016744 0ustar hibbyhibbyfrom chirp.drivers import anytone from chirp.drivers import btech from chirp.drivers import anytone778uv from chirp.drivers import anytone_ht from chirp.drivers import anytone_iii from chirp.drivers import baofeng_common from chirp.drivers import baofeng_uv3r from chirp.drivers import baofeng_wp970i from chirp.drivers import bf_t1 from chirp.drivers import bf_t8 from chirp.drivers import boblov_x3plus from chirp.drivers import fake from chirp.drivers import fd268 from chirp.drivers import ft1500m from chirp.drivers import ft1802 from chirp.drivers import ft1d from chirp.drivers import ft2800 from chirp.drivers import ft2900 from chirp.drivers import ft4 from chirp.drivers import ft450d from chirp.drivers import ft50 from chirp.drivers import ft2d from chirp.drivers import ft60 from chirp.drivers import ft70 from chirp.drivers import ft7100 from chirp.drivers import tk760g from chirp.drivers import ft8100 from chirp.drivers import ft817 from chirp.drivers import ft818 from chirp.drivers import ft857 from chirp.drivers import ft90 from chirp.drivers import ft7800 from chirp.drivers import ftm3200d from chirp.drivers import ftm350 from chirp.drivers import ftm7250d from chirp.drivers import ga510 from chirp.drivers import generic_csv from chirp.drivers import generic_tpe from chirp.drivers import gmrsuv1 from chirp.drivers import gmrsv2 from chirp.drivers import h777 from chirp.drivers import hf90 from chirp.drivers import hg_uv98 from chirp.drivers import tk11 from chirp.drivers import bj9900 from chirp.drivers import ic2100 from chirp.drivers import hobbypcb from chirp.drivers import ic208 from chirp.drivers import ic2300 from chirp.drivers import ic2720 from chirp.drivers import ic2200 from chirp.drivers import ic2730 from chirp.drivers import ic2820 from chirp.drivers import ic9x_ll from chirp.drivers import ic9x from chirp.drivers import ic9x_icf from chirp.drivers import icf from chirp.drivers import ic9x_icf_ll from chirp.drivers import icomciv from chirp.drivers import icp7 from chirp.drivers import icq7 from chirp.drivers import ict8 from chirp.drivers import icv80 from chirp.drivers import ict70 from chirp.drivers import ict7h from chirp.drivers import icv86 from chirp.drivers import id51 from chirp.drivers import icx90 from chirp.drivers import icw32 from chirp.drivers import icx8x from chirp.drivers import id31 from chirp.drivers import id5100 from chirp.drivers import id51plus from chirp.drivers import id800 from chirp.drivers import id880 from chirp.drivers import idrp from chirp.drivers import kg935g from chirp.drivers import kguv8d from chirp.drivers import iradio_uv_5118 from chirp.drivers import kenwood_hmk from chirp.drivers import kenwood_itm from chirp.drivers import kenwood_live from chirp.drivers import kguv8e from chirp.drivers import kyd from chirp.drivers import kguv8dplus from chirp.drivers import kguv920pa from chirp.drivers import kguv980p from chirp.drivers import kguv9dplus from chirp.drivers import lt725uv from chirp.drivers import kyd_IP620 from chirp.drivers import leixen from chirp.drivers import mursv1 from chirp.drivers import puxing from chirp.drivers import ap510 from chirp.drivers import puxing_px888k from chirp.drivers import radioddity_r2 from chirp.drivers import radtel_t18 from chirp.drivers import retevis_rb15 from chirp.drivers import retevis_rb17p from chirp.drivers import retevis_rb28 from chirp.drivers import retevis_rt1 from chirp.drivers import retevis_rt21 from chirp.drivers import retevis_rt22 from chirp.drivers import retevis_rt23 from chirp.drivers import retevis_rt26 from chirp.drivers import retevis_rt76p from chirp.drivers import retevis_rt87 from chirp.drivers import retevis_rt98 from chirp.drivers import tdh8 from chirp.drivers import bjuv55 from chirp.drivers import rh5r_v2 from chirp.drivers import tdxone_tdq8a from chirp.drivers import template from chirp.drivers import tg_uv2p from chirp.drivers import th350 from chirp.drivers import th7800 from chirp.drivers import th9000 from chirp.drivers import th9800 from chirp.drivers import th_uv3r from chirp.drivers import th_uv3r25 from chirp.drivers import th_uv8000 from chirp.drivers import th_uv88 from chirp.drivers import th_uvf8d from chirp.drivers import thd72 from chirp.drivers import thd74 from chirp.drivers import thuv1f from chirp.drivers import tk270 from chirp.drivers import tk3140 from chirp.drivers import tk760 from chirp.drivers import alinco from chirp.drivers import tk8102 from chirp.drivers import tk8160 from chirp.drivers import tk8180 from chirp.drivers import tmd710 from chirp.drivers import tmv71 from chirp.drivers import tmv71_ll from chirp.drivers import ts2000 from chirp.drivers import ts480 from chirp.drivers import ts590 from chirp.drivers import ts850 from chirp.drivers import uv5r from chirp.drivers import uv5x3 from chirp.drivers import uv6r from chirp.drivers import uvb5 from chirp.drivers import vgc from chirp.drivers import vx170 from chirp.drivers import vx2 from chirp.drivers import vx3 from chirp.drivers import vx5 from chirp.drivers import vx510 from chirp.drivers import vx6 from chirp.drivers import vx7 from chirp.drivers import vx8 from chirp.drivers import __init__ from chirp.drivers import wouxun from chirp.drivers import wouxun_common from chirp.drivers import baofeng_digital from chirp.drivers import iradio_uv_5118plus from chirp.drivers import retevis_ha1g from chirp.drivers import retevis_rt18 from chirp.drivers import ksun_m6 from chirp.drivers import tk481 from chirp.drivers import ict10 from chirp.drivers import uvk5 from chirp.drivers import uvk5_cn from chirp.drivers import icf520 from chirp.drivers import retevis_ra86 from chirp.drivers import retevis_ra87 from chirp.drivers import ar8200 from chirp.drivers import retevis_rt97s from chirp.drivers import baofeng_uv17 from chirp.drivers import yaesu_clone from chirp.drivers import tdm11 from chirp.drivers import radtel_rt490 from chirp.drivers import retevis_ra25 from chirp.drivers import uvk5_egzumer from chirp.drivers import ft8000r from chirp.drivers import radtel_rt620 from chirp.drivers import retevis_c2 from chirp.drivers import ftm300d from chirp.drivers import ftm150 from chirp.drivers import ts790e from chirp.drivers import retevis_ha2 from chirp.drivers import tyt_uvl15w from chirp.drivers import icm710 from chirp.drivers import alinco_dr735t from chirp.drivers import mml_jc8810 from chirp.drivers import tk690 from chirp.drivers import vx150 from chirp.drivers import nx800 from chirp.drivers import vx1 from chirp.drivers import radioddity_db40g from chirp.drivers import radioddity_gm30 from chirp.drivers import baofeng_dm32uv from chirp.drivers import kenwood_d7 from chirp.drivers import retevis_h777v4 from chirp.drivers import radtel_rt880g from chirp.drivers import iradio_common from chirp.drivers import radtel_rt900 from chirp.drivers import tk280 from chirp.drivers import baofeng_uv17Pro __all__ = ['anytone', 'btech', 'anytone778uv', 'anytone_ht', 'anytone_iii', 'baofeng_common', 'baofeng_uv3r', 'baofeng_wp970i', 'bf_t1', 'bf_t8', 'boblov_x3plus', 'fake', 'fd268', 'ft1500m', 'ft1802', 'ft1d', 'ft2800', 'ft2900', 'ft4', 'ft450d', 'ft50', 'ft2d', 'ft60', 'ft70', 'ft7100', 'tk760g', 'ft8100', 'ft817', 'ft818', 'ft857', 'ft90', 'ft7800', 'ftm3200d', 'ftm350', 'ftm7250d', 'ga510', 'generic_csv', 'generic_tpe', 'gmrsuv1', 'gmrsv2', 'h777', 'hf90', 'hg_uv98', 'tk11', 'bj9900', 'ic2100', 'hobbypcb', 'ic208', 'ic2300', 'ic2720', 'ic2200', 'ic2730', 'ic2820', 'ic9x_ll', 'ic9x', 'ic9x_icf', 'icf', 'ic9x_icf_ll', 'icomciv', 'icp7', 'icq7', 'ict8', 'icv80', 'ict70', 'ict7h', 'icv86', 'id51', 'icx90', 'icw32', 'icx8x', 'id31', 'id5100', 'id51plus', 'id800', 'id880', 'idrp', 'kg935g', 'kguv8d', 'iradio_uv_5118', 'kenwood_hmk', 'kenwood_itm', 'kenwood_live', 'kguv8e', 'kyd', 'kguv8dplus', 'kguv920pa', 'kguv980p', 'kguv9dplus', 'lt725uv', 'kyd_IP620', 'leixen', 'mursv1', 'puxing', 'ap510', 'puxing_px888k', 'radioddity_r2', 'radtel_t18', 'retevis_rb15', 'retevis_rb17p', 'retevis_rb28', 'retevis_rt1', 'retevis_rt21', 'retevis_rt22', 'retevis_rt23', 'retevis_rt26', 'retevis_rt76p', 'retevis_rt87', 'retevis_rt98', 'tdh8', 'bjuv55', 'rh5r_v2', 'tdxone_tdq8a', 'template', 'tg_uv2p', 'th350', 'th7800', 'th9000', 'th9800', 'th_uv3r', 'th_uv3r25', 'th_uv8000', 'th_uv88', 'th_uvf8d', 'thd72', 'thd74', 'thuv1f', 'tk270', 'tk3140', 'tk760', 'alinco', 'tk8102', 'tk8160', 'tk8180', 'tmd710', 'tmv71', 'tmv71_ll', 'ts2000', 'ts480', 'ts590', 'ts850', 'uv5r', 'uv5x3', 'uv6r', 'uvb5', 'vgc', 'vx170', 'vx2', 'vx3', 'vx5', 'vx510', 'vx6', 'vx7', 'vx8', '__init__', 'wouxun', 'wouxun_common', 'baofeng_digital', 'iradio_uv_5118plus', 'retevis_ha1g', 'retevis_rt18', 'ksun_m6', 'tk481', 'ict10', 'uvk5', 'uvk5_cn', 'icf520', 'retevis_ra86', 'retevis_ra87', 'ar8200', 'retevis_rt97s', 'baofeng_uv17', 'yaesu_clone', 'tdm11', 'radtel_rt490', 'retevis_ra25', 'uvk5_egzumer', 'ft8000r', 'radtel_rt620', 'retevis_c2', 'ftm300d', 'ftm150', 'ts790e', 'retevis_ha2', 'tyt_uvl15w', 'icm710', 'alinco_dr735t', 'mml_jc8810', 'tk690', 'vx150', 'nx800', 'vx1', 'radioddity_db40g', 'radioddity_gm30', 'baofeng_dm32uv', 'kenwood_d7', 'retevis_h777v4', 'radtel_rt880g', 'iradio_common', 'radtel_rt900', 'tk280', 'baofeng_uv17Pro'] chirp-20261030/chirp/drivers/alinco.py0000644000175000017500000006364115266622247016462 0ustar hibbyhibby# Copyright 2011 Dan Smith # 2016 Matt Weyland # # 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 . from chirp import chirp_common, bitwise, memmap, errors, directory, util from chirp.settings import RadioSettingGroup, RadioSetting from chirp.settings import RadioSettingValueBoolean import time import logging import binascii LOG = logging.getLogger(__name__) 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] class AlincoStyleRadio(chirp_common.CloneModeRadio): """Base class for all known Alinco radios""" _memsize = 0 _model = b"NONE" def _send(self, data): LOG.debug("PC->R: (%2i)\n%s" % (len(data), util.hexprint(data))) self.pipe.write(data) self.pipe.read(len(data)) def _read(self, length): data = self.pipe.read(length) LOG.debug("R->PC: (%2i)\n%s" % (len(data), util.hexprint(data))) return data def _download_chunk(self, addr): if addr % 16: raise Exception("Addr 0x%04x not on 16-byte boundary" % addr) cmd = b"AL~F%04XR\r\n" % addr self._send(cmd) resp = self._read(RLENGTH).strip() if len(resp) == 0: raise errors.RadioNoResponse() if b":" not in resp: raise errors.RadioError("Unexpected response from radio") addr, _data = resp.split(b":", 1) data = binascii.unhexlify(_data) if len(data) != 16: LOG.debug("Response was:") LOG.debug("|%s|") LOG.debug("Which I converted to:") LOG.debug(util.hexprint(data)) raise Exception("Radio returned less than 16 bytes") return data def _download(self, limit): self._identify() data = b"" for addr in range(0, limit, 16): time.sleep(0.1) data += self._download_chunk(addr) 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(b"AL~E\r\n") self._read(20) return memmap.MemoryMapBytes(data) def _identify(self): for _i in range(0, 3): self._send(b"%s\r\n" % self._model) resp = self._read(6) if resp.strip() == b"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 = binascii.b2a_hex(_data).upper() cmd = b"AL~F%04XW%s\r\n" % (addr, data) self._send(cmd) def _upload(self, limit): if not self._identify(): raise errors.RadioError("I can't talk to this model") for addr in range(0x100, limit, 16): time.sleep(0.1) self._upload_chunk(addr) 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(b"AL~E\r\n") self._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 as 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 as 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.can_delete = False rf.valid_tuning_steps = STEPS rf.valid_name_length = 7 rf.valid_power_levels = self._power_levels rf.valid_dtcs_codes = DCS_CODES[self.VENDOR] 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 if self.MODEL == "JT220M" and mem.freq == 0: mem.empty = True return mem elif self.MODEL == "JT220M": mem.immutable = ['empty'] 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) @directory.register class DR03Radio(DRx35Radio): """Alinco DR03""" VENDOR = "Alinco" MODEL = "DR03T" _model = b"DR135" _memsize = 4096 _range = [(28000000, 29695000)] @classmethod def match_model(cls, filedata, filename): return len(filedata) == cls._memsize and \ filedata[0x64:0x66] == b'\x00\x28' @directory.register class DR06Radio(DRx35Radio): """Alinco DR06""" VENDOR = "Alinco" MODEL = "DR06T" _model = b"DR435" _memsize = 4096 _range = [(50000000, 53995000)] @classmethod def match_model(cls, filedata, filename): return len(filedata) == cls._memsize and \ filedata[0x64:0x66] == b'\x00\x50' @directory.register class DR135Radio(DRx35Radio): """Alinco DR135""" VENDOR = "Alinco" MODEL = "DR135T" _model = b"DR135" _memsize = 4096 _range = [(118000000, 174000000)] @classmethod def match_model(cls, filedata, filename): return len(filedata) == cls._memsize and \ filedata[0x64:0x66] == b'\x01\x44' @directory.register class DR235Radio(DRx35Radio): """Alinco DR235""" VENDOR = "Alinco" MODEL = "DR235T" _model = b"DR235" _memsize = 4096 _range = [(216000000, 280000000)] @classmethod def match_model(cls, filedata, filename): return len(filedata) == cls._memsize and \ filedata[0x64:0x66] == b'\x02\x22' @directory.register class DR435Radio(DRx35Radio): """Alinco DR435""" VENDOR = "Alinco" MODEL = "DR435T" _model = b"DR435" _memsize = 4096 _range = [(350000000, 511000000)] @classmethod def match_model(cls, filedata, filename): return len(filedata) == cls._memsize and \ filedata[0x64:0x66] == b'\x04\x00' @directory.register class DJ596Radio(DRx35Radio): """Alinco DJ596""" VENDOR = "Alinco" MODEL = "DJ596" _model = b"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:0x66] == b'\x45\x01' @directory.register class JT220MRadio(DRx35Radio): """Jetstream JT220""" VENDOR = "Jetstream" MODEL = "JT220M" _model = b"DR136" _memsize = 8192 _range = [(216000000, 280000000)] @classmethod def match_model(cls, filedata, filename): return len(filedata) == cls._memsize and \ filedata[0x60:0x64] == b'2009' @directory.register class DJ175Radio(DRx35Radio): """Alinco DJ175""" VENDOR = "Alinco" MODEL = "DJ175" _model = b"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 = b"AL~F%04XR\r\n" % addr self._send(cmd) _data = self._read(34).strip() if len(_data) == 0: raise errors.RadioNoResponse() data = binascii.unhexlify(_data) if len(data) != 16: LOG.debug("Response was:") LOG.debug("|%r|" % _data) LOG.debug("Which I converted to:") LOG.debug(util.hexprint(data)) raise Exception("Radio returned less than 16 bytes") return data DJG7_MEM_FORMAT = """ #seekto 0x200; ul16 bank[50]; ul16 special_bank[7]; #seekto 0x1200; struct { u8 empty; ul32 freq; u8 mode; u8 step; ul32 offset; u8 duplex; u8 squelch_type; u8 tx_tone; u8 rx_tone; u8 dcs; ul24 unknown1; u8 skip; ul32 unknown2; ul32 unknown3; ul32 unknown4; char name[32]; } memory[1000]; """ class AlincoDJG7(AlincoStyleRadio): """Alinco DJ-G7EG""" VENDOR = "Alinco" MODEL = "DJ-G7EG" BAUD_RATE = 57600 # Those are different from the other Alinco radios. STEPS = [5.0, 6.25, 8.33, 10.0, 12.5, 15.0, 20.0, 25.0, 30.0, 50.0, 100.0, 125.0, 150.0, 200.0, 500.0, 1000.0] DUPLEX = ["", "+", "-"] MODES = ["NFM", "FM", "AM", "WFM"] TMODES = ["", "??1", "Tone", "TSQL", "TSQL-R", "DTCS"] # This is a bit of a hack to avoid overwriting _identify() _memsize = 0x1a7c0 _range = [(500000, 1300000000)] @classmethod def get_prompts(cls): rp = chirp_common.RadioPrompts() rp.pre_download = _( "1. Ensure your firmware version is 4_10 or higher\n" "2. Turn radio off\n" "3. Connect your interface cable\n" "4. Turn radio on\n" "5. Press and release PTT 3 times while holding MONI key\n" "6. Supported baud rates: 57600 (default) and 19200\n" " (rotate dial while holding MONI to change)\n" "7. Click OK\n") rp.pre_upload = _( "1. Ensure your firmware version is 4_10 or higher\n" "2. Turn radio off\n" "3. Connect your interface cable\n" "4. Turn radio on\n" "5. Press and release PTT 3 times while holding MONI key\n" "6. Supported baud rates: 57600 (default) and 19200\n" " (rotate dial while holding MONI to change)\n" "7. Click OK\n") return rp def get_features(self): rf = chirp_common.RadioFeatures() rf.has_dtcs_polarity = False rf.has_bank = False rf.has_settings = False rf.valid_modes = self.MODES rf.valid_tmodes = ["", "Tone", "TSQL", "Cross", "TSQL-R", "DTCS"] rf.valid_tuning_steps = self.STEPS rf.valid_bands = self._range rf.valid_skips = ["", "S"] rf.valid_characters = chirp_common.CHARSET_ASCII rf.valid_name_length = 16 rf.memory_bounds = (0, 999) return rf def _download_chunk(self, addr): if addr % 0x40: raise Exception("Addr 0x%04x not on 64-byte boundary" % addr) cmd = b"AL~F%05XR\r" % addr self._send(cmd) # Response: "\r\n[ ... data ... ]\r\n # data is encoded in hex, hence we read two chars per byte _data = self._read(2+2*64+2).strip() if len(_data) == 0: raise errors.RadioNoResponse() data = binascii.unhexlify(_data) if len(data) != 64: LOG.debug("Response was:") LOG.debug("|%s|" % _data) LOG.debug("Which I converted to:") LOG.debug(util.hexprint(data)) raise Exception("Chunk from radio has wrong size") return data def _detect_baudrate_and_identify(self): if self._identify(): return True else: # Apparently Alinco support suggests to try again at a lower baud # rate if their cable fails with the default rate. See #4355. LOG.info("Could not talk to radio. Trying again at 19200 baud") self.pipe.baudrate = 19200 return self._identify() def _download(self, limit): self._detect_baudrate_and_identify() data = b"\x00"*0x200 for addr in range(0x200, limit, 0x40): data += self._download_chunk(addr) # Other Alinco drivers delay here, but doesn't seem to be necessary # for this model. if self.status_fn: status = chirp_common.Status() status.cur = addr status.max = limit status.msg = "Downloading from radio" self.status_fn(status) return memmap.MemoryMapBytes(data) def _upload_chunk(self, addr): if addr % 0x40: raise Exception("Addr 0x%04x not on 64-byte boundary" % addr) _data = self._mmap[addr:addr+0x40] data = binascii.b2a_hex(_data).upper() cmd = b"AL~F%05XW%s\r" % (addr, data) self._send(cmd) resp = self._read(6) if resp.strip() != b"OK": raise Exception("Unexpected response from radio: %s" % resp) def _upload(self, limit): if not self._detect_baudrate_and_identify(): raise Exception("I can't talk to this model") for addr in range(0x200, self._memsize, 0x40): self._upload_chunk(addr) # Other Alinco drivers delay here, but doesn't seem to be necessary # for this model. if self.status_fn: status = chirp_common.Status() status.cur = addr status.max = self._memsize status.msg = "Uploading to radio" self.status_fn(status) def _get_empty_flag(self, freq, mode): # Returns flag used to hide a channel from the main band. This occurs # when the mode is anything but NFM or FM (main band can only do those) # or when the frequency is outside of the range supported by the main # band. if mode not in ("NFM", "FM"): return 0x01 if (freq >= 136000000 and freq < 174000000) or \ (freq >= 400000000 and freq < 470000000) or \ (freq >= 1240000000 and freq < 1300000000): return 0x02 else: return 0x01 def _check_channel_consistency(self, number): _mem = self._memobj.memory[number] if _mem.empty != 0x00: if _mem.unknown1 == 0xffffff: # Previous versions of this code have skipped the unknown # fields. They contain bytes of value if the channel is empty # and thus those bytes remain 0xff when the channel is put to # use. The radio is totally fine with this but the Alinco # programming software is not (see #5275). Here, we check for # this and report if it is encountered. LOG.warning("Channel %d is inconsistent: Found 0xff in " "non-empty channel. Touch channel to fix." % number) if _mem.empty != self._get_empty_flag(_mem.freq, self.MODES[_mem.mode]): LOG.warning("Channel %d is inconsistent: Found out of band " "frequency. Touch channel to fix." % number) def process_mmap(self): self._memobj = bitwise.parse(DJG7_MEM_FORMAT, self._mmap) # We check all channels for corruption (see bug #5275) but we don't fix # it automatically because it would be unpolite to modify something on # a read operation. A log message is emitted though for the user to # take actions. for number in range(len(self._memobj.memory)): self._check_channel_consistency(number) def get_memory(self, number): _mem = self._memobj.memory[number] mem = chirp_common.Memory() mem.number = number if _mem.empty == 0: mem.empty = True else: mem.freq = int(_mem.freq) mem.mode = self.MODES[_mem.mode] mem.tuning_step = self.STEPS[_mem.step] mem.offset = int(_mem.offset) mem.duplex = self.DUPLEX[_mem.duplex] if self.TMODES[_mem.squelch_type] == "TSQL" and \ _mem.tx_tone != _mem.rx_tone: mem.tmode = "Cross" mem.cross_mode = "Tone->Tone" else: mem.tmode = self.TMODES[_mem.squelch_type] mem.rtone = ALINCO_TONES[_mem.tx_tone-1] mem.ctone = ALINCO_TONES[_mem.rx_tone-1] mem.dtcs = DCS_CODES[self.VENDOR][_mem.dcs] if _mem.skip: mem.skip = "S" # FIXME find out what every other byte is used for. Japanese? mem.name = str(_mem.name.get_raw(asbytes=False)[::2]).rstrip('\0') return mem def set_memory(self, mem): # Get a low-level memory object mapped to the image _mem = self._memobj.memory[mem.number] if mem.empty: _mem.set_raw("\xff" * (_mem.size() // 8)) _mem.empty = 0x00 else: _mem.empty = self._get_empty_flag(mem.freq, mem.mode) _mem.freq = mem.freq _mem.mode = self.MODES.index(mem.mode) _mem.step = self.STEPS.index(mem.tuning_step) _mem.offset = mem.offset _mem.duplex = self.DUPLEX.index(mem.duplex) if mem.tmode == "Cross": _mem.squelch_type = self.TMODES.index("TSQL") try: _mem.tx_tone = ALINCO_TONES.index(mem.rtone)+1 except ValueError: raise errors.UnsupportedToneError( "This radio does not support tone %.1fHz" % mem.rtone) try: _mem.rx_tone = ALINCO_TONES.index(mem.ctone)+1 except ValueError: raise errors.UnsupportedToneError( "This radio does not support tone %.1fHz" % mem.ctone) elif mem.tmode == "TSQL": _mem.squelch_type = self.TMODES.index("TSQL") # Note how the same TSQL tone is copied to both memory # locations try: _mem.tx_tone = ALINCO_TONES.index(mem.ctone)+1 _mem.rx_tone = ALINCO_TONES.index(mem.ctone)+1 except ValueError: raise errors.UnsupportedToneError( "This radio does not support tone %.1fHz" % mem.ctone) else: _mem.squelch_type = self.TMODES.index(mem.tmode) try: _mem.tx_tone = ALINCO_TONES.index(mem.rtone)+1 except ValueError: raise errors.UnsupportedToneError( "This radio does not support tone %.1fHz" % mem.rtone) try: _mem.rx_tone = ALINCO_TONES.index(mem.ctone)+1 except ValueError: raise errors.UnsupportedToneError( "This radio does not support tone %.1fHz" % mem.ctone) _mem.dcs = DCS_CODES[self.VENDOR].index(mem.dtcs) _mem.skip = (mem.skip == "S") _mem.name = "\x00".join(mem.name.rstrip()).ljust(32, "\x00") _mem.unknown1 = 0x3e001c _mem.unknown2 = 0x0000000a _mem.unknown3 = 0x00000000 _mem.unknown4 = 0x00000000 @directory.register class AlincoDJG7EG(AlincoDJG7): """Alinco DJ-G7EG""" MODEL = "DJ-G7EG" _model = b"AL~DJ-G7EG" @directory.register class AlincoDJG7T(AlincoDJG7): """Alinco DJ-G7T""" MODEL = "DJ-G7T" _model = b"AL~DJ-G7T" chirp-20261030/chirp/drivers/alinco_dr735t.py0000644000175000017500000003736415172631605017567 0ustar hibbyhibby# Copyright 2024 Jacob Calvert # # 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 . from chirp import chirp_common, bitwise, errors, memmap, directory, util from chirp.settings import RadioSettingGroup, RadioSetting from chirp.settings import RadioSettingValueBoolean, RadioSettingValueList from chirp.drivers.alinco import ALINCO_TONES, CHARSET import logging import codecs MEM_FORMAT = """ struct { u8 used; u8 skip; u8 favorite; u8 unknown3; ul32 frequency; ul32 shift; u8 shift_direction; u8 subtone_selection; u8 rx_tone_index; u8 tx_tone_index; u8 dcs_index; u8 unknown17; u8 power_index; u8 busy_channel_lockout; u8 mode; u8 heterodyne_mode; u8 unknown22; u8 bell; u8 name[6]; u8 dcs_off; u8 unknown31; u8 standby_screen_color; u8 rx_screen_color; u8 tx_screen_color; u8 unknown35_to_63[29]; } memory[1000]; """ LOG = logging.getLogger(__name__) @directory.register class AlincoDR735T(chirp_common.CloneModeRadio): """Base class for DR735T radio""" """Alinco DR735T""" VENDOR = "Alinco" MODEL = "DR735T" BAUD_RATE = 38400 TONE_MODE_MAP = { 0x00: "", 0x01: "Tone", 0x03: "TSQL", 0x0C: "DTCS" } SHIFT_DIR_MAP = ["", "-", "+"] POWER_MAP = [ chirp_common.PowerLevel("High", watts=50.0), chirp_common.PowerLevel("Mid", watts=25.00), chirp_common.PowerLevel("Low", watts=5.00), ] MODE_MAP = { 0x00: "FM", 0x01: "NFM", 0x02: "AM", 0x03: "NAM", 0x80: "Auto" } HET_MODE_MAP = ["Normal", "Reverse"] SCREEN_COLOR_MAP = [f"Color {n+1}" for n in range(16)] _freq_ranges = [ (108000000, 136000000), (136000000, 174000000), (400000000, 480000000) ] _no_channels = 1000 _model = b"DR735TN" def get_features(self): rf = chirp_common.RadioFeatures() # convert to list to deal with dict_values unsubscriptable rf.valid_tmodes = list(self.TONE_MODE_MAP.values()) rf.valid_modes = list(self.MODE_MAP.values()) rf.valid_skips = ["", "S"] rf.valid_bands = self._freq_ranges rf.valid_tuning_steps = [5.0, 6.25, 12.5] rf.memory_bounds = (0, self._no_channels-1) rf.has_ctone = True rf.has_bank = False rf.has_dtcs_polarity = False rf.has_tuning_step = False rf.can_delete = False rf.valid_name_length = 6 rf.valid_characters = chirp_common.CHARSET_UPPER_NUMERIC rf.valid_power_levels = self.POWER_MAP rf.valid_dtcs_codes = chirp_common.DTCS_CODES return rf def _identify(self) -> bool: command = b"AL~WHO\r\n" self.pipe.write(command) self.pipe.read(len(command)) # expect DR735TN\r\n radio_id = self.pipe.read(9).strip() if not radio_id: raise errors.RadioNoResponse() LOG.debug('Model string is %s' % util.hexprint(radio_id)) return radio_id in (b"DR735TN", b"DR735TE") def do_download(self): if not self._identify(): raise errors.RadioError("Unsupported radio model.") channel_data = b"" for channel_no in range(0, self._no_channels): command = f"AL~EEPEL{channel_no << 6:04X}R\r\n".encode() self.pipe.write(command) self.pipe.read(len(command)) channel_spec = self.pipe.read(128) # 64 bytes, as hex self.pipe.read(2) # \r\n channel_spec = codecs.decode(channel_spec, "hex") if len(channel_spec) != 64: exit(1) channel_data += channel_spec if self.status_fn: status = chirp_common.Status() status.cur = channel_no status.max = self._no_channels status.msg = f"Downloading channel {channel_no} from radio" self.status_fn(status) return memmap.MemoryMapBytes(channel_data) def do_upload(self): if not self._identify(): raise errors.RadioError("Unsupported radio model.") command = b"AL~DR735J\r\n" self.pipe.write(command) self.pipe.read(len(command)) resp = self.pipe.read(4) if resp != b"OK\r\n": errors.RadioError("Could not go into download mode.") for channel_no in range(0, self._no_channels): write_data = self.get_mmap()[channel_no*64:(channel_no+1)*64] write_data = codecs.encode(write_data, 'hex').upper() command = f"AL~EEPEL{channel_no << 6:04X}W".encode( ) + write_data + b"\r\n" LOG.debug(f"COMM: {command}") self.pipe.write(command) back = self.pipe.read(len(command)) LOG.debug(f"BACK: {back}") resp = self.pipe.read(4) LOG.debug(f"RESP: {resp}") if resp != b"OK\r\n": raise errors.RadioError("failed to write to channel") if self.status_fn: status = chirp_common.Status() status.cur = channel_no status.max = self._no_channels status.msg = f"Uploading channel {channel_no} to radio" self.status_fn(status) command = b"AL~RESET\r\n" self.pipe.write(command) self.pipe.read(len(command)) # command + OK\r\n self.pipe.read(4) def sync_in(self): try: self._mmap = self.do_download() except errors.RadioError: raise except Exception as exc: raise errors.RadioError(f"Failed to download from radio: {exc}") self.process_mmap() def sync_out(self): try: self.do_upload() except errors.RadioError: raise except Exception as exc: raise errors.RadioError(f"Failed to download from radio: {exc}") 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] mem = chirp_common.Memory() mem.number = number # Set the memory number if _mem.used != 0x55: mem.empty = True mem.freq = 400000000 mem.name = "" mem.tmode = self.TONE_MODE_MAP[0] mem.duplex = self.SHIFT_DIR_MAP[0] mem.offset = 0 mem.rtone = ALINCO_TONES[0] mem.ctone = ALINCO_TONES[0] mem.dtcs = chirp_common.DTCS_CODES[0] mem.power = self.POWER_MAP[0] mem.skip = '' mem.mode = self.MODE_MAP[0] self._get_extra_default(mem) return mem else: mem.empty = False mem.freq = int(_mem.frequency) mem.name = "".join([CHARSET[_mem.name[i]] for i in range(6)]).strip() mem.name = mem.name.replace('\x00', '') mem.tmode = self.TONE_MODE_MAP[int(_mem.subtone_selection)] mem.duplex = self.SHIFT_DIR_MAP[_mem.shift_direction] mem.offset = _mem.shift mem.rtone = ALINCO_TONES[_mem.rx_tone_index] mem.ctone = ALINCO_TONES[_mem.tx_tone_index] mem.dtcs = chirp_common.DTCS_CODES[_mem.dcs_index] mem.power = self.POWER_MAP[_mem.power_index] mem.skip = 'S' if bool(_mem.skip) else '' mem.mode = self.MODE_MAP[int(_mem.mode)] self._get_extra(_mem, mem) return mem def set_memory(self, mem): # Get a low-level memory object mapped to the image _mem = self._memobj.memory[mem.number] def find_key_in(d: dict, target_val): for k, v in d.items(): if v == target_val: return k if not mem.empty: mapped_name = [CHARSET.index(' ').to_bytes(1, 'little')]*6 for (i, c) in enumerate(mem.name.ljust(6)[:6].upper().strip()): if c not in chirp_common.CHARSET_UPPER_NUMERIC: c = " " # just make it a space mapped_name[i] = CHARSET.index(c).to_bytes(1, 'little') _mem.frequency = int(mem.freq) _mem.name = b''.join(mapped_name) _mem.mode = find_key_in(self.MODE_MAP, mem.mode) _mem.subtone_selection = find_key_in(self.TONE_MODE_MAP, mem.tmode) _mem.shift = mem.offset _mem.used = 0x00 if mem.empty else 0x55 _mem.power_index = self.POWER_MAP.index( mem.power) if mem.power in self.POWER_MAP else 0 _mem.skip = 0x01 if mem.skip == "S" else 0x00 try: _mem.rx_tone_index = ALINCO_TONES.index(mem.rtone) except ValueError: raise errors.UnsupportedToneError("This radio does " "not support " "tone %.1fHz" % mem.rtone) try: _mem.tx_tone_index = ALINCO_TONES.index(mem.ctone) except ValueError: raise errors.UnsupportedToneError("This radio does " "not support " "tone %.1fHz" % mem.ctone) _mem.dcs_index = chirp_common.DTCS_CODES.index( mem.dtcs if mem.dtcs else chirp_common.DTCS_CODES[0]) _mem.shift_direction = self.SHIFT_DIR_MAP.index( mem.duplex if mem.duplex else self.SHIFT_DIR_MAP[0]) else: _mem.frequency = 0 _mem.name = b"\x00"*6 _mem.mode = find_key_in(self.MODE_MAP, "Auto") _mem.subtone_selection = find_key_in(self.TONE_MODE_MAP, "") _mem.shift = 0 _mem.used = 0x00 if mem.empty else 0x55 _mem.power_index = 0 _mem.skip = 0x01 if mem.skip == "S" else 0x00 _mem.rx_tone_index = 0 _mem.tx_tone_index = 0 _mem.dcs_index = 0 _mem.shift_direction = self.SHIFT_DIR_MAP.index("") self._set_extra(_mem, mem) def _get_extra_default(self, mem): mem.extra = RadioSettingGroup("extra", "Extra") het_mode = RadioSetting("heterodyne_mode", "Heterodyne Mode", RadioSettingValueList( self.HET_MODE_MAP, current=self.HET_MODE_MAP[0] )) het_mode.set_doc("Heterodyne Mode") bcl = RadioSetting("bcl", "BCL", RadioSettingValueBoolean(False)) bcl.set_doc("Busy Channel Lockout") fav = RadioSetting("fav", "Favorite", RadioSettingValueBoolean(False)) fav.set_doc("Favorite Channel") bell = RadioSetting("bell", "Bell", RadioSettingValueBoolean(False)) bell.set_doc("Bell Alert") stby_screen = RadioSetting("stby_screen", "Standby Screen Color", RadioSettingValueList( self.SCREEN_COLOR_MAP, current=self.SCREEN_COLOR_MAP[0])) stby_screen.set_doc("Standby Screen Color") rx_screen = RadioSetting("rx_screen", "RX Screen Color", RadioSettingValueList( self.SCREEN_COLOR_MAP, current=self.SCREEN_COLOR_MAP[0])) rx_screen.set_doc("RX Screen Color") tx_screen = RadioSetting("tx_screen", "TX Screen Color", RadioSettingValueList( self.SCREEN_COLOR_MAP, current=self.SCREEN_COLOR_MAP[0])) tx_screen.set_doc("TX Screen Color") mem.extra.append(het_mode) mem.extra.append(bcl) mem.extra.append(fav) mem.extra.append(bell) mem.extra.append(stby_screen) mem.extra.append(rx_screen) mem.extra.append(tx_screen) def _get_extra(self, _mem, mem): mem.extra = RadioSettingGroup("extra", "Extra") het_mode = RadioSetting( "heterodyne_mode", "Heterodyne Mode", RadioSettingValueList( self.HET_MODE_MAP, current_index=int(_mem.heterodyne_mode))) het_mode.set_doc("Heterodyne Mode") bcl = RadioSetting("bcl", "BCL", RadioSettingValueBoolean( bool(_mem.busy_channel_lockout))) bcl.set_doc("Busy Channel Lockout") fav = RadioSetting("fav", "Favorite", RadioSettingValueBoolean(bool(_mem.favorite))) fav.set_doc("Favorite Channel") bell = RadioSetting("bell", "Bell", RadioSettingValueBoolean(bool(_mem.bell))) bell.set_doc("Bell Alert") stby_screen = RadioSetting( "stby_screen", "Standby Screen Color", RadioSettingValueList( self.SCREEN_COLOR_MAP, current_index=int(_mem.standby_screen_color))) stby_screen.set_doc("Standby Screen Color") rx_screen = RadioSetting( "rx_screen", "RX Screen Color", RadioSettingValueList( self.SCREEN_COLOR_MAP, current_index=int(_mem.rx_screen_color))) rx_screen.set_doc("RX Screen Color") tx_screen = RadioSetting( "tx_screen", "TX Screen Color", RadioSettingValueList( self.SCREEN_COLOR_MAP, current_index=int(_mem.tx_screen_color))) tx_screen.set_doc("TX Screen Color") mem.extra.append(het_mode) mem.extra.append(bcl) mem.extra.append(stby_screen) mem.extra.append(fav) mem.extra.append(bell) mem.extra.append(rx_screen) mem.extra.append(tx_screen) def _set_extra(self, _mem, mem): for setting in mem.extra: if setting.get_name() == "heterodyne_mode": _mem.heterodyne_mode = \ self.HET_MODE_MAP.index( setting.value) if \ setting.value else self.HET_MODE_MAP[0] if setting.get_name() == "bcl": _mem.busy_channel_lockout = int(setting.value) if setting.get_name() == "fav": _mem.favorite = int(setting.value) if setting.get_name() == "bell": _mem.bell = int(setting.value) if setting.get_name() == "stby_screen": _mem.standby_screen_color = \ self.SCREEN_COLOR_MAP.index(setting.value) if \ setting.value else self.SCREEN_COLOR_MAP[0] if setting.get_name() == "rx_screen": _mem.rx_screen_color = \ self.SCREEN_COLOR_MAP.index(setting.value) if \ setting.value else self.SCREEN_COLOR_MAP[0] if setting.get_name() == "tx_screen": _mem.tx_screen_color = \ self.SCREEN_COLOR_MAP.index(setting.value) if \ setting.value else self.SCREEN_COLOR_MAP[0] chirp-20261030/chirp/drivers/anytone.py0000644000175000017500000006055415161444633016665 0ustar hibbyhibby# Copyright 2013 Dan Smith # # 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 . import struct import logging from chirp import bitwise from chirp import chirp_common from chirp import checksum from chirp import directory from chirp import errors from chirp import memmap from chirp import util from chirp.settings import RadioSettingGroup, RadioSetting, RadioSettings, \ RadioSettingValueList, RadioSettingValueString, RadioSettingValueBoolean LOG = logging.getLogger(__name__) BLOCK_SIZE = 0x10 NUMBER_OF_MEMORY_LOCATIONS = 758 MEMORY_LOCATION_SIZE = 32 ADDR_FIRST_WRITABLE_BYTE = 0x0100 ADDR_LAST_BLOCK_BEFORE_MEMORIES = 0x19F0 ADDR_FIRST_MEMORY_LOCATION = 0x2000 # This _mem_format structure is defined separately from the rest because it # needs to be parsed twice -- once in the middle of downloading data from the # radio, and then again (along with everything else) once the download is # complete. _mem_format = """ #seekto 0x0100; struct { u8 even_unknown:2, even_pskip:1, even_skip:1, odd_unknown:2, odd_pskip:1, odd_skip:1; } flags[379]; """ mem_format = _mem_format + """ struct memory { bbcd freq[4]; bbcd offset[4]; u8 unknownA:4, tune_step:4; u8 rxdcsextra:1, txdcsextra:1, rxinv:1, txinv:1, channel_width:2, rev:1, txoff:1; u8 talkaround:1, compander:1, unknown8:1, is_am:1, power:2, duplex:2; u8 dtmfSlotNum:4, rxtmode:2, txtmode:2; u8 unknown5:2, txtone:6; u8 unknown6:2, rxtone:6; u8 txcode; u8 rxcode; u8 unknown10:2, pttid:2, unknown11:2, bclo:2; u8 unknown7; u8 unknown9:5, sqlMode:3; // [Carrier, CTCSS/DCS Tones, // Opt Sig Only, Tones & Opt Sig, // Tones or Opt Sig] u8 unknown21:6, optsig:2; u8 unknown22:3, twotone:5; u8 unknown23:1, fivetone:7; u8 unknown24:4, scramble:4; char name[7]; ul16 custtone; }; #seekto 0x0030; struct { char serial[16]; } serial_no; #seekto 0x0050; struct { char date[16]; } version; #seekto 0x0280; struct { u8 unknown1:6, display:2; u8 unknown2[11]; // TODO Backlight brightness is in here (RGB) u8 unknown3:3, apo:5; u8 unknown4a[2]; u8 unknown4b:6, mute:2; // TODO Keypad locked is in here (0=unlocked, 1=locked) u8 unknown4; u8 unknown5:5, beep:1, unknown6:2; u8 unknown[334]; // TODO Programmable keys A-D are here char welcome[8]; } settings; #seekto 0x0400; struct { u8 code[12]; u8 len; u8 unknown2[3]; } dtmf_codes[16]; #seekto 0x0540; struct memory memblk1[12]; #seekto 0x2000; struct memory memory[758]; // FIXME It's actually only 750 // TODO VFO scan limits are here (after the 750th memory; seekto 0x7DC0) #seekto 0x7ec0; struct memory memblk2[10]; """ class FlagObj(object): def __init__(self, flagobj, which): self._flagobj = flagobj self._which = which def _get(self, flag): return getattr(self._flagobj, "%s_%s" % (self._which, flag)) def _set(self, flag, value): return setattr(self._flagobj, "%s_%s" % (self._which, flag), value) def get_skip(self): return self._get("skip") def set_skip(self, value): self._set("skip", value) skip = property(get_skip, set_skip) def get_pskip(self): return self._get("pskip") def set_pskip(self, value): self._set("pskip", value) pskip = property(get_pskip, set_pskip) def set(self): self._set("unknown", 3) self._set("skip", 1) self._set("pskip", 1) def clear(self): self._set("unknown", 0) self._set("skip", 0) self._set("pskip", 0) def get(self): return (self._get("unknown") << 2 | self._get("skip") << 1 | self._get("pskip")) def __repr__(self): return repr(self._flagobj) def _is_loc_used(memobj, loc): return memobj.flags[loc / 2].get_raw(asbytes=False) != "\xFF" def _addr_to_loc(addr): return (addr - ADDR_FIRST_MEMORY_LOCATION) / MEMORY_LOCATION_SIZE def _should_send_addr(memobj, addr): if addr < ADDR_FIRST_MEMORY_LOCATION or addr >= 0x7EC0: return True else: return _is_loc_used(memobj, _addr_to_loc(addr)) def _echo_write(radio, data): try: radio.pipe.write(data) radio.pipe.read(len(data)) except Exception as e: LOG.error("Error writing to radio: %s" % e) raise errors.RadioError("Unable to write to radio") def _read(radio, length): try: data = radio.pipe.read(length) except Exception as e: LOG.error("Error reading from radio: %s" % e) raise errors.RadioError("Unable to read from radio") if len(data) != length: LOG.error("Short read from radio (%i, expected %i)" % (len(data), length)) LOG.debug(util.hexprint(data)) raise errors.RadioError("Short read from radio") return data valid_model = [b'QX588UV', b'HR-2040', b'DB-50M\x00', b'DB-750X'] def _ident(radio): # Chew garbage try: radio.pipe.read(32) except Exception: raise errors.RadioError("Unable to flush serial connection") radio.pipe.timeout = 1 _echo_write(radio, b"PROGRAM") response = radio.pipe.read(3) if response != b"QX\x06": LOG.debug("Response was:\n%s" % util.hexprint(response)) raise errors.RadioError("Unsupported model or bad connection") _echo_write(radio, b"\x02") response = radio.pipe.read(16) LOG.debug(util.hexprint(response)) if response[1:8] not in valid_model: LOG.debug("Response was:\n%s" % util.hexprint(response)) raise errors.RadioError("Unsupported model") def _finish(radio): endframe = b"\x45\x4E\x44" _echo_write(radio, endframe) result = radio.pipe.read(1) if result != b"\x06": LOG.debug("Got:\n%s" % util.hexprint(result)) raise errors.RadioError("Radio did not finish cleanly") def _send(radio, cmd, addr, length, data=None): frame = struct.pack(">cHb", cmd, addr, length) if data: frame += data frame += struct.pack('BB', checksum.checksum_8bit(frame[1:]), 0x06) _echo_write(radio, frame) LOG.debug("Sent:\n%s" % util.hexprint(frame)) if data: result = radio.pipe.read(1) if result != b'\x06': LOG.debug("Ack was: %s" % repr(result)) raise errors.RadioError( "Radio did not accept block at %04x" % addr) return result = _read(radio, length + 6) LOG.debug("Got:\n%s" % util.hexprint(result)) header = result[0:4] data = result[4:-2] ack = result[-1] if ack != 0x06: LOG.debug("Ack was: %s" % repr(ack)) raise errors.RadioError("Radio NAK'd block at %04x" % addr) _cmd, _addr, _length = struct.unpack(">cHb", header) if _addr != addr or _length != _length: LOG.debug("Expected/Received:") LOG.debug(" Length: %02x/%02x" % (length, _length)) LOG.debug(" Addr: %04x/%04x" % (addr, _addr)) raise errors.RadioError("Radio send unexpected block") cs = checksum.checksum_8bit(result[1:-2]) if cs != result[-2]: LOG.debug("Calculated: %02x" % cs) LOG.debug("Actual: %02x" % result[-2]) raise errors.RadioError("Block at 0x%04x failed checksum" % addr) return data def _download(radio): _ident(radio) memobj = None data = b"" for start, end in radio._ranges: for addr in range(start, end, BLOCK_SIZE): if memobj is not None and not _should_send_addr(memobj, addr): block = b"\xFF" * BLOCK_SIZE else: block = _send(radio, b'R', addr, BLOCK_SIZE) data += block status = chirp_common.Status() status.cur = len(data) status.max = end status.msg = "Cloning from radio" radio.status_fn(status) if addr == ADDR_LAST_BLOCK_BEFORE_MEMORIES: memobj = bitwise.parse(_mem_format, data) _finish(radio) return memmap.MemoryMapBytes(data) def _upload(radio): _ident(radio) for start, end in radio._ranges: for addr in range(start, end, BLOCK_SIZE): if addr < ADDR_FIRST_WRITABLE_BYTE: continue if not _should_send_addr(radio._memobj, addr): continue block = radio._mmap[addr:addr + BLOCK_SIZE] _send(radio, b'W', addr, len(block), block) status = chirp_common.Status() status.cur = addr status.max = end status.msg = "Cloning to radio" radio.status_fn(status) _finish(radio) TONES = tuple(sorted([62.5] + list(chirp_common.TONES))) TMODES = ['', 'Tone', 'DTCS', ''] DUPLEXES = ['', '-', '+', 'off'] MODES = ["FM", "FM", "NFM"] POWER_LEVELS = [chirp_common.PowerLevel("High", watts=50), chirp_common.PowerLevel("Mid1", watts=25), chirp_common.PowerLevel("Mid2", watts=10), chirp_common.PowerLevel("Low", watts=5)] BCLO = ['Off', 'Repeater', 'Busy'] DTMF_SLOTS = ['M%d' % x for x in range(1, 17)] # Chose not to expose SCRAMBLE_CODES = ['Off'] + # ['%d' % x for x in range(1, 10)] + ['Define 1', 'Define 2'] TONE2_SLOTS = ['%d' % x for x in range(0, 24)] TONE5_SLOTS = ['%d' % x for x in range(0, 100)] SQL_MODES = ["Carrier", "CTCSS/DCS", "Opt Sig Only", "Tones AND Sig", "Tones OR Sig"] OPT_SIG_SQL = ["Off"] + SQL_MODES[2:] OPT_SIGS = ['Off', 'DTMF', '2Tone', '5Tone'] PTT_IDS = ['Off', 'Begin', 'End', 'Begin & End'] TUNING_STEPS = [2.5, 5, 6.25, 10, 12.5, 15, 20, 25, 30, 50] # FIXME (1) Not sure if 15 is a valid step, (2) 100 might be a valid step @directory.register class AnyTone5888UVRadio(chirp_common.CloneModeRadio, chirp_common.ExperimentalRadio): """AnyTone 5888UV""" VENDOR = "AnyTone" MODEL = "5888UV" BAUD_RATE = 9600 _file_ident = [b"QX588UV", b"588UVN"] # May try to mirror the OEM behavior later _ranges = [ (0x0000, 0x8000), ] @classmethod def get_prompts(cls): rp = chirp_common.RadioPrompts() rp.experimental = ("The Anytone driver is currently experimental. " "There are no known issues with it, but you should " "proceed with caution.") return rp def get_features(self): rf = chirp_common.RadioFeatures() rf.has_settings = True rf.has_bank = False rf.has_cross = True rf.valid_duplexes = DUPLEXES rf.valid_tuning_steps = TUNING_STEPS rf.has_rx_dtcs = True rf.valid_skips = ["", "S", "P"] rf.valid_modes = ["FM", "NFM", "AM"] rf.valid_tmodes = ['', 'Tone', 'TSQL', 'DTCS', 'Cross'] rf.valid_cross_modes = ['Tone->DTCS', 'DTCS->Tone', 'DTCS->DTCS', '->Tone', '->DTCS', 'Tone->Tone'] rf.valid_tones = TONES rf.valid_dtcs_codes = chirp_common.ALL_DTCS_CODES rf.valid_bands = [(108000000, 500000000)] rf.valid_characters = chirp_common.CHARSET_ALPHANUMERIC + "-*#" rf.valid_name_length = 7 rf.valid_power_levels = POWER_LEVELS rf.memory_bounds = (1, NUMBER_OF_MEMORY_LOCATIONS) return rf def sync_in(self): self._mmap = _download(self) self.process_mmap() def sync_out(self): _upload(self) def process_mmap(self): self._memobj = bitwise.parse(mem_format, self._mmap) def _get_memobjs(self, number): number -= 1 _mem = self._memobj.memory[number] _flg = FlagObj(self._memobj.flags[number / 2], number % 2 and "even" or "odd") return _mem, _flg def _get_dcs_index(self, _mem, which): base = getattr(_mem, '%scode' % which) extra = getattr(_mem, '%sdcsextra' % which) return (int(extra) << 8) | int(base) def _set_dcs_index(self, _mem, which, index): base = getattr(_mem, '%scode' % which) extra = getattr(_mem, '%sdcsextra' % which) base.set_value(index & 0xFF) extra.set_value(index >> 8) def get_raw_memory(self, number): _mem, _flg = self._get_memobjs(number) return repr(_mem) + repr(_flg) def get_memory(self, number): _mem, _flg = self._get_memobjs(number) mem = chirp_common.Memory() mem.number = number if _flg.get() == 0x0F: mem.empty = True return mem mem.freq = int(_mem.freq) * 100 # compensate for 6.25 and 12.5 kHz tuning steps, add 500 Hz if needed lastdigit = int(_mem.freq) % 10 if (lastdigit == 2 or lastdigit == 7): mem.freq += 50 mem.offset = int(_mem.offset) * 100 mem.name = str(_mem.name).rstrip().replace('\x00', '') mem.duplex = DUPLEXES[_mem.duplex] mem.mode = _mem.is_am and "AM" or MODES[_mem.channel_width] mem.tuning_step = TUNING_STEPS[_mem.tune_step] if _mem.txoff: mem.duplex = DUPLEXES[3] rxtone = txtone = None rxmode = TMODES[_mem.rxtmode] if (_mem.sqlMode == SQL_MODES.index("Carrier") or _mem.sqlMode == SQL_MODES.index("Opt Sig Only")): rxmode = TMODES.index('') txmode = TMODES[_mem.txtmode] if txmode == "Tone": # If custom tone is being used, show as 88.5 (and set # checkbox in extras) Future: Improve chirp_common, so I # can add "CUSTOM" into TONES if _mem.txtone == len(TONES): txtone = 88.5 else: txtone = TONES[_mem.txtone] elif txmode == "DTCS": txtone = chirp_common.ALL_DTCS_CODES[self._get_dcs_index(_mem, 'tx')] if rxmode == "Tone": # If custom tone is being used, show as 88.5 (and set # checkbox in extras) Future: Improve chirp_common, so I # can add "CUSTOM" into TONES if _mem.rxtone == len(TONES): rxtone = 88.5 else: rxtone = TONES[_mem.rxtone] elif rxmode == "DTCS": rxtone = chirp_common.ALL_DTCS_CODES[self._get_dcs_index(_mem, 'rx')] rxpol = _mem.rxinv and "R" or "N" txpol = _mem.txinv and "R" or "N" chirp_common.split_tone_decode(mem, (txmode, txtone, txpol), (rxmode, rxtone, rxpol)) mem.skip = _flg.get_skip() and "S" or _flg.get_pskip() and "P" or "" mem.power = POWER_LEVELS[_mem.power] mem.extra = RadioSettingGroup("Extra", "extra") rs = RadioSetting("rev", "Reverse", RadioSettingValueBoolean(_mem.rev)) mem.extra.append(rs) rs = RadioSetting("compander", "Compander", RadioSettingValueBoolean(_mem.compander)) mem.extra.append(rs) rs = RadioSetting("talkaround", "Talkaround", RadioSettingValueBoolean(_mem.talkaround)) mem.extra.append(rs) rs = RadioSetting("pttid", "PTT ID", RadioSettingValueList( PTT_IDS, current_index=_mem.pttid)) mem.extra.append(rs) rs = RadioSetting("bclo", "Busy Channel Lockout", RadioSettingValueList(BCLO, current_index=_mem.bclo)) mem.extra.append(rs) rs = RadioSetting("optsig", "Optional Signaling", RadioSettingValueList(OPT_SIGS, current_index=_mem.optsig)) mem.extra.append(rs) rs = RadioSetting("OPTSIGSQL", "Squelch w/Opt Signaling", RadioSettingValueList( OPT_SIG_SQL, SQL_MODES[_mem.sqlMode] if SQL_MODES[_mem.sqlMode] in OPT_SIG_SQL else "Off")) mem.extra.append(rs) rs = RadioSetting( "dtmfSlotNum", "DTMF", RadioSettingValueList( DTMF_SLOTS, current_index=_mem.dtmfSlotNum)) mem.extra.append(rs) rs = RadioSetting("twotone", "2-Tone", RadioSettingValueList(TONE2_SLOTS, current_index=_mem.twotone)) mem.extra.append(rs) rs = RadioSetting("fivetone", "5-Tone", RadioSettingValueList(TONE5_SLOTS, current_index=_mem.fivetone)) mem.extra.append(rs) # Chose not to expose scramble rs = RadioSetting("scramble", # "Scrambler Switch", RadioSettingValueList(SCRAMBLE_CODES, # SCRAMBLE_CODES[_mem.scramble])) mem.extra.append(rs) # Memory properties dialog is only capable of Boolean and List # RadioSettingValue classes, so cannot configure it rs = # RadioSetting("custtone", "Custom CTCSS", # RadioSettingValueFloat(min(TONES), max(TONES), _mem.custtone # and _mem.custtone / 10 or 151.1, 0.1, 1)) # mem.extra.append(rs) custToneStr = chirp_common.format_freq(_mem.custtone) rs = RadioSetting("CUSTTONETX", "Use Custom CTCSS (%s) for Tx" % custToneStr, RadioSettingValueBoolean(_mem.txtone == len(TONES))) mem.extra.append(rs) rs = RadioSetting("CUSTTONERX", "Use Custom CTCSS (%s) for Rx" % custToneStr, RadioSettingValueBoolean(_mem.rxtone == len(TONES))) mem.extra.append(rs) return mem def set_memory(self, mem): _mem, _flg = self._get_memobjs(mem.number) if mem.empty: _flg.set() return _flg.clear() _mem.set_raw("\x00" * MEMORY_LOCATION_SIZE) _mem.freq = mem.freq / 100 _mem.offset = mem.offset / 100 _mem.name = mem.name.ljust(7) _mem.is_am = mem.mode == "AM" _mem.tune_step = TUNING_STEPS.index(mem.tuning_step) if mem.duplex == "off": _mem.duplex = DUPLEXES.index("") _mem.txoff = 1 else: _mem.duplex = DUPLEXES.index(mem.duplex) _mem.txoff = 0 try: _mem.channel_width = MODES.index(mem.mode) except ValueError: _mem.channel_width = 0 ((txmode, txtone, txpol), (rxmode, rxtone, rxpol)) = chirp_common.split_tone_encode(mem) _mem.txtmode = TMODES.index(txmode) _mem.rxtmode = TMODES.index(rxmode) if rxmode != '': _mem.sqlMode = SQL_MODES.index("CTCSS/DCS") else: _mem.sqlMode = SQL_MODES.index("Carrier") if txmode == "Tone": _mem.txtone = TONES.index(txtone) elif txmode == "DTCS": self._set_dcs_index(_mem, 'tx', chirp_common.ALL_DTCS_CODES.index(txtone)) if rxmode == "Tone": _mem.rxtone = TONES.index(rxtone) elif rxmode == "DTCS": self._set_dcs_index(_mem, 'rx', chirp_common.ALL_DTCS_CODES.index(rxtone)) _mem.txinv = txpol == "R" _mem.rxinv = rxpol == "R" _flg.set_skip(mem.skip == "S") _flg.set_pskip(mem.skip == "P") if mem.power: _mem.power = POWER_LEVELS.index(mem.power) else: _mem.power = 0 for setting in mem.extra: if setting.get_name() == "ignore": LOG.debug("*** ignore: %s" % str(setting.value)) # Future: elif setting.get_name() == "custtone": Future: # setattr(_mem, "custtone", setting.value.get_value() * # 10) elif setting.get_name() == "OPTSIGSQL": if str(setting.value) != "Off": _mem.sqlMode = SQL_MODES.index(str(setting.value)) elif setting.get_name() == "CUSTTONETX": if setting.value: _mem.txtone = len(TONES) elif setting.get_name() == "CUSTTONERX": if setting.value: _mem.rxtone = len(TONES) else: setattr(_mem, setting.get_name(), setting.value) return mem def get_settings(self): _settings = self._memobj.settings basic = RadioSettingGroup("basic", "Basic") settings = RadioSettings(basic) display = ["Frequency", "Channel", "Name"] rs = RadioSetting("display", "Display", RadioSettingValueList( display, current_index=_settings.display)) basic.append(rs) apo = ["Off"] + ['%.1f hour(s)' % (0.5 * x) for x in range(1, 25)] rs = RadioSetting("apo", "Automatic Power Off", RadioSettingValueList(apo, current_index=_settings.apo)) basic.append(rs) def filter(s): s_ = "" for i in range(0, 8): c = str(s[i]) s_ += (c if c in chirp_common.CHARSET_ASCII else "") return s_ welcome_msg = ''.join(filter(_settings.welcome)) rs = RadioSetting("welcome", "Welcome Message", RadioSettingValueString(0, 8, welcome_msg)) basic.append(rs) rs = RadioSetting("beep", "Beep Enabled", RadioSettingValueBoolean(_settings.beep)) basic.append(rs) MUTE_CHOICES = ["Off", "TX", "RX", "TX/RX"] rs = RadioSetting("mute", "Sub Band Mute", RadioSettingValueList(MUTE_CHOICES, current_index=_settings.mute)) basic.append(rs) dtmf = RadioSettingGroup('dtmf', 'DTMF') for i in range(16): length = int(self._memobj.dtmf_codes[i].len) val = bitwise.bcd_to_numeric_str(self._memobj.dtmf_codes[i].code) val = val[:length] val = val.replace('E', '*').replace('F', '#') rs = RadioSetting('dtmf%i' % i, 'DTMF M%i' % (i + 1), RadioSettingValueString( 0, 24, val, autopad=False, charset='01234567890*#abcdABCD')) dtmf.append(rs) settings.append(dtmf) return settings def set_settings(self, settings): _settings = self._memobj.settings for element in settings: if element.get_name() == 'dtmf': for i in range(16): val = str(element['dtmf%i' % i].value).strip() val = val.replace('*', 'E').replace('#', 'F') self._memobj.dtmf_codes[i].len = len(val) bitwise.numeric_str_to_bcd( self._memobj.dtmf_codes[i].code, val) continue if not isinstance(element, RadioSetting): self.set_settings(element) continue name = element.get_name() setattr(_settings, name, element.value) @classmethod def match_model(cls, filedata, filename): for _ident in cls._file_ident: if _ident in filedata[0x20:0x40]: return True return False @directory.register class IntekHR2040Radio(AnyTone5888UVRadio): """Intek HR-2040""" VENDOR = "Intek" MODEL = "HR-2040" _file_ident = [b"HR-2040"] @directory.register class PolmarDB50MRadio(AnyTone5888UVRadio): """Polmar DB-50M""" VENDOR = "Polmar" MODEL = "DB-50M" _file_ident = [b"DB-50M"] @directory.register class PowerwerxDB750XRadio(AnyTone5888UVRadio): """Powerwerx DB-750X""" VENDOR = "Powerwerx" MODEL = "DB-750X" _file_ident = [b"DB-750X"] def get_settings(self): return {} chirp-20261030/chirp/drivers/anytone778uv.py0000644000175000017500000020672615172631605017510 0ustar hibbyhibby# Copyright 2020 Joe Milbourn # Copyright 2021 Jim Unroe # # 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 . # # TODO use the band field from ver_response # TODO handle radio settings # # Supported features # * Read and write memory access for 200 normal memories # * CTCSS and DTCS for transmit and receive # * Scan list # * Tx off # * Duplex (+ve, -ve, odd, and off splits) # * Transmit power # * Channel width (25 kHz and 12.5 kHz) # * Retevis RT95, CRT Micron UV, and Midland DBR2500 radios # * Full range of frequencies for tx and rx, supported band read from radio # during download, not verified on upload. Radio will refuse to TX if out of # band. # * Busy channel lock out (for each memory) # # Unsupported features # * VFO1, VFO2, and TRF memories # * custom CTCSS tones # * Any non-memory radio settings # * Reverse, talkaround, scramble # * probably other things too - like things encoded by the unknown bits in the # memory struct from chirp import chirp_common, directory, memmap, errors, util, checksum from chirp import bitwise from chirp.settings import RadioSettingGroup, RadioSetting, \ RadioSettingValueBoolean, RadioSettingValueList, \ RadioSettingValueString, RadioSettings import struct import time import logging LOG = logging.getLogger(__name__) # 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. MEM_FORMAT = ''' #seekto 0x0000; struct { bbcd freq[4]; bbcd offset[4]; u8 unknown1; u8 talkaround:1, scramble:1, unknown:2, txpower:2, duplex:2; u8 unknown_bits1:4, channel_width:2, reverse:1, tx_off:1; u8 unknown_bits2:4, dtcs_decode_en:1, ctcss_decode_en:1, dtcs_encode_en:1, ctcss_encode_en:1; u8 ctcss_dec_tone; u8 ctcss_enc_tone; u8 dtcs_decode_code; u8 unknown_bits6:6, dtcs_decode_invert:1, dtcs_decode_code_highbit:1; u8 dtcs_encode_code; u8 unknown_bits7:6, dtcs_encode_invert:1, dtcs_encode_code_highbit:1; u8 unknown_bits4:6, busy_channel_lockout:2; u8 unknown6; u8 unknown_bits5:7, tone_squelch_en:1; u8 unknown7; u8 unknown8; u8 unknown9; char name[6]; // the VOX models have 6-character names // the original models only have 5-character names // the 1st byte is not used and will always be 0x00 ul16 customctcss; } memory[200]; #seekto 0x1940; struct { u8 occupied_bitfield[32]; u8 scan_enabled_bitfield[32]; } memory_status; #seekto 0x1980; struct { char line[7]; // starting display } starting_display; #seekto 0x1990; struct { u8 code[16]; // DTMF Encode M1-M16 } pttid[16]; #seekto 0x1A90; struct { u8 pttIdStart[16]; // 0x1A90 ptt id starting u8 pttIdEnd[16]; // 0x1AA0 ptt id ending u8 remoteStun[16]; // 0x1AB0 remotely stun u8 remoteKill[16]; // 0x1AC0 remotely kill u8 intervalChar; // 0x1AD0 dtmf interval character u8 groupCode; // 0x1AD1 group code u8 unk1ad2:6, // 0x1AD2 decodingResponse:2; // decoding response u8 pretime; // 0x1AD3 pretime u8 firstDigitTime; // 0x1AD4 first digit time u8 autoResetTime; // 0x1AD5 auto reset time u8 selfID[3]; // 0x1AD6 dtmf self id u8 unk1ad9:7, // 0x1AD9 sideTone:1; // side tone u8 timeLapse; // 0x1ADA time-lapse after encode u8 pauseTime; // 0x1ADB ptt id pause time } dtmf; #seekto 0x3200; struct { u8 unk3200:5, // 0x3200 beepVolume:3; // beep volume u8 unk3201:4, // 0x3201 frequencyStep:4; // frequency step u8 unk3202:6, // 0x3202 displayMode:2; // display mode u8 unk0x3203; u8 unk3204:4, // 0x3204 squelchLevelA:4; // squelch level a u8 unk3205:4, // 0x3205 squelchLevelB:4; // squelch level b u8 unk3206:2, // 0x3206 speakerVol:6; // speaker volume u8 unk3207:7, // 0x3207 powerOnPasswd:1; // power-on password u8 unk3208:6, // 0x3208 scanType:2; // scan type u8 unk3209:6, // 0x3209 scanRecoveryT:2; // scan recovery time u8 unk320a:7, // 0x320A autoPowerOn:1; // auto power on u8 unk320b:7, // 0x320B main:1; // main u8 unk320c:7, // 0x320C dualWatch:1; // dual watch (rx way select) u8 unk320d:5, // 0x320D backlightBr:3; // backlight brightness u8 unk320e:3, // 0x320E timeOutTimer:5; // time out timer u8 unk320f:6, // 0x320F autoPowerOff:2; // auto power off u8 unk3210:6, // 0x3210 tbstFrequency:2; // tbst frequency u8 unk3211:7, // 0x3211 screenDir:1; // screen direction u8 unk3212:2, // 0x3212 micKeyBrite:6; // hand mic key brightness u8 unk3213:6, // 0x3213 speakerSwitch:2; // speaker switch u8 keyPA; // 0x3214 key pa u8 keyPB; // 0x3215 key pb u8 keyPC; // 0x3216 key pc u8 keyPD; // 0x3217 key pd u8 unk3218:5, // 0x3218 steType:3; // ste type u8 unk3219:6, // 0x3219 steFrequency:2; // ste frequency u8 unk321a:5, // 0x321A dtmfTxTime:3; // dtmf transmitting time u8 unk_bit7_6:2, // 0x321B monKeyFunction:1, // mon key function channelLocked:1, // channel locked saveChParameter:1, // save channel parameter powerOnReset:1, // power on reset trfEnable:1, // trf enable knobMode:1; // knob mode u8 unk321c:7, // 0x321C voxOnOff:1; // vox on/off u8 voxLevel; // 0x321D vox level u8 voxDelay; // 0x321E vox delay } settings; #seekto 0x3240; struct { char digits[6]; // password } password; #seekto 0x3250; struct { u8 keyMode1P1; // 0x3250 key mode 1 p1 u8 keyMode1P2; // 0x3251 key mode 1 p2 u8 keyMode1P3; // 0x3252 key mode 1 p3 u8 keyMode1P4; // 0x3253 key mode 1 p4 u8 keyMode1P5; // 0x3254 key mode 1 p5 u8 keyMode1P6; // 0x3255 key mode 1 p6 u8 keyMode2P1; // 0x3256 key mode 2 p1 u8 keyMode2P2; // 0x3257 key mode 2 p2 u8 keyMode2P3; // 0x3258 key mode 2 p3 u8 keyMode2P4; // 0x3259 key mode 2 p4 u8 keyMode2P5; // 0x325A key mode 2 p5 u8 keyMode2P6; // 0x325B key mode 2 p6 } pfkeys; #seekto 0x3260; struct { u8 mrChanA; // 0x3260 mr channel a u8 unknown1_0:7, // 0x3261 vfomrA:1; // vfo/mr mode a u8 unknown2; u8 unknown3; u8 unknown4; u8 unknown5; u8 unknown6; u8 mrChanB; // 0x3267 mr channel b u8 unknown8_0:4, // 0x3268 scan_active:1, unknown8_1:2, vfomrB:1; // vfo/mr mode b u8 unknown9; u8 unknowna; u8 unknownb; u8 unknownc; u8 bandlimit; // 0x326D mode u8 unknownd; u8 unknowne; u8 unknownf; } radio_settings; ''' # Format for the version messages returned by the radio VER_FORMAT = ''' u8 hdr; char model[7]; u8 bandlimit; char version[6]; u8 ack; ''' TXPOWER_LOW = 0x00 TXPOWER_MED = 0x01 TXPOWER_HIGH = 0x02 DUPLEX_NOSPLIT = 0x00 DUPLEX_POSSPLIT = 0x01 DUPLEX_NEGSPLIT = 0x02 DUPLEX_ODDSPLIT = 0x03 CHANNEL_WIDTH_25kHz = 0x02 CHANNEL_WIDTH_20kHz = 0x01 CHANNEL_WIDTH_12d5kHz = 0x00 BUSY_CHANNEL_LOCKOUT_OFF = 0x00 BUSY_CHANNEL_LOCKOUT_REPEATER = 0x01 BUSY_CHANNEL_LOCKOUT_BUSY = 0x02 MEMORY_ADDRESS_RANGE = (0x0000, 0x3290) MEMORY_RW_BLOCK_SIZE = 0x10 MEMORY_RW_BLOCK_CMD_SIZE = 0x16 POWER_LEVELS = [chirp_common.PowerLevel('Low', dBm=37), chirp_common.PowerLevel('Medium', dBm=40), chirp_common.PowerLevel('High', dBm=44)] # CTCSS Tone definitions TONE_CUSTOM_CTCSS = 0x33 TONE_MAP_VAL_TO_TONE = {0x00: 62.5, 0x01: 67.0, 0x02: 69.3, 0x03: 71.9, 0x04: 74.4, 0x05: 77.0, 0x06: 79.7, 0x07: 82.5, 0x08: 85.4, 0x09: 88.5, 0x0a: 91.5, 0x0b: 94.8, 0x0c: 97.4, 0x0d: 100.0, 0x0e: 103.5, 0x0f: 107.2, 0x10: 110.9, 0x11: 114.8, 0x12: 118.8, 0x13: 123.0, 0x14: 127.3, 0x15: 131.8, 0x16: 136.5, 0x17: 141.3, 0x18: 146.2, 0x19: 151.4, 0x1a: 156.7, 0x1b: 159.8, 0x1c: 162.2, 0x1d: 165.5, 0x1e: 167.9, 0x1f: 171.3, 0x20: 173.8, 0x21: 177.3, 0x22: 179.9, 0x23: 183.5, 0x24: 186.2, 0x25: 189.9, 0x26: 192.8, 0x27: 196.6, 0x28: 199.5, 0x29: 203.5, 0x2a: 206.5, 0x2b: 210.7, 0x2c: 218.1, 0x2d: 225.7, 0x2e: 229.1, 0x2f: 233.6, 0x30: 241.8, 0x31: 250.3, 0x32: 254.1} TONE_MAP_TONE_TO_VAL = {TONE_MAP_VAL_TO_TONE[val]: val for val in TONE_MAP_VAL_TO_TONE} TONES_EN_TXTONE = (1 << 3) TONES_EN_RXTONE = (1 << 2) TONES_EN_TXCODE = (1 << 1) TONES_EN_RXCODE = (1 << 0) TONES_EN_NO_TONE = 0 # Radio supports upper case and symbols CHARSET_ASCII_PLUS = chirp_common.CHARSET_UPPER_NUMERIC + '- ' # Band limits as defined by the band byte in ver_response, defined in Hz, for # VHF and UHF, used for RX and TX. BAND_LIMITS = {0x00: [(144000000, 148000000), (430000000, 440000000)], 0x01: [(136000000, 174000000), (400000000, 490000000)], 0x02: [(144000000, 146000000), (430000000, 440000000)]} # Get band limits from a band limit value def get_band_limits_Hz(limit_value): if limit_value not in BAND_LIMITS: limit_value = 0x01 LOG.warning('Unknown band limit value 0x%02x, default to 0x01') bandlimitfrequencies = BAND_LIMITS[limit_value] return bandlimitfrequencies # Calculate the checksum used in serial packets # Send a command to the radio, return any reply stripping the echo of the # command (tx and rx share a single pin in this radio) def send_serial_command(serial, command, expectedlen=None): ''' send a command to the radio, and return any response. set expectedlen to return as soon as that many bytes are read. ''' serial.write(command) serial.flush() response = b'' tout = time.time() + 0.5 while time.time() < tout: if serial.inWaiting(): response += serial.read() # remember everything gets echo'd back if len(response) - len(command) == expectedlen: break # cut off what got echo'd back, we don't need to see it again if response.startswith(command): response = response[len(command):] return response # strip trailing 0x00 to convert a string returned by bitwise.parse into a # python string def cstring_to_py_string(cstring): return "".join(c for c in cstring if c != '\x00') # Check the radio version reported to see if it's one we support, # returns bool version supported, the band index, and has_vox def check_ver(ver_response, allowed_types): ''' Check the returned radio version is one we approve of ''' LOG.debug('ver_response = ') LOG.debug(util.hexprint(ver_response)) resp = bitwise.parse(VER_FORMAT, ver_response) verok = False if resp.hdr == 0x49 and resp.ack == 0x06: model, version = [cstring_to_py_string(bitwise.get_string(s)).strip() for s in (resp.model, resp.version)] LOG.debug('radio model: \'%s\' version: \'%s\'' % (model, version)) LOG.debug('allowed_types = %s' % allowed_types) if model in allowed_types: LOG.debug('model in allowed_types') if version in allowed_types[model]: LOG.debug('version in allowed_types[model]') verok = True else: raise errors.RadioError('Failed to parse version response') return verok, int(resp.bandlimit) # Put the radio in programming mode, sending the initial command and checking # the response. raise RadioError if there is no response (500ms timeout), and # if the returned version isn't matched by check_ver def enter_program_mode(serial): # place the radio in program mode, and confirm program_response = send_serial_command(serial, b'PROGRAM') if not program_response: raise errors.RadioNoResponse() if program_response != b'QX\x06': raise errors.RadioError('No initial response from radio.') LOG.debug('entered program mode') # read the radio ID string, make sure it matches one we know about return send_serial_command(serial, b'\x02') def get_bandlimit_from_ver(radio, ver_response): verok, bandlimit, = check_ver(ver_response, radio.ALLOWED_RADIO_TYPES) if not verok: LOG.debug('Radio version response not allowed for %s-%s: %s', radio.VENDOR, radio.MODEL, ver_response) raise errors.RadioError('Radio model/version mismatch') return bandlimit # Exit programming mode def exit_program_mode(serial): try: send_serial_command(serial, b'END') except Exception as e: LOG.error('Failed to exit programming mode: %s', e) # Parse a packet from the radio returning the header (R/W, address, data, and # checksum valid def parse_read_response(resp): if not resp: raise errors.RadioNoResponse() if len(resp) < 6: raise errors.RadioError('Short read response from radio') addr = resp[:4] data = bytes(resp[4:-2]) cs = checksum.checksum_8bit(d for d in resp[1:-2]) valid = cs == resp[-2] if not valid: LOG.error('checksumfail: %02x, expected %02x' % (cs, resp[-2])) LOG.error('msg data: %s' % util.hexprint(resp)) return addr, data, valid # Download data from the radio and populate the memory map def do_download(radio): '''Download memories from the radio''' # NOTE: The radio is already in programming mode because of # detect_from_serial() # Get the serial port connection serial = radio.pipe try: memory_data = bytes() # status info for the UI status = chirp_common.Status() status.cur = 0 status.max = (MEMORY_ADDRESS_RANGE[1] - MEMORY_ADDRESS_RANGE[0]) // MEMORY_RW_BLOCK_SIZE status.msg = 'Cloning from radio...' radio.status_fn(status) for addr in range(MEMORY_ADDRESS_RANGE[0], MEMORY_ADDRESS_RANGE[1] + MEMORY_RW_BLOCK_SIZE, MEMORY_RW_BLOCK_SIZE): read_command = struct.pack('>BHB', 0x52, addr, MEMORY_RW_BLOCK_SIZE) read_response = send_serial_command(serial, read_command, MEMORY_RW_BLOCK_CMD_SIZE) # LOG.debug('read response:\n%s' % util.hexprint(read_response)) address, data, valid = parse_read_response(read_response) if not valid: raise errors.RadioError('Invalid response received from radio') memory_data += data # update UI status.cur = (addr - MEMORY_ADDRESS_RANGE[0])\ // MEMORY_RW_BLOCK_SIZE radio.status_fn(status) except errors.RadioError as e: raise e except Exception as e: raise errors.RadioError('Failed to download from radio: %s' % e) finally: exit_program_mode(radio.pipe) return memmap.MemoryMapBytes(memory_data) # Build a write data command to send to the radio def make_write_data_cmd(addr, data, datalen): cmd = struct.pack('>BHB', 0x57, addr, datalen) cmd += data cs = checksum.checksum_8bit(c for c in cmd[1:]) cmd += struct.pack('>BB', cs, 0x06) return cmd # Upload a memory map to the radio def do_upload(radio): try: ver_response = enter_program_mode(radio.pipe) bandlimit = get_bandlimit_from_ver(radio, ver_response) if bandlimit != radio._memobj.radio_settings.bandlimit: LOG.warning('radio and image bandlimits differ' ' some channels many not work' ' (img:0x%02x radio:0x%02x)' % (int(bandlimit), int(radio._memobj.radio_settings.bandlimit))) LOG.warning('radio bands: %s' % get_band_limits_Hz( int(radio._memobj.radio_settings.bandlimit))) LOG.warning('img bands: %s' % get_band_limits_Hz(bandlimit)) serial = radio.pipe # send the initial message, radio responds with something that looks a # bit like a bitfield, but I don't know what it is yet. read_command = struct.pack('>BHB', 0x52, 0x3b10, MEMORY_RW_BLOCK_SIZE) read_response = send_serial_command(serial, read_command, MEMORY_RW_BLOCK_CMD_SIZE) address, data, valid = parse_read_response(read_response) LOG.debug('Got initial response from radio: %s' % util.hexprint(read_response)) bptr = 0 memory_addrs = list(range(MEMORY_ADDRESS_RANGE[0], MEMORY_ADDRESS_RANGE[1] + MEMORY_RW_BLOCK_SIZE, MEMORY_RW_BLOCK_SIZE)) # status info for the UI status = chirp_common.Status() status.cur = 0 status.max = len(memory_addrs) status.msg = 'Cloning to radio...' radio.status_fn(status) for idx, addr in enumerate(memory_addrs): write_command = make_write_data_cmd( addr, radio._mmap[bptr:bptr+MEMORY_RW_BLOCK_SIZE], MEMORY_RW_BLOCK_SIZE) # LOG.debug('write data:\n%s' % util.hexprint(write_command)) write_response = send_serial_command(serial, write_command, 0x01) bptr += MEMORY_RW_BLOCK_SIZE if write_response == '\x0a': # NACK from radio, e.g. checksum wrong LOG.debug('Radio returned 0x0a - NACK:') LOG.debug(' * write cmd:\n%s' % util.hexprint(write_command)) LOG.debug(' * write response:\n%s' % util.hexprint(write_response)) exit_program_mode(radio.pipe) raise errors.RadioError('Radio NACK\'d write command') # update UI status.cur = idx radio.status_fn(status) except errors.RadioError: raise except Exception as e: raise errors.RadioError('Failed to download from radio: %s' % e) finally: exit_program_mode(radio.pipe) # Get the value of @bitfield @number of bits in from 0 def get_bitfield(bitfield, number): ''' Get the value of @bitfield @number of bits in ''' byteidx = number//8 bitidx = number - (byteidx * 8) return bitfield[byteidx] & (1 << bitidx) # Set the @value of @bitfield @number of bits in from 0 def set_bitfield(bitfield, number, value): ''' Set the @value of @bitfield @number of bits in ''' byteidx = number//8 bitidx = number - (byteidx * 8) if value is True: bitfield[byteidx] |= (1 << bitidx) else: bitfield[byteidx] &= ~(1 << bitidx) return bitfield # Translate the radio's version of a code as stored to a real code def dtcs_code_bits_to_val(highbit, lowbyte): return chirp_common.ALL_DTCS_CODES[highbit*256 + lowbyte] # Translate the radio's version of a tone as stored to a real tone def ctcss_tone_bits_to_val(tone_byte): # TODO use the custom setting 0x33 and ref the custom ctcss # field tone_byte = int(tone_byte) if tone_byte in TONE_MAP_VAL_TO_TONE: return TONE_MAP_VAL_TO_TONE[tone_byte] elif tone_byte == TONE_CUSTOM_CTCSS: LOG.info('custom ctcss not implemented (yet?).') else: raise errors.UnsupportedToneError('unknown ctcss tone value: %02x' % tone_byte) # Translate a real tone to the radio's version as stored def ctcss_code_val_to_bits(tone_value): if tone_value in TONE_MAP_TONE_TO_VAL: return TONE_MAP_TONE_TO_VAL[tone_value] else: raise errors.UnsupportedToneError('Tone %f not supported' % tone_value) # Translate a real code to the radio's version as stored def dtcs_code_val_to_bits(code): val = chirp_common.ALL_DTCS_CODES.index(code) return (val & 0xFF), ((val >> 8) & 0x01) class AnyTone778UVBase(chirp_common.CloneModeRadio, chirp_common.ExperimentalRadio): '''AnyTone 778UV and probably Retevis RT95 and others''' BAUD_RATE = 9600 NAME_LENGTH = 5 HAS_VOX = False @classmethod def detect_from_serial(cls, pipe): ver_response = enter_program_mode(pipe) for radio_cls in cls.detected_models(): ver_ok, _ = check_ver(ver_response, radio_cls.ALLOWED_RADIO_TYPES) if ver_ok: return radio_cls LOG.warning('No match for ver_response: %s', util.hexprint(ver_response)) raise errors.RadioError('Incorrect radio model') @classmethod def get_prompts(cls): rp = chirp_common.RadioPrompts() rp.experimental = \ ('This is experimental support for the %s %s. ' 'Please send in bug and enhancement requests!' % (cls.VENDOR, cls.MODEL)) return rp # 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.has_settings = True rf.can_odd_split = True rf.has_name = True rf.has_offset = True rf.valid_name_length = self.NAME_LENGTH rf.valid_duplexes = ['', '+', '-', 'split', 'off'] rf.valid_characters = CHARSET_ASCII_PLUS rf.has_dtcs = True rf.has_rx_dtcs = True rf.has_dtcs_polarity = True rf.valid_dtcs_codes = chirp_common.ALL_DTCS_CODES rf.has_ctone = True rf.has_cross = True rf.valid_tmodes = ['', 'Tone', 'TSQL', 'DTCS', 'Cross'] rf.valid_cross_modes = ['Tone->Tone', 'Tone->DTCS', 'DTCS->Tone', 'DTCS->DTCS', 'DTCS->', '->DTCS', '->Tone'] rf.memory_bounds = (1, 200) # This radio supports memories 1-200 try: rf.valid_bands = get_band_limits_Hz( int(self._memobj.radio_settings.bandlimit)) except AttributeError: # If we're asked without memory loaded, assume the most permissive rf.valid_bands = get_band_limits_Hz(1) except Exception as e: LOG.exception( 'Failed to get band limits for anytone778uv: %s' % e) rf.valid_bands = get_band_limits_Hz(1) rf.valid_modes = ['FM', 'NFM'] rf.valid_power_levels = POWER_LEVELS rf.valid_tuning_steps = [2.5, 5, 6.25, 10, 12.5, 20, 25, 30, 50] rf.has_tuning_step = False return rf # Do a download of the radio from the serial port def sync_in(self): self._mmap = do_download(self) self.process_mmap() # Do an upload of the radio to the serial port def sync_out(self): do_upload(self) # Convert the raw byte array into a memory object structure def process_mmap(self): self._memobj = bitwise.parse(MEM_FORMAT, self._mmap) # 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 - 1]) # 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): number -= 1 # Get a low-level memory object mapped to the image _mem = self._memobj.memory[number] _mem_status = self._memobj.memory_status # Create a high-level memory object to return to the UI mem = chirp_common.Memory() mem.number = number + 1 # Set the memory number # Check if this memory is present in the occupied list mem.empty = get_bitfield(_mem_status.occupied_bitfield, number) == 0 if not mem.empty: # Check if this memory is in the scan enabled list mem.skip = '' if get_bitfield(_mem_status.scan_enabled_bitfield, number) == 0: mem.skip = 'S' # set the name if self.NAME_LENGTH == 5: # original models with 5-character name length temp_name = str(_mem.name) # Strip the first character and set the alpha tag mem.name = str(temp_name[1:6]).rstrip() else: # new VOX models with 6-character name length mem.name = str(_mem.name).rstrip() # Set the alpha tag # Convert your low-level frequency and offset to Hertz mem.freq = int(_mem.freq) * 10 mem.offset = int(_mem.offset) * 10 # Set the duplex flags if _mem.duplex == DUPLEX_POSSPLIT: mem.duplex = '+' elif _mem.duplex == DUPLEX_NEGSPLIT: mem.duplex = '-' elif _mem.duplex == DUPLEX_NOSPLIT: mem.duplex = '' elif _mem.duplex == DUPLEX_ODDSPLIT: mem.duplex = 'split' else: LOG.error('%s: get_mem: unhandled duplex: %02x' % (mem.name, _mem.duplex)) # handle tx off if _mem.tx_off: mem.duplex = 'off' # Set the channel width if _mem.channel_width == CHANNEL_WIDTH_25kHz: mem.mode = 'FM' elif _mem.channel_width == CHANNEL_WIDTH_20kHz: LOG.info( '%s: get_mem: promoting 20 kHz channel width to 25 kHz' % mem.name) mem.mode = 'FM' elif _mem.channel_width == CHANNEL_WIDTH_12d5kHz: mem.mode = 'NFM' else: LOG.error('%s: get_mem: unhandled channel width: 0x%02x' % (mem.name, _mem.channel_width)) # set the power level if _mem.txpower == TXPOWER_LOW: mem.power = POWER_LEVELS[0] elif _mem.txpower == TXPOWER_MED: mem.power = POWER_LEVELS[1] elif _mem.txpower == TXPOWER_HIGH: mem.power = POWER_LEVELS[2] else: LOG.error('%s: get_mem: unhandled power level: 0x%02x' % (mem.name, _mem.txpower)) # CTCSS Tones # TODO support custom ctcss tones here txtone = None rxtone = None rxcode = None txcode = None # check if dtcs tx is enabled if _mem.dtcs_encode_en: txcode = dtcs_code_bits_to_val(_mem.dtcs_encode_code_highbit, _mem.dtcs_encode_code) # check if dtcs rx is enabled if _mem.dtcs_decode_en: rxcode = dtcs_code_bits_to_val(_mem.dtcs_decode_code_highbit, _mem.dtcs_decode_code) if txcode is not None: LOG.debug('%s: get_mem dtcs_enc: %d' % (mem.name, txcode)) if rxcode is not None: LOG.debug('%s: get_mem dtcs_dec: %d' % (mem.name, rxcode)) # tsql set if radio squelches on tone tsql = _mem.tone_squelch_en # check if ctcss tx is enabled if _mem.ctcss_encode_en: txtone = ctcss_tone_bits_to_val(_mem.ctcss_enc_tone) # check if ctcss rx is enabled if _mem.ctcss_decode_en: rxtone = ctcss_tone_bits_to_val(_mem.ctcss_dec_tone) # Define this here to allow a readable if-else tree enabling tone # options enabled = 0 enabled |= (txtone is not None) * TONES_EN_TXTONE enabled |= (rxtone is not None) * TONES_EN_RXTONE enabled |= (txcode is not None) * TONES_EN_TXCODE enabled |= (rxcode is not None) * TONES_EN_RXCODE # Add some debugging output for the tone bitmap enstr = [] if enabled & TONES_EN_TXTONE: enstr += ['TONES_EN_TXTONE'] if enabled & TONES_EN_RXTONE: enstr += ['TONES_EN_RXTONE'] if enabled & TONES_EN_TXCODE: enstr += ['TONES_EN_TXCODE'] if enabled & TONES_EN_RXCODE: enstr += ['TONES_EN_RXCODE'] if enabled == 0: enstr = ['TONES_EN_NOTONE'] LOG.debug('%s: enabled = %s' % ( mem.name, '|'.join(enstr))) mem.tmode = '' if enabled == TONES_EN_NO_TONE: mem.tmode = '' elif enabled == TONES_EN_TXTONE: mem.tmode = 'Tone' mem.rtone = txtone elif enabled == TONES_EN_RXTONE and tsql: mem.tmode = 'Cross' mem.cross_mode = '->Tone' mem.ctone = rxtone elif enabled == (TONES_EN_TXTONE | TONES_EN_RXTONE) and tsql: if txtone == rxtone: # TSQL mem.tmode = 'TSQL' mem.ctone = txtone else: # Tone->Tone mem.tmode = 'Cross' mem.cross_mode = 'Tone->Tone' mem.ctone = rxtone mem.rtone = txtone elif enabled == TONES_EN_TXCODE: mem.tmode = 'Cross' mem.cross_mode = 'DTCS->' mem.dtcs = txcode elif enabled == TONES_EN_RXCODE and tsql: mem.tmode = 'Cross' mem.cross_mode = '->DTCS' mem.rx_dtcs = rxcode elif enabled == (TONES_EN_TXCODE | TONES_EN_RXCODE) and tsql: if rxcode == txcode: mem.tmode = 'DTCS' mem.rx_dtcs = rxcode # #8327 Not sure this is the correct interpretation of # DevelopersToneModes, but it seems to make it work round # tripping with the Anytone software. DTM implies that we # might not need to set mem.dtcs, but if we do it only DTCS # rx works (as if we were Cross:None->DTCS). mem.dtcs = rxcode else: mem.tmode = 'Cross' mem.cross_mode = 'DTCS->DTCS' mem.rx_dtcs = rxcode mem.dtcs = txcode elif enabled == (TONES_EN_TXCODE | TONES_EN_RXTONE) and tsql: mem.tmode = 'Cross' mem.cross_mode = 'DTCS->Tone' mem.dtcs = txcode mem.ctone = rxtone elif enabled == (TONES_EN_TXTONE | TONES_EN_RXCODE) and tsql: mem.tmode = 'Cross' mem.cross_mode = 'Tone->DTCS' mem.rx_dtcs = rxcode mem.rtone = txtone else: LOG.error('%s: Unhandled tmode enabled = %d.' % ( mem.name, enabled)) # Can get here if e.g. TONE_EN_RXCODE is set and tsql isn't # In that case should we perhaps store the tone and code values # if they're present and then setup tmode and cross_mode as # appropriate later? # set the dtcs polarity dtcs_pol_bit_to_str = {0: 'N', 1: 'R'} mem.dtcs_polarity = '%s%s' %\ (dtcs_pol_bit_to_str[_mem.dtcs_encode_invert == 1], dtcs_pol_bit_to_str[_mem.dtcs_decode_invert == 1]) # Extra mem.extra = RadioSettingGroup("Extra", "extra") # Busy channel lockout bcl_options = ['Off', 'Repeater', 'Busy'] bcl_option = bcl_options[_mem.busy_channel_lockout] rs = RadioSetting("busy_channel_lockout", "Busy Channel Lockout", RadioSettingValueList(bcl_options, bcl_option)) mem.extra.append(rs) 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 - 1] _mem_status = self._memobj.memory_status # set the occupied bitfield _mem_status.occupied_bitfield = \ set_bitfield(_mem_status.occupied_bitfield, mem.number - 1, not mem.empty) # set the scan add bitfield _mem_status.scan_enabled_bitfield = \ set_bitfield(_mem_status.scan_enabled_bitfield, mem.number - 1, (not mem.empty) and (mem.skip != 'S')) if mem.empty: # Set the whole memory to 0xff _mem.set_raw('\xff' * (_mem.size() // 8)) else: _mem.set_raw('\x00' * (_mem.size() // 8)) _mem.freq = int(mem.freq / 10) _mem.offset = int(mem.offset / 10) # Store the alpha tag if self.NAME_LENGTH == 5: # original models with 5-character name length temp_name = mem.name.ljust(self.NAME_LENGTH)[:self.NAME_LENGTH] # prefix the 5-character name with 0x00 to fit structure _mem.name = temp_name.rjust(6, chr(00)) else: # new VOX models with 6-character name length _mem.name = mem.name.ljust(self.NAME_LENGTH)[:self.NAME_LENGTH] # Set duplex bitfields _mem.tx_off = 0 if mem.duplex == '+': _mem.duplex = DUPLEX_POSSPLIT elif mem.duplex == '-': _mem.duplex = DUPLEX_NEGSPLIT elif mem.duplex == '': _mem.duplex = DUPLEX_NOSPLIT elif mem.duplex == 'split': # TODO: this is an unverified punt! _mem.duplex = DUPLEX_ODDSPLIT elif mem.duplex == 'off': # handle tx off _mem.tx_off = 1 else: LOG.error('%s: set_mem: unhandled duplex: %s' % (mem.name, mem.duplex)) # Set the channel width - remember we promote 20 kHz channels to FM # on import # , so don't handle them here if mem.mode == 'FM': _mem.channel_width = CHANNEL_WIDTH_25kHz elif mem.mode == 'NFM': _mem.channel_width = CHANNEL_WIDTH_12d5kHz else: LOG.error('%s: set_mem: unhandled mode: %s' % ( mem.name, mem.mode)) # set the power level if mem.power == POWER_LEVELS[0]: _mem.txpower = TXPOWER_LOW elif mem.power == POWER_LEVELS[1]: _mem.txpower = TXPOWER_MED elif mem.power == POWER_LEVELS[2]: _mem.txpower = TXPOWER_HIGH else: LOG.error('%s: set_mem: unhandled power level: %s' % (mem.name, mem.power)) # TODO set the CTCSS values # TODO support custom ctcss tones here # Default - tones off, carrier sql _mem.ctcss_encode_en = 0 _mem.ctcss_decode_en = 0 _mem.tone_squelch_en = 0 _mem.ctcss_enc_tone = 0x00 _mem.ctcss_dec_tone = 0x00 _mem.customctcss = 0x00 _mem.dtcs_encode_en = 0 _mem.dtcs_encode_code_highbit = 0 _mem.dtcs_encode_code = 0 _mem.dtcs_encode_invert = 0 _mem.dtcs_decode_en = 0 _mem.dtcs_decode_code_highbit = 0 _mem.dtcs_decode_code = 0 _mem.dtcs_decode_invert = 0 dtcs_pol_str_to_bit = {'N': 0, 'R': 1} _mem.dtcs_encode_invert = dtcs_pol_str_to_bit[mem.dtcs_polarity[0]] _mem.dtcs_decode_invert = dtcs_pol_str_to_bit[mem.dtcs_polarity[1]] if mem.tmode == 'Tone': _mem.ctcss_encode_en = 1 _mem.ctcss_enc_tone = ctcss_code_val_to_bits(mem.rtone) elif mem.tmode == 'TSQL': _mem.ctcss_encode_en = 1 _mem.ctcss_enc_tone = ctcss_code_val_to_bits(mem.ctone) _mem.ctcss_decode_en = 1 _mem.tone_squelch_en = 1 _mem.ctcss_dec_tone = ctcss_code_val_to_bits(mem.ctone) elif mem.tmode == 'DTCS': _mem.dtcs_encode_en = 1 _mem.dtcs_encode_code, _mem.dtcs_encode_code_highbit = \ dtcs_code_val_to_bits(mem.dtcs) _mem.dtcs_decode_en = 1 _mem.dtcs_decode_code, _mem.dtcs_decode_code_highbit = \ dtcs_code_val_to_bits(mem.dtcs) _mem.tone_squelch_en = 1 elif mem.tmode == 'Cross': txmode, rxmode = mem.cross_mode.split('->') if txmode == 'Tone': _mem.ctcss_encode_en = 1 _mem.ctcss_enc_tone = ctcss_code_val_to_bits(mem.rtone) elif txmode == '': pass elif txmode == 'DTCS': _mem.dtcs_encode_en = 1 _mem.dtcs_encode_code, _mem.dtcs_encode_code_highbit = \ dtcs_code_val_to_bits(mem.dtcs) else: LOG.error('%s: unhandled cross TX mode: %s' % ( mem.name, mem.cross_mode)) if rxmode == 'Tone': _mem.ctcss_decode_en = 1 _mem.tone_squelch_en = 1 _mem.ctcss_dec_tone = ctcss_code_val_to_bits(mem.ctone) elif rxmode == '': pass elif rxmode == 'DTCS': _mem.dtcs_decode_en = 1 _mem.dtcs_decode_code, _mem.dtcs_decode_code_highbit = \ dtcs_code_val_to_bits(mem.rx_dtcs) _mem.tone_squelch_en = 1 else: LOG.error('%s: unhandled cross RX mode: %s' % ( mem.name, mem.cross_mode)) elif mem.tmode: LOG.error('%s: Unhandled tmode/cross %s/%s.' % (mem.name, mem.tmode, mem.cross_mode)) LOG.debug('%s: tmode=%s, cross=%s, rtone=%f, ctone=%f' % ( mem.name, mem.tmode, mem.cross_mode, mem.rtone, mem.ctone)) LOG.debug('%s: CENC=%d, CDEC=%d, t(enc)=%02x, t(dec)=%02x' % ( mem.name, _mem.ctcss_encode_en, _mem.ctcss_decode_en, ctcss_code_val_to_bits(mem.rtone), ctcss_code_val_to_bits(mem.ctone))) # set unknown defaults, based on reading memory set by vendor tool _mem.unknown1 = 0x00 _mem.unknown6 = 0x00 _mem.unknown7 = 0x00 _mem.unknown8 = 0x00 _mem.unknown9 = 0x00 # Extra settings (eg. Busy channel lockout) for setting in mem.extra: setattr(_mem, setting.get_name(), setting.value) def get_settings(self): """Translate the MEM_FORMAT structs into setstuf in the UI""" _settings = self._memobj.settings _radio_settings = self._memobj.radio_settings _password = self._memobj.password _pfkeys = self._memobj.pfkeys _dtmf = self._memobj.dtmf # Function Setup function = RadioSettingGroup("function", "Function Setup") group = RadioSettings(function) # MODE SET # Channel Locked rs = RadioSettingValueBoolean(_settings.channelLocked) rset = RadioSetting("settings.channelLocked", "Channel locked", rs) function.append(rset) # Menu 3 - Display Mode options = ["Frequency", "Channel", "Name"] rs = RadioSettingValueList( options, current_index=_settings.displayMode) rset = RadioSetting("settings.displayMode", "Display Mode", rs) function.append(rset) # VFO/MR A options = ["MR", "VFO"] rs = RadioSettingValueList( options, current_index=_radio_settings.vfomrA) rset = RadioSetting("radio_settings.vfomrA", "VFO/MR mode A", rs) function.append(rset) # MR Channel A options = ["%s" % x for x in range(1, 201)] rs = RadioSettingValueList( options, current_index=_radio_settings.mrChanA) rset = RadioSetting("radio_settings.mrChanA", "MR channel A", rs) function.append(rset) # VFO/MR B options = ["MR", "VFO"] rs = RadioSettingValueList( options, current_index=_radio_settings.vfomrB) rset = RadioSetting("radio_settings.vfomrB", "VFO/MR mode B", rs) function.append(rset) # MR Channel B options = ["%s" % x for x in range(1, 201)] rs = RadioSettingValueList( options, current_index=_radio_settings.mrChanB) rset = RadioSetting("radio_settings.mrChanB", "MR channel B", rs) function.append(rset) # DISPLAY SET # Starting Display name = "" for i in range(7): # 0 - 7 name += chr(int(self._memobj.starting_display.line[i])) name = name.upper().rstrip() # remove trailing spaces rs = RadioSettingValueString(0, 7, name) rs.set_charset(chirp_common.CHARSET_ALPHANUMERIC) rset = RadioSetting("starting_display.line", "Starting display", rs) function.append(rset) # Menu 11 - Backlight Brightness options = ["%s" % x for x in range(1, 4)] rs = RadioSettingValueList( options, current_index=_settings.backlightBr - 1) rset = RadioSetting("settings.backlightBr", "Backlight brightness", rs) function.append(rset) # Menu 15 - Screen Direction options = ["Positive", "Inverted"] rs = RadioSettingValueList(options, current_index=_settings.screenDir) rset = RadioSetting("settings.screenDir", "Screen direction", rs) function.append(rset) # Hand Mic Key Brightness options = ["%s" % x for x in range(1, 32)] rs = RadioSettingValueList( options, current_index=_settings.micKeyBrite - 1) rset = RadioSetting("settings.micKeyBrite", "Hand mic key brightness", rs) function.append(rset) # VOL SET # Menu 1 - Beep Volume options = ["OFF"] + ["%s" % x for x in range(1, 6)] rs = RadioSettingValueList(options, current_index=_settings.beepVolume) rset = RadioSetting("settings.beepVolume", "Beep volume", rs) function.append(rset) # Menu 5 - Volume level Setup options = ["%s" % x for x in range(1, 37)] rs = RadioSettingValueList( options, current_index=_settings.speakerVol - 1) rset = RadioSetting("settings.speakerVol", "Speaker volume", rs) function.append(rset) # Menu 16 - Speaker Switch options = ["Host on | Hand mic off", "Host on | Hand mic on", "Host off | Hand mic on"] rs = RadioSettingValueList( options, current_index=_settings.speakerSwitch) rset = RadioSetting("settings.speakerSwitch", "Speaker switch", rs) function.append(rset) # STE SET # STE Frequency options = ["Off", "55.2 Hz", "259.2 Hz"] rs = RadioSettingValueList( options, current_index=_settings.steFrequency) rset = RadioSetting("settings.steFrequency", "STE frequency", rs) function.append(rset) # STE Type options = ["Off", "Silent", "120 degrees", "180 degrees", "240 degrees"] rs = RadioSettingValueList(options, current_index=_settings.steType) rset = RadioSetting("settings.steType", "STE type", rs) function.append(rset) # ON/OFF SET # The Power-on Password feature is not available on models with VOX if not self.HAS_VOX: # Power-on Password rs = RadioSettingValueBoolean(_settings.powerOnPasswd) rset = RadioSetting("settings.powerOnPasswd", "Power-on Password", rs) function.append(rset) # Password def _char_to_str(chrx): """ Remove ff pads from char array """ # chrx is char array str1 = "" for sx in chrx: if int(sx) > 31 and int(sx) < 127: str1 += chr(int(sx)) return str1 def _pswd_vfy(setting, obj, atrb): """ Verify password is 1-6 chars, numbers 1-5 """ str1 = str(setting.value).strip() # initial # valid chars str2 = ''.join([c for c in str1 if c in '0123456789']) if str1 != str2: # Two lines due to python 73 char limit sx = "Bad characters in Password" raise errors.RadioError(sx) str2 = str1.ljust(6, chr(00)) # pad to 6 with 00's setattr(obj, atrb, str2) return sx = _char_to_str(_password.digits).strip() rx = RadioSettingValueString(0, 6, sx) sx = "Password (numerals 0-9)" rset = RadioSetting("password.digits", sx, rx) rset.set_apply_callback(_pswd_vfy, _password, "digits") function.append(rset) # Menu 9 - Auto Power On rs = RadioSettingValueBoolean(_settings.autoPowerOn) rset = RadioSetting("settings.autoPowerOn", "Auto power on", rs) function.append(rset) # Menu 13 - Auto Power Off options = ["Off", "30 minutes", "60 minutes", "120 minutes"] rs = RadioSettingValueList( options, current_index=_settings.autoPowerOff) rset = RadioSetting("settings.autoPowerOff", "Auto power off", rs) function.append(rset) # Power On Reset Enable rs = RadioSettingValueBoolean(_settings.powerOnReset) rset = RadioSetting("settings.powerOnReset", "Power on reset", rs) function.append(rset) # FUNCTION SET # Menu 4 - Squelch Level A options = ["OFF"] + ["%s" % x for x in range(1, 10)] rs = RadioSettingValueList( options, current_index=_settings.squelchLevelA) rset = RadioSetting("settings.squelchLevelA", "Squelch level A", rs) function.append(rset) # Squelch Level B options = ["OFF"] + ["%s" % x for x in range(1, 10)] rs = RadioSettingValueList( options, current_index=_settings.squelchLevelB) rset = RadioSetting("settings.squelchLevelB", "Squelch level B", rs) function.append(rset) # Menu 7 - Scan Type options = ["Time operated (TO)", "Carrier operated (CO)", "Search (SE)"] rs = RadioSettingValueList(options, current_index=_settings.scanType) rset = RadioSetting("settings.scanType", "Scan mode", rs) function.append(rset) # Menu 8 - Scan Recovery Time options = ["%s seconds" % x for x in range(5, 20, 5)] rs = RadioSettingValueList( options, current_index=_settings.scanRecoveryT) rset = RadioSetting("settings.scanRecoveryT", "Scan recovery time", rs) function.append(rset) # Main options = ["A", "B"] rs = RadioSettingValueList(options, current_index=_settings.main) rset = RadioSetting("settings.main", "Main", rs) function.append(rset) # Menu 10 - Dual Watch (RX Way Select) rs = RadioSettingValueBoolean(_settings.dualWatch) rset = RadioSetting("settings.dualWatch", "Dual watch", rs) function.append(rset) # Menu 12 - Time Out Timer options = ["OFF"] + ["%s minutes" % x for x in range(1, 31)] rs = RadioSettingValueList( options, current_index=_settings.timeOutTimer) rset = RadioSetting("settings.timeOutTimer", "Time out timer", rs) function.append(rset) # TBST Frequency options = ["1000 Hz", "1450 Hz", "1750 Hz", "2100 Hz"] rs = RadioSettingValueList( options, current_index=_settings.tbstFrequency) rset = RadioSetting("settings.tbstFrequency", "TBST frequency", rs) function.append(rset) # Save Channel Parameter rs = RadioSettingValueBoolean(_settings.saveChParameter) rset = RadioSetting("settings.saveChParameter", "Save channel parameter", rs) function.append(rset) # MON Key Function options = ["Squelch off momentary", "Squelch off"] rs = RadioSettingValueList( options, current_index=_settings.monKeyFunction) rset = RadioSetting("settings.monKeyFunction", "MON key function", rs) function.append(rset) # Frequency Step options = ["2.5 kHz", "5 kHz", "6.25 kHz", "10 kHz", "12.5 kHz", "20 kHz", "25 kHz", "30 kHz", "50 kHz"] rs = RadioSettingValueList( options, current_index=_settings.frequencyStep) rset = RadioSetting("settings.frequencyStep", "Frequency step", rs) function.append(rset) # Knob Mode options = ["Volume", "Channel"] rs = RadioSettingValueList(options, current_index=_settings.knobMode) rset = RadioSetting("settings.knobMode", "Knob mode", rs) function.append(rset) # TRF Enable rs = RadioSettingValueBoolean(_settings.trfEnable) rset = RadioSetting("settings.trfEnable", "TRF enable", rs) function.append(rset) if self.HAS_VOX: # VOX On/Off rs = RadioSettingValueBoolean(_settings.voxOnOff) rset = RadioSetting("settings.voxOnOff", "VOX", rs) function.append(rset) # VOX Delay options = ["0.5 S", "1.0 S", "1.5 S", "2.0 S", "2.5 S", "3.0 S", "3.5 S", "4.0 S", "4.5 S"] rs = RadioSettingValueList( options, current_index=_settings.voxDelay) rset = RadioSetting("settings.voxDelay", "VOX delay", rs) function.append(rset) # VOX Level options = ["%s" % x for x in range(1, 10)] rs = RadioSettingValueList( options, current_index=_settings.voxLevel) rset = RadioSetting("settings.voxLevel", "VOX Level", rs) function.append(rset) # Key Assignment pfkeys = RadioSettingGroup("pfkeys", "Key Assignment") group.append(pfkeys) options = ["A/B", "V/M", "SQL", "VOL", "POW", "CDT", "REV", "SCN", "CAL", "TALK", "BND", "SFT", "MON", "DIR", "TRF", "RDW", "NULL"] if self.HAS_VOX: options.insert(16, "VOX") # Key Mode 1 # P1 rs = RadioSettingValueList( options, current_index=_pfkeys.keyMode1P1 - 1) rset = RadioSetting("pfkeys.keyMode1P1", "Key mode 1 P1", rs) pfkeys.append(rset) # P2 rs = RadioSettingValueList( options, current_index=_pfkeys.keyMode1P2 - 1) rset = RadioSetting("pfkeys.keyMode1P2", "Key mode 1 P2", rs) pfkeys.append(rset) # P3 rs = RadioSettingValueList( options, current_index=_pfkeys.keyMode1P3 - 1) rset = RadioSetting("pfkeys.keyMode1P3", "Key mode 1 P3", rs) pfkeys.append(rset) # P4 rs = RadioSettingValueList( options, current_index=_pfkeys.keyMode1P4 - 1) rset = RadioSetting("pfkeys.keyMode1P4", "Key mode 1 P4", rs) pfkeys.append(rset) # P5 rs = RadioSettingValueList( options, current_index=_pfkeys.keyMode1P5 - 1) rset = RadioSetting("pfkeys.keyMode1P5", "Key mode 1 P5", rs) pfkeys.append(rset) # P6 rs = RadioSettingValueList( options, current_index=_pfkeys.keyMode1P6 - 1) rset = RadioSetting("pfkeys.keyMode1P6", "Key mode 1 P6", rs) pfkeys.append(rset) # Key Mode 2 # P1 rs = RadioSettingValueList( options, current_index=_pfkeys.keyMode2P1 - 1) rset = RadioSetting("pfkeys.keyMode2P1", "Key mode 2 P1", rs) pfkeys.append(rset) # P2 rs = RadioSettingValueList( options, current_index=_pfkeys.keyMode2P2 - 1) rset = RadioSetting("pfkeys.keyMode2P2", "Key mode 2 P2", rs) pfkeys.append(rset) # P3 rs = RadioSettingValueList( options, current_index=_pfkeys.keyMode2P3 - 1) rset = RadioSetting("pfkeys.keyMode2P3", "Key mode 2 P3", rs) pfkeys.append(rset) # P4 rs = RadioSettingValueList( options, current_index=_pfkeys.keyMode2P4 - 1) rset = RadioSetting("pfkeys.keyMode2P4", "Key mode 2 P4", rs) pfkeys.append(rset) # P5 rs = RadioSettingValueList( options, current_index=_pfkeys.keyMode2P5 - 1) rset = RadioSetting("pfkeys.keyMode2P5", "Key mode 2 P5", rs) pfkeys.append(rset) # P6 rs = RadioSettingValueList( options, current_index=_pfkeys.keyMode2P6 - 1) rset = RadioSetting("pfkeys.keyMode2P6", "Key mode 2 P6", rs) pfkeys.append(rset) options = ["V/M", "SQL", "VOL", "POW", "CDT", "REV", "SCN", "CAL", "TALK", "BND", "SFT", "MON", "DIR", "TRF", "RDW"] if self.HAS_VOX: options.insert(15, "VOX") # PA rs = RadioSettingValueList(options, current_index=_settings.keyPA - 2) rset = RadioSetting("settings.keyPA", "Key PA", rs) pfkeys.append(rset) # PB rs = RadioSettingValueList(options, current_index=_settings.keyPB - 2) rset = RadioSetting("settings.keyPB", "Key PB", rs) pfkeys.append(rset) # PC rs = RadioSettingValueList(options, current_index=_settings.keyPC - 2) rset = RadioSetting("settings.keyPC", "Key PC", rs) pfkeys.append(rset) # PD rs = RadioSettingValueList(options, current_index=_settings.keyPD - 2) rset = RadioSetting("settings.keyPD", "Key PD", rs) pfkeys.append(rset) # DTMF dtmf = RadioSettingGroup("dtmf", "DTMF") group.append(dtmf) # DTMF Transmitting Time options = ["50 milliseconds", "100 milliseconds", "200 milliseconds", "300 milliseconds", "500 milliseconds"] rs = RadioSettingValueList(options, current_index=_settings.dtmfTxTime) rset = RadioSetting("settings.dtmfTxTime", "DTMF transmitting time", rs) dtmf.append(rset) # DTMF Self ID # DTMF Interval Character IC_CHOICES = ["A", "B", "C", "D", "*", "#"] IC_VALUES = [0x0A, 0x0B, 0x0C, 0x0D, 0x0E, 0x0F] def apply_ic_listvalue(setting, obj): LOG.debug("Setting value: " + str(setting.value) + " from list") val = str(setting.value) index = IC_CHOICES.index(val) val = IC_VALUES[index] obj.set_value(val) if _dtmf.intervalChar in IC_VALUES: idx = IC_VALUES.index(_dtmf.intervalChar) else: idx = IC_VALUES.index(0x0E) rs = RadioSetting("dtmf.intervalChar", "DTMF interval character", RadioSettingValueList(IC_CHOICES, current_index=idx)) rs.set_apply_callback(apply_ic_listvalue, _dtmf.intervalChar) dtmf.append(rs) # Group Code GC_CHOICES = ["Off", "A", "B", "C", "D", "*", "#"] GC_VALUES = [0xFF, 0x0A, 0x0B, 0x0C, 0x0D, 0x0E, 0x0F] def apply_gc_listvalue(setting, obj): LOG.debug("Setting value: " + str(setting.value) + " from list") val = str(setting.value) index = GC_CHOICES.index(val) val = GC_VALUES[index] obj.set_value(val) if _dtmf.groupCode in GC_VALUES: idx = GC_VALUES.index(_dtmf.groupCode) else: idx = GC_VALUES.index(0x0A) rs = RadioSetting("dtmf.groupCode", "DTMF interval character", RadioSettingValueList(GC_CHOICES, current_index=idx)) rs.set_apply_callback(apply_gc_listvalue, _dtmf.groupCode) dtmf.append(rs) # Decoding Response options = ["None", "Beep tone", "Beep tone & respond"] rs = RadioSettingValueList( options, current_index=_dtmf.decodingResponse) rset = RadioSetting("dtmf.decodingResponse", "Decoding response", rs) dtmf.append(rset) # First Digit Time options = ["%s" % x for x in range(0, 2510, 10)] rs = RadioSettingValueList(options, current_index=_dtmf.firstDigitTime) rset = RadioSetting("dtmf.firstDigitTime", "First Digit Time(ms)", rs) dtmf.append(rset) # First Digit Time options = ["%s" % x for x in range(10, 2510, 10)] rs = RadioSettingValueList(options, current_index=_dtmf.pretime - 1) rset = RadioSetting("dtmf.pretime", "Pretime(ms)", rs) dtmf.append(rset) # Auto Reset Time options = ["%s" % x for x in range(0, 25100, 100)] rs = RadioSettingValueList(options, current_index=_dtmf.autoResetTime) rset = RadioSetting("dtmf.autoResetTime", "Auto Reset time(ms)", rs) dtmf.append(rset) # Time-Lapse After Encode options = ["%s" % x for x in range(10, 2510, 10)] rs = RadioSettingValueList(options, current_index=_dtmf.timeLapse - 1) rset = RadioSetting("dtmf.timeLapse", "Time-lapse after encode(ms)", rs) dtmf.append(rset) # PTT ID Pause Time options = ["Off", "-", "-", "-", "-"] + [ "%s" % x for x in range(5, 76)] rs = RadioSettingValueList(options, current_index=_dtmf.pauseTime) rset = RadioSetting("dtmf.pauseTime", "PTT ID pause time(s)", rs) dtmf.append(rset) # Side Tone rs = RadioSettingValueBoolean(_dtmf.sideTone) rset = RadioSetting("dtmf.sideTone", "Side tone", rs) dtmf.append(rset) # PTT ID Starting DTMF_CHARS = "0123456789ABCD*# " _codeobj = _dtmf.pttIdStart _code = "".join([DTMF_CHARS[x] for x in _codeobj if int(x) < 0x1F]) val = RadioSettingValueString(0, 16, _code, False) val.set_charset(DTMF_CHARS) rs = RadioSetting("dtmf.pttIdStart", "PTT ID starting", val) def apply_code(setting, obj): code = [] for j in range(0, 16): try: code.append(DTMF_CHARS.index(str(setting.value)[j])) except IndexError: code.append(0xFF) obj.pttIdStart = code rs.set_apply_callback(apply_code, _dtmf) dtmf.append(rs) # PTT ID Ending _codeobj = _dtmf.pttIdEnd _code = "".join([DTMF_CHARS[x] for x in _codeobj if int(x) < 0x1F]) val = RadioSettingValueString(0, 16, _code, False) val.set_charset(DTMF_CHARS) rs = RadioSetting("dtmf.pttIdEnd", "PTT ID ending", val) def apply_code(setting, obj): code = [] for j in range(0, 16): try: code.append(DTMF_CHARS.index(str(setting.value)[j])) except IndexError: code.append(0xFF) obj.pttIdEnd = code rs.set_apply_callback(apply_code, _dtmf) dtmf.append(rs) # Remotely Kill _codeobj = _dtmf.remoteKill _code = "".join([DTMF_CHARS[x] for x in _codeobj if int(x) < 0x1F]) val = RadioSettingValueString(0, 16, _code, False) val.set_charset(DTMF_CHARS) rs = RadioSetting("dtmf.remoteKill", "Remotely kill", val) def apply_code(setting, obj): code = [] for j in range(0, 16): try: code.append(DTMF_CHARS.index(str(setting.value)[j])) except IndexError: code.append(0xFF) obj.remoteKill = code rs.set_apply_callback(apply_code, _dtmf) dtmf.append(rs) # Remotely Stun _codeobj = _dtmf.remoteStun _code = "".join([DTMF_CHARS[x] for x in _codeobj if int(x) < 0x1F]) val = RadioSettingValueString(0, 16, _code, False) val.set_charset(DTMF_CHARS) rs = RadioSetting("dtmf.remoteStun", "Remotely stun", val) def apply_code(setting, obj): code = [] for j in range(0, 16): try: code.append(DTMF_CHARS.index(str(setting.value)[j])) except IndexError: code.append(0xFF) obj.remoteStun = code rs.set_apply_callback(apply_code, _dtmf) dtmf.append(rs) # DTMF Encode # M1 - M16 for i in range(0, 16): _codeobj = self._memobj.pttid[i].code _code = "".join([DTMF_CHARS[x] for x in _codeobj if int(x) < 0x1F]) val = RadioSettingValueString(0, 16, _code, False) val.set_charset(DTMF_CHARS) rs = RadioSetting("pttid/%i.code" % i, "DTMF encode M%i" % (i + 1), val) def apply_code(setting, obj): code = [] for j in range(0, 16): try: code.append(DTMF_CHARS.index(str(setting.value)[j])) except IndexError: code.append(0xFF) obj.code = code rs.set_apply_callback(apply_code, self._memobj.pttid[i]) dtmf.append(rs) return group def set_settings(self, settings): _settings = self._memobj.settings _mem = self._memobj for element in settings: if not isinstance(element, RadioSetting): self.set_settings(element) continue else: try: name = element.get_name() if "." in name: bits = name.split(".") obj = self._memobj for bit in bits[:-1]: if "/" in bit: bit, index = bit.split("/", 1) index = int(index) obj = getattr(obj, bit)[index] else: obj = getattr(obj, bit) setting = bits[-1] else: obj = _settings setting = element.get_name() if element.has_apply_callback(): LOG.debug("Using apply callback") element.run_apply_callback() elif setting == "timeLapse": setattr(obj, setting, int(element.value) + 1) elif setting == "pretime": setattr(obj, setting, int(element.value) + 1) elif setting == "backlightBr": setattr(obj, setting, int(element.value) + 1) elif setting == "micKeyBrite": setattr(obj, setting, int(element.value) + 1) elif setting == "speakerVol": setattr(obj, setting, int(element.value) + 1) elif "keyMode" in setting: setattr(obj, setting, int(element.value) + 1) elif "keyP" in setting: setattr(obj, setting, int(element.value) + 2) elif element.value.get_mutable(): LOG.debug("Setting %s = %s" % (setting, element.value)) setattr(obj, setting, element.value) except Exception: LOG.debug(element.get_name()) raise # Original non-VOX models @directory.register class AnyTone778UV(AnyTone778UVBase): VENDOR = "AnyTone" MODEL = "778UV" # Allowed radio types is a dict keyed by model of a list of version # strings ALLOWED_RADIO_TYPES = {'AT778UV': ['V100', 'V200']} @directory.register class RetevisRT95(AnyTone778UVBase): VENDOR = "Retevis" MODEL = "RT95" # Allowed radio types is a dict keyed by model of a list of version # strings ALLOWED_RADIO_TYPES = {'RT95': ['V100']} @directory.register class CRTMicronUV(AnyTone778UVBase): VENDOR = "CRT" MODEL = "Micron UV" # Allowed radio types is a dict keyed by model of a list of version # strings ALLOWED_RADIO_TYPES = {'MICRON': ['V100']} @directory.register class MidlandDBR2500(AnyTone778UVBase): VENDOR = "Midland" MODEL = "DBR2500" # Allowed radio types is a dict keyed by model of a list of version # strings ALLOWED_RADIO_TYPES = {'DBR2500': ['V100']} @directory.register class YedroYCM04vus(AnyTone778UVBase): VENDOR = "Yedro" MODEL = "YC-M04VUS" # Allowed radio types is a dict keyed by model of a list of version # strings ALLOWED_RADIO_TYPES = {'YCM04UV': ['V100']} class AnyTone778UVvoxBase(AnyTone778UVBase): '''AnyTone 778UV VOX, Retevis RT95 VOX and others''' NAME_LENGTH = 6 HAS_VOX = True # New VOX models @directory.register @directory.detected_by(AnyTone778UV) class AnyTone778UVvox(AnyTone778UVvoxBase): VENDOR = "AnyTone" MODEL = "778UV VOX" # Allowed radio types is a dict keyed by model of a list of version # strings ALLOWED_RADIO_TYPES = {'778UV-P': ['V100']} @directory.register @directory.detected_by(RetevisRT95) class RetevisRT95vox(AnyTone778UVvoxBase): VENDOR = "Retevis" MODEL = "RT95 VOX" # Allowed radio types is a dict keyed by model of a list of version # strings ALLOWED_RADIO_TYPES = {'RT95-P': ['V100']} @directory.register @directory.detected_by(CRTMicronUV) class CRTMicronUVvox(AnyTone778UVvoxBase): VENDOR = "CRT" MODEL = "Micron UV V2" # Allowed radio types is a dict keyed by model of a list of version # strings ALLOWED_RADIO_TYPES = {'MICRONP': ['V100']} chirp-20261030/chirp/drivers/anytone_ht.py0000644000175000017500000007233115172631605017353 0ustar hibbyhibby# Copyright 2015 Jim Unroe # # 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 . import struct import logging from chirp import bitwise from chirp import chirp_common from chirp import checksum from chirp import directory from chirp import errors from chirp import memmap from chirp import util from chirp.settings import RadioSetting, RadioSettingGroup, \ RadioSettingValueInteger, RadioSettingValueList, \ RadioSettingValueBoolean, RadioSettingValueString, \ RadioSettings LOG = logging.getLogger(__name__) mem_format = """ struct memory { bbcd freq[4]; bbcd offset[4]; u8 unknown1:4, tune_step:4; u8 unknown2:2, txdcsextra:1, txinv:1, channel_width:2, unknown3:1, tx_off:1; u8 unknown4:2, rxdcsextra:1, rxinv:1, power:2, duplex:2; u8 unknown5:4, rxtmode:2, txtmode:2; u8 unknown6:2, txtone:6; u8 unknown7:2, rxtone:6; u8 txcode; u8 rxcode; u8 unknown8[3]; char name[6]; u8 squelch:4, unknown9:2, bcl:2; u8 unknownA; u8 unknownB:7, sqlmode:1; u8 unknownC[4]; }; #seekto 0x0010; struct { u8 unknown1; u8 unknown2:5, bands1:3; char model[7]; u8 unknown3:5, bands2:3; u8 unknown4[6]; u8 unknown5[16]; char date[9]; u8 unknown6[7]; u8 unknown7[16]; u8 unknown8[16]; char dealer[16]; char stockdate[9]; u8 unknown9[7]; char selldate[9]; u8 unknownA[7]; char seller[16]; } oem_info; #seekto 0x0100; u8 used_flags[50]; #seekto 0x0120; u8 skip_flags[50]; #seekto 0x0220; struct { u8 unknown220:6, display:2; u8 unknown221:7, upvfomr:1; u8 unknown222:7, dnvfomr:1; u8 unknown223:7, fmvfomr:1; u8 upmrch; u8 dnmrch; u8 unknown226:1, fmmrch:7; u8 unknown227; u8 unknown228:7, fastscano:1; // obltr-8r only u8 unknown229:6, pause:2; u8 unknown22A:5, stop:3; u8 unknown22B:6, backlight:2; u8 unknown22C:6, color:2; u8 unknown22D:6, vdisplay:2; u8 unknown22E; u8 unknown22F:5, pf1key:3; u8 beep:1, alarmoff:1, main:1, radio:1, unknown230:1, allband:1, elimtail:1, monikey:1; u8 fastscan:1, // termn-8r only keylock:1, unknown231:2, lwenable:1, swenable:1, fmenable:1, amenable:1; u8 unknown232:3, tot:5; u8 unknown233:7, amvfomr:1; u8 unknown234:3, apo:5; u8 unknown235:5, pf2key:3; // keylock for obltr-8r u8 unknown236; u8 unknown237:4, save:4; u8 unknown238:5, tbst:3; u8 unknown239:4, voxlevel:4; u8 unknown23A:3, voxdelay:5; u8 unknown23B:5, tail:3; u8 unknown23C; u8 unknown23D:1, ammrch:7; u8 unknown23E:5, vvolume:3; u8 unknown23F:5, fmam:3; u8 unknown240:4, upmrbank:4; u8 unknown241:7, upwork:1; u8 unknown242:7, uplink:1; u8 unknown243:4, dnmrbank:4; u8 unknown244:7, dnwork:1; u8 unknown245:7, downlink:1; u8 unknown246:7, banklink1:1; u8 unknown247:7, banklink2:1; u8 unknown248:7, banklink3:1; u8 unknown249:7, banklink4:1; u8 unknown24A:7, banklink5:1; u8 unknown24B:7, banklink6:1; u8 unknown24C:7, banklink7:1; u8 unknown24D:7, banklink8:1; u8 unknown24E:7, banklink9:1; u8 unknown24F:7, banklink0:1; u8 unknown250:6, noaa:2; u8 unknown251:5, noaach:3; u8 unknown252:6, part95:2; u8 unknown253:3, gmrs:5; u8 unknown254:5, murs:3; u8 unknown255:5, amsql:3; } settings; #seekto 0x0246; struct { u8 unused:7, bank:1; } banklink[10]; #seekto 0x03E0; struct { char line1[6]; char line2[6]; } welcome_msg; #seekto 0x2000; struct memory memory[200]; """ def _echo_write(radio, data): try: radio.pipe.write(data) except Exception as e: LOG.error("Error writing to radio: %s" % e) raise errors.RadioError("Unable to write to radio") def _read(radio, length): try: data = radio.pipe.read(length) except Exception as e: LOG.error("Error reading from radio: %s" % e) raise errors.RadioError("Unable to read from radio") if len(data) != length: LOG.error("Short read from radio (%i, expected %i)" % (len(data), length)) LOG.debug(util.hexprint(data)) raise errors.RadioError("Short read from radio") return data valid_model = [b'TERMN8R', b'OBLTR8R'] def _ident(radio): radio.pipe.timeout = 1 _echo_write(radio, b"PROGRAM") response = radio.pipe.read(3) if not response: raise errors.RadioNoResponse() if response != b"QX\x06": LOG.debug("Response was:\n%s" % util.hexprint(response)) raise errors.RadioError("Unexpected response from radio") _echo_write(radio, b"\x02") response = radio.pipe.read(16) LOG.debug(util.hexprint(response)) if radio._file_ident not in response: LOG.debug("Response was:\n%s" % util.hexprint(response)) raise errors.RadioError("Unsupported model") def _finish(radio): endframe = b"\x45\x4E\x44" _echo_write(radio, endframe) result = radio.pipe.read(1) if result != b"\x06": LOG.debug("Got:\n%s" % util.hexprint(result)) raise errors.RadioError("Radio did not finish cleanly") def _send(radio, cmd, addr, length, data=None): frame = struct.pack(">cHb", cmd, addr, length) if data: frame += data frame += bytes([checksum.checksum_8bit(frame[1:])]) frame += b"\x06" _echo_write(radio, frame) LOG.debug("Sent:\n%s" % util.hexprint(frame)) if data: result = radio.pipe.read(1) if result != b"\x06": LOG.debug("Ack was: %s" % repr(result)) raise errors.RadioError( "Radio did not accept block at %04x" % addr) return result = _read(radio, length + 6) LOG.debug("Got:\n%s" % util.hexprint(result)) header = result[:4] data = result[4:-2] ack = result[-1:] if ack != b"\x06": LOG.debug("Ack was: %s" % repr(ack)) raise errors.RadioError("Radio NAK'd block at %04x" % addr) _cmd, _addr, _length = struct.unpack(">cHb", header) if _addr != addr or _length != _length: LOG.debug("Expected/Received:") LOG.debug(" Length: %02x/%02x" % (length, _length)) LOG.debug(" Addr: %04x/%04x" % (addr, _addr)) raise errors.RadioError("Radio send unexpected block") cs = checksum.checksum_8bit(result[1:-2]) if cs != result[-2]: LOG.debug("Calculated: %02x" % cs) LOG.debug("Actual: %02x" % result[-2]) raise errors.RadioError("Block at 0x%04x failed checksum" % addr) return data def _download(radio): _ident(radio) memobj = None data = b"" for start, end in radio._ranges: for addr in range(start, end, 0x10): block = _send(radio, b'R', addr, 0x10) data += block status = chirp_common.Status() status.cur = len(data) status.max = end status.msg = "Cloning from radio" radio.status_fn(status) _finish(radio) return memmap.MemoryMapBytes(data) def _upload(radio): _ident(radio) for start, end in radio._ranges: for addr in range(start, end, 0x10): if addr < 0x0100: continue block = radio._mmap[addr:addr + 0x10] _send(radio, b'W', addr, len(block), block) status = chirp_common.Status() status.cur = addr status.max = end status.msg = "Cloning to radio" radio.status_fn(status) _finish(radio) APO = ['Off', '30 Minutes', '1 Hour', '2 Hours'] BACKLIGHT = ['Off', 'On', 'Auto'] BCLO = ['Off', 'Repeater', 'Busy'] CHARSET = chirp_common.CHARSET_ASCII COLOR = ['Blue', 'Orange', 'Purple'] DISPLAY = ['Frequency', 'N/A', 'Name'] DUPLEXES = ['', '', '-', '+', 'split', 'off'] GMRS = ['GMRS %s' % x for x in range(1, 8)] + \ ['GMRS %s' % x for x in range(15, 23)] + \ ['GMRS Repeater %s' % x for x in range(15, 23)] MAIN = ['Up', 'Down'] MODES = ['FM', 'NFM'] MONI = ['Squelch Off Momentarily', 'Squelch Off'] MRBANK = ['Bank %s' % x for x in range(1, 10)] + ['Bank 0'] MURS = ['MURS %s' % x for x in range(1, 6)] NOAA = ['Weather Off', 'Weather On', 'Weather Alerts'] NOAACH = ['WX %s' % x for x in range(1, 8)] PART95 = ['Normal(Part 90)', 'GMRS(Part 95A)', 'MURS(Part 95J)'] PAUSE = ['%s Seconds (TO)' % x for x in range(5, 20, 5)] + ['2 Seconds (CO)'] PFKEYT = ['Off', 'VOLT', 'CALL', 'FHSS', 'SUB PTT', 'ALARM', 'MONI'] PFKEYO = ['Off', 'VOLT', 'CALL', 'SUB PTT', 'ALARM', 'MONI'] POWER_LEVELS = [chirp_common.PowerLevel("High", watts=5), chirp_common.PowerLevel("Mid", watts=2), chirp_common.PowerLevel("Low", watts=1)] SAVE = ['Off', '1:2', '1:3', '1:5', '1:8', 'Auto'] SQUELCH = ['%s' % x for x in range(0, 10)] STOP = ['%s Seconds' % x for x in range(0, 4)] + ['Manual'] TAIL = ['Off', '120 Degree', '180 Degree', '240 Degree'] TBST = ['Off', '1750 Hz', '2100 Hz', '1000 Hz', '1450 Hz'] TMODES = ['', 'Tone', 'DTCS', ''] TONES = [62.5] + list(chirp_common.TONES) TOT = ['Off'] + ['%s Seconds' % x for x in range(10, 280, 10)] VDISPLAY = ['Frequency/Channel', 'Battery Voltage', 'Off'] VFOMR = ["VFO", "MR"] VOXLEVEL = ['Off'] + ['%s' % x for x in range(1, 11)] VOXDELAY = ['%.1f Seconds' % (0.1 * x) for x in range(5, 31)] WORKMODE = ["Channel", "Bank"] @directory.register class AnyToneTERMN8RRadio(chirp_common.CloneModeRadio, chirp_common.ExperimentalRadio): """AnyTone TERMN-8R""" VENDOR = "AnyTone" MODEL = "TERMN-8R" BAUD_RATE = 9600 _file_ident = b"TERMN8R" # May try to mirror the OEM behavior later _ranges = [ (0x0000, 0x8000), ] @classmethod def get_prompts(cls): rp = chirp_common.RadioPrompts() rp.experimental = ("The Anytone driver is currently experimental. " "There are no known issues with it, but you should " "proceed with caution.") return rp def get_features(self): rf = chirp_common.RadioFeatures() rf.has_settings = True rf.has_bank = False rf.has_cross = True rf.valid_tuning_steps = [2.5, 5, 6.25, 10, 12.5, 20, 25, 30, 50] rf.has_tuning_step = False rf.has_rx_dtcs = True rf.valid_skips = ["", "S"] rf.valid_modes = MODES rf.valid_tmodes = ['', 'Tone', 'TSQL', 'DTCS', 'Cross'] rf.valid_cross_modes = ["Tone->Tone", "Tone->DTCS", "DTCS->Tone", "->Tone", "->DTCS", "DTCS->", "DTCS->DTCS"] rf.valid_tones = TONES rf.valid_dtcs_codes = chirp_common.ALL_DTCS_CODES rf.valid_bands = [(136000000, 174000000), (400000000, 520000000)] rf.valid_characters = CHARSET rf.valid_name_length = 6 rf.valid_power_levels = POWER_LEVELS rf.valid_duplexes = DUPLEXES rf.can_odd_split = True rf.memory_bounds = (0, 199) return rf def sync_in(self): self._mmap = _download(self) self.process_mmap() def sync_out(self): _upload(self) def process_mmap(self): self._memobj = bitwise.parse(mem_format, self._mmap) def _get_dcs_index(self, _mem, which): base = getattr(_mem, '%scode' % which) extra = getattr(_mem, '%sdcsextra' % which) return (int(extra) << 8) | int(base) def _set_dcs_index(self, _mem, which, index): base = getattr(_mem, '%scode' % which) extra = getattr(_mem, '%sdcsextra' % which) base.set_value(index & 0xFF) extra.set_value(index >> 8) def get_memory(self, number): bitpos = (1 << (number % 8)) bytepos = (number / 8) _mem = self._memobj.memory[number] _skp = self._memobj.skip_flags[bytepos] _usd = self._memobj.used_flags[bytepos] mem = chirp_common.Memory() mem.number = number if _usd & bitpos: mem.empty = True return mem mem.freq = int(_mem.freq) * 100 # compensate for 6.25 and 12.5 kHz tuning steps, add 500 Hz if needed lastdigit = int(_mem.freq) % 10 if (lastdigit == 2 or lastdigit == 7): mem.freq += 50 mem.offset = int(_mem.offset) * 100 mem.name = self.filter_name(str(_mem.name).rstrip()) mem.duplex = DUPLEXES[_mem.duplex] mem.mode = MODES[_mem.channel_width] if bool(_mem.tx_off): mem.duplex = "off" mem.offset = 0 rxtone = txtone = None rxmode = TMODES[_mem.rxtmode] txmode = TMODES[_mem.txtmode] if txmode == "Tone": txtone = TONES[_mem.txtone] elif txmode == "DTCS": txtone = chirp_common.ALL_DTCS_CODES[self._get_dcs_index(_mem, 'tx')] if rxmode == "Tone": rxtone = TONES[_mem.rxtone] elif rxmode == "DTCS": rxtone = chirp_common.ALL_DTCS_CODES[self._get_dcs_index(_mem, 'rx')] rxpol = _mem.rxinv and "R" or "N" txpol = _mem.txinv and "R" or "N" chirp_common.split_tone_decode(mem, (txmode, txtone, txpol), (rxmode, rxtone, rxpol)) if _skp & bitpos: mem.skip = "S" mem.power = POWER_LEVELS[_mem.power] mem.extra = RadioSettingGroup("Extra", "extra") rs = RadioSetting("bcl", "Busy Channel Lockout", RadioSettingValueList(BCLO, current_index=_mem.bcl)) mem.extra.append(rs) rs = RadioSetting("squelch", "Squelch", RadioSettingValueList(SQUELCH, current_index=_mem.squelch)) mem.extra.append(rs) 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.skip_flags[bytepos] _usd = self._memobj.used_flags[bytepos] if mem.empty: _usd |= bitpos _skp |= bitpos _mem.set_raw("\xFF" * 32) return _usd &= ~bitpos if _mem.get_raw(asbytes=False) == ("\xFF" * 32): LOG.debug("Initializing empty memory") _mem.set_raw("\x00" * 32) _mem.squelch = 3 _mem.freq = mem.freq / 100 if mem.duplex == "off": _mem.duplex = DUPLEXES.index("") _mem.offset = 0 _mem.tx_off = True elif mem.duplex == "split": diff = mem.offset - mem.freq _mem.duplex = DUPLEXES.index("-") if diff < 0 \ else DUPLEXES.index("+") _mem.offset = abs(diff) / 100 else: _mem.offset = mem.offset / 100 _mem.duplex = DUPLEXES.index(mem.duplex) _mem.name = mem.name.ljust(6) try: _mem.channel_width = MODES.index(mem.mode) except ValueError: _mem.channel_width = 0 ((txmode, txtone, txpol), (rxmode, rxtone, rxpol)) = chirp_common.split_tone_encode(mem) _mem.txtmode = TMODES.index(txmode) _mem.rxtmode = TMODES.index(rxmode) if txmode == "Tone": _mem.txtone = TONES.index(txtone) elif txmode == "DTCS": self._set_dcs_index(_mem, 'tx', chirp_common.ALL_DTCS_CODES.index(txtone)) if rxmode == "Tone": _mem.sqlmode = 1 _mem.rxtone = TONES.index(rxtone) elif rxmode == "DTCS": _mem.sqlmode = 1 self._set_dcs_index(_mem, 'rx', chirp_common.ALL_DTCS_CODES.index(rxtone)) else: _mem.sqlmode = 0 _mem.txinv = txpol == "R" _mem.rxinv = rxpol == "R" if mem.skip: _skp |= bitpos else: _skp &= ~bitpos if mem.power: _mem.power = POWER_LEVELS.index(mem.power) else: _mem.power = 0 for setting in mem.extra: setattr(_mem, setting.get_name(), setting.value) def get_settings(self): _msg = self._memobj.welcome_msg _oem = self._memobj.oem_info _settings = self._memobj.settings cfg_grp = RadioSettingGroup("cfg_grp", "Function Setup") oem_grp = RadioSettingGroup("oem_grp", "OEM Info") group = RadioSettings(cfg_grp, oem_grp) def _filter(name): filtered = "" for char in str(name): if char in chirp_common.CHARSET_ASCII: filtered += char else: filtered += " " return filtered # # Function Setup # rs = RadioSetting("welcome_msg.line1", "Welcome Message 1", RadioSettingValueString( 0, 6, _filter(_msg.line1))) cfg_grp.append(rs) rs = RadioSetting("welcome_msg.line2", "Welcome Message 2", RadioSettingValueString( 0, 6, _filter(_msg.line2))) cfg_grp.append(rs) rs = RadioSetting("display", "Display Mode", RadioSettingValueList( DISPLAY, current_index=_settings.display)) cfg_grp.append(rs) rs = RadioSetting("upvfomr", "Up VFO/MR", RadioSettingValueList( VFOMR, current_index=_settings.upvfomr)) cfg_grp.append(rs) rs = RadioSetting("dnvfomr", "Down VFO/MR", RadioSettingValueList( VFOMR, current_index=_settings.dnvfomr)) cfg_grp.append(rs) rs = RadioSetting( "upwork", "Up Work Mode", RadioSettingValueList( WORKMODE, current_index=_settings.upwork)) cfg_grp.append(rs) rs = RadioSetting("upmrbank", "Up MR Bank", RadioSettingValueList( MRBANK, current_index=_settings.upmrbank)) cfg_grp.append(rs) rs = RadioSetting("upmrch", "Up MR Channel", RadioSettingValueInteger(0, 200, _settings.upmrch)) cfg_grp.append(rs) rs = RadioSetting( "dnwork", "Down Work Mode", RadioSettingValueList( WORKMODE, current_index=_settings.dnwork)) cfg_grp.append(rs) rs = RadioSetting("dnmrbank", "Down MR Bank", RadioSettingValueList( MRBANK, current_index=_settings.dnmrbank)) cfg_grp.append(rs) rs = RadioSetting("dnmrch", "Down MR Channel", RadioSettingValueInteger(0, 200, _settings.dnmrch)) cfg_grp.append(rs) rs = RadioSetting("main", "Main", RadioSettingValueList(MAIN, current_index=_settings.main)) cfg_grp.append(rs) rs = RadioSetting("pause", "Scan Pause Time", RadioSettingValueList(PAUSE, current_index=_settings.pause)) cfg_grp.append(rs) rs = RadioSetting("stop", "Function Keys Stop Time", RadioSettingValueList(STOP, current_index=_settings.stop)) cfg_grp.append(rs) rs = RadioSetting( "backlight", "Backlight", RadioSettingValueList( BACKLIGHT, current_index=_settings.backlight)) cfg_grp.append(rs) rs = RadioSetting("color", "Backlight Color", RadioSettingValueList(COLOR, current_index=_settings.color)) cfg_grp.append(rs) rs = RadioSetting( "vdisplay", "Vice-Machine Display", RadioSettingValueList( VDISPLAY, current_index=_settings.vdisplay)) cfg_grp.append(rs) rs = RadioSetting( "voxlevel", "Vox Level", RadioSettingValueList( VOXLEVEL, current_index=_settings.voxlevel)) cfg_grp.append(rs) rs = RadioSetting( "voxdelay", "Vox Delay", RadioSettingValueList( VOXDELAY, current_index=_settings.voxdelay)) cfg_grp.append(rs) rs = RadioSetting("tot", "Time Out Timer", RadioSettingValueList(TOT, current_index=_settings.tot)) cfg_grp.append(rs) rs = RadioSetting("tbst", "Tone Burst", RadioSettingValueList(TBST, current_index=_settings.tbst)) cfg_grp.append(rs) rs = RadioSetting("monikey", "MONI Key Function", RadioSettingValueList( MONI, current_index=_settings.monikey)) cfg_grp.append(rs) if self.MODEL == "TERMN-8R": rs = RadioSetting( "pf1key", "PF1 Key Function", RadioSettingValueList( PFKEYT, current_index=_settings.pf1key)) cfg_grp.append(rs) rs = RadioSetting( "pf2key", "PF2 Key Function", RadioSettingValueList( PFKEYT, current_index=_settings.pf2key)) cfg_grp.append(rs) if self.MODEL == "OBLTR-8R": rs = RadioSetting( "pf1key", "PF1 Key Function", RadioSettingValueList( PFKEYO, current_index=_settings.pf1key)) cfg_grp.append(rs) rs = RadioSetting("fmam", "PF2 Key Function", RadioSettingValueList( PFKEYO, current_index=_settings.fmam)) cfg_grp.append(rs) rs = RadioSetting("apo", "Automatic Power Off", RadioSettingValueList(APO, current_index=_settings.apo)) cfg_grp.append(rs) rs = RadioSetting("save", "Power Save", RadioSettingValueList(SAVE, current_index=_settings.save)) cfg_grp.append(rs) rs = RadioSetting("tail", "Tail Eliminator Type", RadioSettingValueList(TAIL, current_index=_settings.tail)) cfg_grp.append(rs) rs = RadioSetting("fmvfomr", "FM VFO/MR", RadioSettingValueList( VFOMR, current_index=_settings.fmvfomr)) cfg_grp.append(rs) rs = RadioSetting("fmmrch", "FM MR Channel", RadioSettingValueInteger(0, 100, _settings.fmmrch)) cfg_grp.append(rs) rs = RadioSetting("noaa", "NOAA", RadioSettingValueList(NOAA, current_index=_settings.noaa)) cfg_grp.append(rs) rs = RadioSetting("noaach", "NOAA Channel", RadioSettingValueList( NOAACH, current_index=_settings.noaach)) cfg_grp.append(rs) rs = RadioSetting("part95", "PART95", RadioSettingValueList( PART95, current_index=_settings.part95)) cfg_grp.append(rs) rs = RadioSetting("gmrs", "GMRS", RadioSettingValueList(GMRS, current_index=_settings.gmrs)) cfg_grp.append(rs) rs = RadioSetting("murs", "MURS", RadioSettingValueList(MURS, current_index=_settings.murs)) cfg_grp.append(rs) for i in range(0, 9): val = self._memobj.banklink[i].bank rs = RadioSetting("banklink/%i.bank" % i, "Bank Link %i" % (i + 1), RadioSettingValueBoolean(val)) cfg_grp.append(rs) val = self._memobj.banklink[9].bank rs = RadioSetting("banklink/9.bank", "Bank Link 0", RadioSettingValueBoolean(val)) cfg_grp.append(rs) rs = RadioSetting("allband", "All Band", RadioSettingValueBoolean(_settings.allband)) cfg_grp.append(rs) rs = RadioSetting("alarmoff", "Alarm Function Off", RadioSettingValueBoolean(_settings.alarmoff)) cfg_grp.append(rs) rs = RadioSetting("beep", "Beep", RadioSettingValueBoolean(_settings.beep)) cfg_grp.append(rs) rs = RadioSetting("radio", "Radio", RadioSettingValueBoolean(_settings.radio)) cfg_grp.append(rs) if self.MODEL == "TERMN-8R": rs = RadioSetting("keylock", "Keylock", RadioSettingValueBoolean(_settings.keylock)) cfg_grp.append(rs) rs = RadioSetting("fastscan", "Fast Scan", RadioSettingValueBoolean(_settings.fastscan)) cfg_grp.append(rs) if self.MODEL == "OBLTR-8R": # "pf2key" is used for OBLTR-8R "keylock" rs = RadioSetting("pf2key", "Keylock", RadioSettingValueBoolean(_settings.pf2key)) cfg_grp.append(rs) rs = RadioSetting("fastscano", "Fast Scan", RadioSettingValueBoolean(_settings.fastscano)) cfg_grp.append(rs) rs = RadioSetting("uplink", "Up Bank Link Select", RadioSettingValueBoolean(_settings.uplink)) cfg_grp.append(rs) rs = RadioSetting("downlink", "Down Bank Link Select", RadioSettingValueBoolean(_settings.downlink)) cfg_grp.append(rs) # # OEM info # val = RadioSettingValueString(0, 7, _filter(_oem.model)) val.set_mutable(False) rs = RadioSetting("oem_info.model", "Model", val) oem_grp.append(rs) val = RadioSettingValueString(0, 9, _filter(_oem.date)) val.set_mutable(False) rs = RadioSetting("oem_info.date", "Date", val) oem_grp.append(rs) val = RadioSettingValueString(0, 16, _filter(_oem.dealer)) val.set_mutable(False) rs = RadioSetting("oem_info.dealer", "Dealer Code", val) oem_grp.append(rs) val = RadioSettingValueString(0, 9, _filter(_oem.stockdate)) val.set_mutable(False) rs = RadioSetting("oem_info.stockdate", "Stock Date", val) oem_grp.append(rs) val = RadioSettingValueString(0, 9, _filter(_oem.selldate)) val.set_mutable(False) rs = RadioSetting("oem_info.selldate", "Sell Date", val) oem_grp.append(rs) 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 else: try: name = element.get_name() if "." in name: bits = name.split(".") obj = self._memobj for bit in bits[:-1]: if "/" in bit: bit, index = bit.split("/", 1) index = int(index) obj = getattr(obj, bit)[index] else: obj = getattr(obj, bit) setting = bits[-1] else: obj = _settings setting = element.get_name() if element.has_apply_callback(): LOG.debug("Using apply callback") element.run_apply_callback() else: LOG.debug("Setting %s = %s" % (setting, element.value)) setattr(obj, setting, element.value) except Exception: LOG.debug(element.get_name()) raise @classmethod def match_model(cls, filedata, filename): return cls._file_ident in filedata[0x10:0x20] @directory.register class AnyToneOBLTR8RRadio(AnyToneTERMN8RRadio): """AnyTone OBLTR-8R""" VENDOR = "AnyTone" MODEL = "OBLTR-8R" _file_ident = b"OBLTR8R" chirp-20261030/chirp/drivers/anytone_iii.py0000644000175000017500000022426715257672455017535 0ustar hibbyhibby# Copyright 2020 Brad Schuler K0BAS # # 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 . import struct import logging import string from chirp import bitwise from chirp import chirp_common from chirp import checksum from chirp import directory from chirp import errors from chirp import memmap from chirp import util from chirp.settings import RadioSetting, RadioSettingGroup, \ RadioSettingValueInteger, RadioSettingValueList, \ RadioSettingValueBoolean, RadioSettingValueString, \ RadioSettingValueFloat, RadioSettings class ATBankModel(chirp_common.BankModel): """AnyTone Banks A-J, Each chan in zero or one bank""" def __init__(self, radio, name='Banks'): super(ATBankModel, self).__init__(radio, name) self._banks = [] self._memBounds = list(range(0, 750)) for i in range(0, 10): self._banks.append( chirp_common.Bank(self, i, string.ascii_uppercase[i])) def get_num_mappings(self): return len(self._banks) def get_mappings(self): return self._banks def add_memory_to_mapping(self, memory, bank): self._radio.set_bank(memory.number, bank.get_index()) def remove_memory_from_mapping(self, memory, mapping): # I would argue that removing a memory from a mapping in which it does # not exist should not throw an error. The end result is the # requested result. The memory is not in the mapping. index = self._radio.get_bank(memory.number) if (index is None or index != mapping.get_index()): raise Exception("Memory %d is not in bank %s" % (memory.number, bank.get_name())) self._radio.clr_bank(memory.number) def get_mapping_memories(self, bank): memories = [] for i in self._memBounds: bank_index = self._radio.get_bank(i) if (bank_index is not None and bank_index == bank.get_index()): memories.append(self._radio.get_memory(i)) return memories def get_memory_mappings(self, memory): bank_index = self._radio.get_bank(memory.number) if bank_index is None: return [] else: return [self._banks[bank_index]] LOG = logging.getLogger(__name__) # TODO: Reverse engineer scramble codes # Examples: 1=x0300 1111=xDA0B 3300=x3323 3400=x4524 # TODO: Reverse engineer derived-value for 2Tone frequencies # Below are some samples: # freq tone derived # 0000 0000 # 288.0 0b40 0823 208.3 # 321.7 0c91 0749 186.5 # 526.8 1494 2397 911.1 # 526.9 1495 2395 910.9 # 712.3 1bd3 1a52 673.8 # 928.1 2441 0286 64.6 # 1527.3 3ba9 0c46 314.2 # 1934.7 4b93 09b1 248.1 # 3116.0 79b8 000c 1.2 mem_format_thruflags = """ struct flag { // Flags about a channel u8 left_only:1, // Channel is 1.25m or AM (meaning left side only) unused:1, // Unused flag scan:2, // chirp_common.SKIP_VALUES group:4; // [A-J, Off] }; #seekto 0x0020; // Read Only struct { u8 unknown1[16]; u8 unknown2[16]; char modellike[14]; u8 unknown3[2]; char serial[16]; u8 zeros16[16]; char date[16]; u8 zeros128[128]; } oem_info; struct flag chan_flags[750]; // Channel flags struct flag limit_flags[10]; // Limit channel flags //u8 effs8[8]; """ mem_format = mem_format_thruflags + """ struct memory { bbcd freq[5]; // rx freq bbcd offset[3]; // tx offset from rx / 100 u8 unknown1:4, tune_step:4; // [2.5k, 5k, 6.25k, 8.33k, 10k, // 12.5k, 15k, 20k, 25k, 30k] u8 rxdcsextra:1, // use with rxcode for index of rx DCS to use txdcsextra:1, // use with txcode for index of tx DCS to use rxdcsinv:1, // inverse DCS rx polarity txdcsinv:1, // inverse DCS tx polarity channel_width:2, // [25 kHz, 20 kHz, 12.5 kHz] rev:1, // reverse the tx and rx frequencies & tones txoff:1; // prevent tx u8 talkaround:1, // Use the rx freq & tone when transmitting compander:1, unknown2:1, is_am:1, // modulation is AM power:2, // [High, Mid1, Mid2, Low] duplex:2; // ['', -, +, off] u8 dtmf_enc_num:4, // zero-based DTMF slot to use for optional // signalling (when optsig = DTMF) rxtmode:2, // ['', Tone, DTCS] txtmode:2; // ['', Tone, DTCS] u8 unknown3:2, txtone:6; // TONES[0..50] & 51=custom u8 unknown4:2, rxtone:6; // TONES[0..50] & 51=custom u8 txcode; // use with txdcsextra for index of tx DCS to use u8 rxcode; // use with rxdcsextra for index of rx DCS to use u8 unknown5:2, pttid:2, // [Off, Begin, End, Begin&End] unknown6:2, bclo:2; // [Off, Repeater, Busy] u8 unknown7:6, band:2; // [2m, 1-1/4m, 350+ MHz, 70cm] u8 unknown8:5, sql_mode:3; // [Carrier, CTCSS/DCS Tones, Opt Sig Only, // Tones & Opt Sig, Tones or Opt Sig] // Implemented in GUI as a combination of rxmode // and opt sig squelch list OPT_SIG_SQL u8 unknown9:6, optsig:2; // [Off, DTMF, 2Tone, 5Tone] u8 unknown10:3, twotone:5; // zero-based 2-tone slot to use for optional // signalling (when optsig = 2Tone) u8 unknown11:1, fivetone:7; // zero-based 5-tone slot to use for optional // signalling (when optsig = 5Tone) u8 unknown12:4, scramble:4; // [Off, 1 ... 9, Define 1, Define 2] char name[7]; ul16 custtone; // custom ctcss tone (%5.1f) * 10 }; struct DTMF12p3 { u8 dtmf[12]; // Each nibble is a DTMF code [0-9A-D*#] u8 num_tones; // Quantity of nibbles/digits in above u8 unknown3[3]; }; struct DTMF7 { u8 dtmf[7]; // Each nibble is a DTMF code [0-9A-D*#] u8 num_tones; // Quantity of nibbles/digits in above }; #seekto 0x0400; // DTMF slots struct DTMF12p3 dtmf_encodings[16]; //#seekto 0x0500; //u8 unknown[64]; #seekto 0x0540; // Hyper channels (12 x 2) struct memory hyper_channels[24]; struct { u8 unknown1:6, display:2; // Display Mode: [freq, chan, name] u8 unknown2:3, sql_left:5; // Left Squelch Level: 1..20 u8 unknown3:3, sql_right:5; // Right Squelch Level: 1..20 u8 unknown4; u8 unknown5:6, scan_tot:2; // Scan Pause Time: [TO, CO, SE] u8 unknown6:7, scan_mode:1; // Scan Mode: [MEM, MSM(Pri Scan)] u8 bg_red; // Background color red intensity // 0-x1f displayed as 1-32d u8 bg_green; // Background color green intensity u8 bg_blue; // Background color blue intensity u8 unknown7:5, tbst_freq:3; // TBST Freq [Off, 1750, 2100, 1000, 1450] Hz u8 unknown8:3, ptt_tot:5; // Talk Timeout Timer: [Off, 1..30] minutes u8 unknown9:6, ptt_lockout:2; // PTT Key Lock: [Off, Right, Left, Both] u8 unknown10:3, apo:5; // Auto Power Off: [Off, 1..18] * 30 minutes u8 unknown11:3, key_bright:5; // Key Light Level: [00-1e] := [1-32] u8 long_key_time; // Long Key Time: # seconds * 100, // so 100 := 1.0 sec, 250 := 2.5 sec u8 unknown12:6, deputy_mute:2; // Mute the Deputy channel (sub band) when // primary is doing this: [Off, Tx, Rx, Tx & Rx] //#seekto 0x0850; u8 disp_chan_lock:1, // Display Channel Lock when display=1(Chan) unknown13:1, keypad_lock:1, // Lock the keypad beep_hi:1, // 0=low 1=high volume no_init:1, // Prohibit initialization: 0=no 1=yes unknown14:3; u8 unknown15:2, clk_shift:1, // CLK Shift: 0=off 1=on ext_spk_on:1, // Enable the external speaker alt_key_mode:1, // Use Alt Keypad Mode: 0=off 1=on beep_on:1, // Enable beep no_tone_elim_tail:1, // Elim squelch tail when no CTCSS/DCS signaling sql_key_mode:1; // SQL Key Function: [Momentary, Toggle] u8 unknown16:5, dtmf_preload_time:3; // DTMF encode preload time: // [100, 300, 500, 800, 1000] ms u8 unknown17:5, dtmf_speed:3; // DTMF transmit time/speed: // [30, 50, 80, 100, 150, 200, 250] ms u8 unknown19:6, tail_elim_type:2; // Tail Eliminator Type: [Off, 120, 180] degrees u8 unknown20:3, key_a_func:5; // Programmable key A function [0-19]: // [Off, DUP, ... Bandwidth, Talk Around] u8 unknown21:3, key_b_func:5; // Programmable key B function u8 unknown22:3, key_c_func:5; // Programmable key C function u8 unknown23:3, key_d_func:5; // Programmable key D function u8 unknown24:7, boot_pw_on:1; // Use boot password (verify password is set!) u8 unknown25:5, rfsql_left:3; // Left RF Squelch Level: // [Off, S-2, S-5, S-9, FULL] u8 unknown26:5, rfsql_right:3; // Right RF Squelch Level u8 scramble1[2]; // Scramble code #1 (unknown encoding) u8 scramble2[2]; // Scramble code #2 (unknown encoding) //#seekto 0x0860 & 0x0880 struct { u8 unknown1; u8 unknown2:6, left_mode:2; // [VFO, Channel, Home] u8 unknown3:6, right_mode:2; // [VFO, Channel, Home] u8 unknown4:6, sub_display:2; // [Freq, DC IN, Off] ul16 left_channel; // Zero-based chan number ul16 right_channel; // Zero-based chan number u8 unknown5; u8 unknown6:6, spkr_mode:2; // Hand/Main [Off/On, On/On, On/Off] u8 unknown7:6, left_vfo_band:2; // [108, 220, 350, 400] MHz u8 unknown8:7, right_vfo_band:1; // [136, 400] MHz u8 unknown9; u8 unknown10:1, vfo_tracked:1, auto_hyper_save:1, auto_rpt_shift:1, auto_am:1, main:1, // [Left, Right] vfo_band_edge:1, unknown11:1; u8 unknown12; u8 unknown13; //#seekto 0x0870 & 0x0890 u8 unknown14:4, left_work_bank:4; // [A - J, Off] u8 unknown15:7, left_bank_mode:1; // [CH, Bank] u8 unknown16:7, left_bank_sw:1; // [Off, On] u8 unknown17:4, right_work_bank:4; // [A - J, Off] u8 unknown18:7, right_bank_mode:1; // [CH, Bank] u8 unknown19:7, right_bank_sw:1; // [Off, On] // This would be better than below: // struct { u8 unknown:7, linked:1; } linkedBanks[10]; // They would need to be in a sub-group of settings in the extras. // A-J (one byte each) 00 or 01 u8 unknown20:7, link_bank_A:1; u8 unknown21:7, link_bank_B:1; u8 unknown22:7, link_bank_C:1; u8 unknown23:7, link_bank_D:1; u8 unknown24:7, link_bank_E:1; u8 unknown25:7, link_bank_F:1; u8 unknown26:7, link_bank_G:1; u8 unknown27:7, link_bank_H:1; u8 unknown28:7, link_bank_I:1; u8 unknown29:7, link_bank_J:1; } hyper_settings[2]; //#seekto 0x08a0; char boot_password[7]; // Boot password u8 unknown30[9]; //#seekto 0x08b0; u8 unknown31[16]; //#seekto 0x08c0; u8 unknown32:4, dtmf_interval_char:4; // DTMF interval character u8 dtmf_group_code; // DTMF Group Code: 0a-0f,ff displayed as A-#,Off u8 unknown33:6, dtmf_dec_resp:2; // DTMF Decoding Response: // [None, Beep, Beep & Respond] u8 unknown34; u8 dtmf_first_dig_time; // DTMF First digit time: 0..250 * 10 ms u8 dtmf_auto_rst_time; // DTMF auto reset time: 0..250 // displayed as 0.0-25.0 in 0.1 increments sec u8 unknown35; u8 dtmf_self_id[3]; // DTMF Self ID: low nibble of each byte is a // DTMF char, exactly 3 chars u8 unknown36; u8 unknown37; u8 unknown38; u8 unknown39:7, dtmf_sidetone_on:1; // DTMF side tone enabled u8 dtmf_enc_delay; // DTMF encode delay: 1..250 * 10 ms u8 dtmf_pttid_delay; // DTMF PTT ID delay: 0,5-75 // shown as [Off,5-75] sec //#seekto 0x08d0; struct DTMF7 dtmf_kill; // DTMF to remotely kill struct DTMF7 dtmf_stun; // DTMF to remotely stun } settings; //#seekto 0x08e0; // 2 Tone settings - for future work //struct { // ul16 calltone1; // Call format 1st tone freq * 10 // ul16 calltone2; // Call format 2nd tone freq * 10 // // (or 00 00 for long tone version of 1st tone) // ul16 calltone1derived; // some derived freq? calculated from 1st tone // ul16 calltone2derived; // some derived freq? calculated from 2nd tone // u8 unknown1:6, // decode_resp:2; // Decode response [None, Beep, Beep & Respond] // u8 tone1_dur; // 1st tone duration * 10 // // shown as range(0.5, 10, 0.1) seconds // u8 tone2_dur; // 2nd tone duration * 10 // // shown as range(0.5, 10, 0.1) seconds // u8 longtone_dur; // Long tone duration * 10 // // shown as range(0.5, 10, 0.1) seconds // u8 gap_time; // Gap time / 100 // // shown as range(0, 2000, 100) msec // u8 auto_rst_time; // Auto reset time * 10 // // shown as range(0, 25, 0.1) sec // u8 unknown:7, // enc_sidetone; // Encode side-tone 0=off 1=on // u8 unknown2:4, // call_format:4; // Call format [A-B, A-C, A-D, B-A, B-C, B-D, // // C-A, C-B, C-D, D-A, D-B, D-C, // // Long A, Long B, Long C] // // Be sure to change callTone1/2 and // // callTone1/2derived when callFormat is changed // //#seekto 0x08f0; // u16 a_tone; // A tone freq * 10 // u16 b_tone; // B tone freq * 10 // u16 c_tone; // C tone freq * 10 // u16 d_tone; // D tone freq * 10 // u8 zeros[8]; //} twotone_settings; //#seekto 0x0900; // Unknown all 0x00 //u8 zeros[512]; //#seekto 0x0b00; // 2 Tone memories (24 slots of 16 bytes) //struct { // ul16 tone1derived; // some derived freq? calculated from 1st tone // ul16 tone2derived; // some derived freq? calculated from 2nd tone // ul16 tone1; // 1st tone freq * 10 // ul16 tone2; // 2nd tone freq * 10 // char name[7]; // Tone name (padded) // u8 zero; //} twotone_slots[24]; //#seekto 0x0c80; // Unknown all 0x24 //u8 twentyfours[128]; //#seekto 0x0d00; // 5 Tone configs (100 slots of 32 bytes) //u8 fivetones[3200]; #seekto 0x1980; // Communication notes struct { char call_id[5]; // Call ID (numeric, max 5 digits (x30-x39) u8 zeros[3]; // 0x00 * 3 } note_call_ids[100]; struct { char name[7]; // Names (ALPHAnumeric max 7 chars) char space; // 0x20 } note_names[100]; //#seekto 0x1fc0; // Unknown all 0x00 //u8 zeros[64]; #seekto 0x2000; struct memory channels[750]; // Normal channels (750 * 32 bytes) //#seekto 0x7dc0; struct memory limit_channels[10]; // Limit channels // (10 * 32 bytes: Five pair of lo/hi) //#seekto 0x7f00; // Unknown //u8 unknown[32]; #seekto 0x7f20; struct DTMF12p3 dtmf_pttid_bot; // DTMF PTT ID Start (BOT) struct DTMF12p3 dtmf_pttid_eot; // DTMF PTT ID End (EOT) //#seekto 0x7f40; //u8 unknown[64] #seekto 0x7f80; // Emergency Information struct { u8 unknown:6, mode:2; // [Alarm, Transpond+Background, // Transpond+Alarm, Both] u8 unknown2:6, eni_type:2; // [None, DTMF, 5Tone] u8 id; // When DTMF: [M1-M16], When 5Tone: 0-99 u8 alarm_time; // [1-255] seconds u8 tx_time; // [0-255] seconds u8 rx_time; // [0-255] seconds u8 unknown3:7, chan_select:1; // [Assigned, Selected] ul16 channel; // [0-749] u8 cycle; // [Continuous, 1-255] } emergency; u8 unknown9[6]; //#seekto 0x7f90; // Unknown //u8 unknown[48] #seekto 0x7fc0; // Welcome message char welcome[7]; //u8 zeros[9]; //#seekto 0x7fd0; //u8 unknown[48]; """ def _is_chan_used(memobj, num): return not memobj.chan_flags[num].unused def _is_limit_chan_used(memobj, num): return not memobj.limit_flags[num].unused def _should_send_addr(memobj, addr): # Skip read-only & out of bounds if addr < 0x0100 or addr >= 0x8000: return False # Skip regions of unknown or uncontrolled by this software if ((addr >= 0x0500 and addr < 0x0540) or (addr >= 0x08e0 and addr < 0x1980) or (addr >= 0x1fc0 and addr < 0x2000) or (addr >= 0x7f00 and addr < 0x7f20) or (addr >= 0x7f40 and addr < 0x7f80) or (addr >= 0x7f90 and addr < 0x7fc0) or addr >= 0x7fd0): return False # Skip unused memories if addr >= 0x2000 and addr < 0x7dc0: return _is_chan_used(memobj, int((addr - 0x2000) / 0x20)) if addr >= 0x7dc0 and addr < 0x7f00: return _is_limit_chan_used(memobj, int((addr - 0x7dc0) / 0x20)) return True def _echo_write(radio, data): try: radio.pipe.write(data) radio.pipe.read(len(data)) except Exception as e: LOG.error("Error writing to radio: %s" % e) raise errors.RadioError("Unable to write to radio") def _read(radio, length): try: data = radio.pipe.read(length) except Exception as e: LOG.error("Error reading from radio: %s" % e) raise errors.RadioError("Unable to read from radio") if len(data) != length: LOG.error("Short read from radio (%i, expected %i)" % (len(data), length)) LOG.debug(util.hexprint(data)) raise errors.RadioError("Short read from radio") return data valid_model = [b'I588UVP'] def _ident(radio): radio.pipe.timeout = 1 _echo_write(radio, b"PROGRAM") response = radio.pipe.read(3) if response != b"QX\x06": LOG.debug("Response was:\n%s" % util.hexprint(response)) raise errors.RadioError("Unsupported model") _echo_write(radio, b"\x02") response = radio.pipe.read(16) LOG.debug(util.hexprint(response)) if response[-1:] != b"\x06": LOG.debug("Response was:\n%s" % util.hexprint(response)) raise errors.RadioError("Missing ack") if response[0:7] not in valid_model: LOG.debug("Response was:\n%s" % util.hexprint(response)) raise errors.RadioError("Unsupported model") # Manufacturer software does this also: _send(radio, b'R', 0x0080, 0x10) def _finish(radio): endframe = b"\x45\x4E\x44" _echo_write(radio, endframe) result = radio.pipe.read(1) if result != b"\x06": LOG.debug("Got:\n%s" % util.hexprint(result)) raise errors.RadioError("Radio did not finish cleanly") def _send(radio, cmd, addr, length, data=None): frame = struct.pack(">cHb", cmd, addr, length) if data: frame += data frame += bytes([checksum.checksum_8bit(frame[1:])]) frame += b"\x06" _echo_write(radio, frame) LOG.debug("Sent:\n%s" % util.hexprint(frame)) if data: result = radio.pipe.read(1) if result != b"\x06": LOG.debug("Ack was: %s" % util.hexprint(result)) raise errors.RadioError( "Radio did not accept block at %04x" % addr) return result = _read(radio, length + 6) LOG.debug("Got:\n%s" % util.hexprint(result)) header = result[0:4] data = result[4:-2] ack = result[-1:] if ack != b"\x06": LOG.debug("Ack was: %s" % repr(ack)) raise errors.RadioError("Radio NAK'd block at %04x" % addr) _cmd, _addr, _length = struct.unpack(">cHb", header) if _addr != addr or _length != _length: LOG.debug("Expected/Received:") LOG.debug(" Length: %02x/%02x" % (length, _length)) LOG.debug(" Addr: %04x/%04x" % (addr, _addr)) raise errors.RadioError("Radio send unexpected block") cs = checksum.checksum_8bit(result[1:-2]) if cs != result[-2]: LOG.debug("Calculated: %02x" % cs) LOG.debug("Actual: %02x" % result[-2]) raise errors.RadioError("Block at 0x%04x failed checksum" % addr) return data def _download(radio): _ident(radio) memobj = None data = b"" for start, end in radio._ranges: for addr in range(start, end, 0x10): if memobj is not None and not _should_send_addr(memobj, addr): block = b"\x00" * 0x10 else: block = _send(radio, b'R', addr, 0x10) data += block status = chirp_common.Status() status.cur = len(data) status.max = end status.msg = "Cloning from radio" radio.status_fn(status) if addr == 0x0400 - 0x10: memobj = bitwise.parse(mem_format_thruflags, data) _finish(radio) return memmap.MemoryMapBytes(data) def _upload(radio): _ident(radio) for start, end in radio._ranges: for addr in range(start, end, 0x10): if not _should_send_addr(radio._memobj, addr): continue block = radio._mmap[addr:addr + 0x10] _send(radio, b'W', addr, len(block), block) status = chirp_common.Status() status.cur = addr status.max = end status.msg = "Cloning to radio" radio.status_fn(status) _finish(radio) def _filter(s, charset): rc = "" for i in range(0, len(s)): c = str(s[i]) rc += (c if c in charset else "") return rc SEVEN_SPACES = " " APO = ['Off'] + ['%.1f hour(s)' % (0.5 * x) for x in range(1, 25)] ONEANDQTRBAND = (220000000, 260000000) LEFT_BANDS = [ (108000000, 180000000), # L-108 ONEANDQTRBAND, # L-220 (350000000, 399995000), # L-350 (400000000, 523000000) # L/R-400 ] RIGHT_BANDS = [ (136000000, 174000000), # R-136 (400000000, 523000000) # L/R-400 ] BCLO = ['Off', 'Repeater', 'Busy'] BEEP_VOL = ['Low', 'High'] CROSS_MODES = list(chirp_common.CROSS_MODES) CROSS_MODES.remove("Tone->") DISPLAY = ['Freq', 'Chan#', 'Name'] DTMF_SLOTS = ['M%d' % x for x in range(1, 17)] DUPLEXES = ['', '-', '+', 'off'] KEY_FUNCS = ['Off', 'DUP', 'PRI On', 'Power', 'Set DCS/CTCSS Code', 'MHz', 'Reverse', 'HM Chan', 'Main L/R Switch', 'VFO/MR', 'Scan', 'Squelch Off', 'Call TBST', 'Call', 'Tone Compande', 'Scramble', 'Add Opt Signal', 'Bandwidth', 'Talk Around'] MODES = ["WFM", "FM", "NFM"] OPT_SIGS = ['Off', 'DTMF', '2Tone', '5Tone'] POWER_LEVELS = [chirp_common.PowerLevel("High", watts=50), chirp_common.PowerLevel("Mid1", watts=25), chirp_common.PowerLevel("Mid2", watts=10), chirp_common.PowerLevel("Low", watts=5)] PTT_IDS = ['Off', 'Begin', 'End', 'Begin & End'] RF_SQUELCHES = ['Off', 'S-2', 'S-5', 'S-9', 'FULL'] SCAN_PAUSES = ['TO', 'CO', 'SE'] SCAN_MODES = ['MEM', 'MSM (Pri Scan)'] SCRAMBLE_CODES = ['Off'] for x in range(1, 10): SCRAMBLE_CODES.append('%d' % x) SCRAMBLE_CODES += ['Define 1', 'Define 2'] T_MODES = ['', 'Tone', 'DTCS', ''] TONE2_SLOTS = ['%d' % x for x in range(0, 24)] TONE5_SLOTS = ['%d' % x for x in range(0, 100)] # Future: chirp_common should present TONES instead of hard-coded list TONES = (62.5,) + chirp_common.TONES TOT = ['Off'] + ['%s Minutes' % x for x in range(1, 31)] TUNING_STEPS = [2.5, 5, 6.25, 8.33, 10, 12.5, 15, 20, 25, 30] PTT_KEY_LOCKS = ["Off", "Right", "Left", "Both"] TBST_FREQS = ["Off", "1750 Hz", "2100 Hz", "1000 Hz", "1450 Hz"] LONG_KEY_TIMES = ["1 second", "1.5 seconds", "2 seconds", "2.5 seconds"] DEPUTY_CHAN_MUTES = ["Off", "Tx", "Rx", "Both"] SQL_BTN_MODES = ["Momentary", "Toggle"] SQL_MODES = ["Carrier", "CTCSS/DCS", "Opt Sig Only", "Tones AND Sig", "Tones OR Sig"] OPT_SIG_SQL = ["Off"] + SQL_MODES[2:] TAIL_ELIM_TYPES = ["Off", "120 degrees", "180 degrees"] LIMIT_NAMES = ["Limit %s %d" % (c, i + 1) for i in range(0, 5) for c in ('Lo', 'Hi')] HYPER_NAMES = ["Hyper-%s %s%s" % (h, x, y) for h in ('1', '2') for x in ('', 'H') for y in ('L-108', 'L-220', 'L-350', 'L-400', 'R-136', 'R-400')] HYPER_MODES = ['VFO', 'Channel', 'Home'] HYPER_SUB_DISPLAYS = ['Freq', 'DC IN', 'Off'] HYPER_SPKR_MODES = ['Mic off', 'Mic on', 'Main off'] HYPER_L_VFOS = ['108 MHz', '220 MHz', '350 MHz', '400 MHz'] HYPER_R_VFOS = ['136 MHz', '400 MHz'] HYPER_MAINS = ['Left', 'Right'] HYPER_BANK_MODES = ['CH', 'Bank'] EMER_MODES = ['Alarm', 'Transpond+Background', 'Transpond+Alarm', 'Both'] EMER_ENI_TYPES = ['None', 'DTMF', '5Tone'] EMER_CHAN_SEL = ['Assigned', 'Selected'] EMER_CYCLES = ['Continuous'] + ["%d seconds" % x for x in range(1, 256)] BANK_CHOICES = ["%s" % chr(x) for x in range(ord("A"), ord("J") + 1)] + ['Off'] DTMF_CHARS = "0123456789ABCD*#" DTMF_INTCHARS = "ABCD*#" DTMF_PRELOADS = ['100 ms', '300 ms', '500 ms', '800 ms', '1000 ms'] DTMF_SPEEDS = ['30 ms', '50 ms', '80 ms', '100 ms', '150 ms', '200 ms', '250 ms'] DTMF_GROUPS = list("ABCDEF") + ['Off'] DTMF_RESPONSES = ['None', 'Beep', 'Beep & Respond'] DTMF_PTTDELAYS = ['Off'] + ["%d seconds" % x for x in range(5, 76)] def _dtmf_encode(dtmf_str, numBytes): dtmf_bytes = [] if len(dtmf_str) != 0: _byte = 0x00 for i in range(1, len(dtmf_str)+1): if i % 2 == 0: _byte |= DTMF_CHARS.index(dtmf_str[i-1]) dtmf_bytes.append(_byte) _byte = 0x00 else: _byte = DTMF_CHARS.index(dtmf_str[i-1]) << 4 if len(dtmf_str) % 2 == 1: dtmf_bytes.append(_byte) while (len(dtmf_bytes) < numBytes): dtmf_bytes.append(0) return dtmf_bytes def _dtmf_decode(dtmf_bytes, num_tones): dtmf_str = "" x = 1 while (x <= num_tones): _byte = dtmf_bytes[(x-1)/2] dtmf_str += DTMF_CHARS[_byte >> 4] x += 1 if (x <= num_tones): dtmf_str += DTMF_CHARS[_byte & 0x0F] x += 1 return dtmf_str class _RadioSettingValueOffsetInt(RadioSettingValueInteger): """An integer setting whose real value is offset from displayed value""" def __init__(self, minval, maxval, current, step=1, offset=0): RadioSettingValueInteger.__init__(self, minval, maxval, current, step) self.offset = offset @directory.register class AnyTone5888UVIIIRadio(chirp_common.CloneModeRadio, chirp_common.ExperimentalRadio): """AnyTone 5888UVIII""" VENDOR = "AnyTone" MODEL = "5888UVIII" BAUD_RATE = 9600 _file_ident = b"588UVP" @classmethod def match_model(cls, filedata, filename): return cls._file_ident in filedata[0x40:0x4D] _ranges = [ (0x0000, 0x8000) ] def get_bank_model(self): return ATBankModel(self) @classmethod def get_prompts(cls): rp = chirp_common.RadioPrompts() rp.experimental = ("The Anytone 5888UVIII driver is currently " "experimental. There are no known issues with it, " "but you should proceed with caution. 2-tone, " "5-tone, and scramble settings are limited. ") return rp def get_features(self): rf = chirp_common.RadioFeatures() rf.has_rx_dtcs = True rf.has_bank = True rf.has_cross = True rf.has_settings = True rf.valid_modes = MODES + ['AM'] rf.valid_tmodes = ['', 'Tone', 'TSQL', 'DTCS', 'Cross'] rf.valid_cross_modes = CROSS_MODES rf.valid_duplexes = DUPLEXES rf.valid_tuning_steps = TUNING_STEPS rf.valid_bands = LEFT_BANDS + RIGHT_BANDS rf.valid_skips = chirp_common.SKIP_VALUES rf.valid_power_levels = POWER_LEVELS rf.valid_characters = chirp_common.CHARSET_UPPER_NUMERIC + "-" rf.valid_name_length = 7 # Future: rf.valid_tones = TONES # Future: update memdetail.py to use rf.valid_dtcs_codes # Future: update memdetail.py to use rf.dtcs_polarity rf.valid_dtcs_codes = chirp_common.ALL_DTCS_CODES rf.memory_bounds = (1, 750) rf.valid_special_chans = LIMIT_NAMES + HYPER_NAMES return rf def sync_in(self): self._mmap = _download(self) self.process_mmap() def sync_out(self): _upload(self) def process_mmap(self): self._memobj = bitwise.parse(mem_format, self._mmap) def _get_memobjs(self, number): if number <= len(self._memobj.channels): _mem = self._memobj.channels[number - 1] _flg = self._memobj.chan_flags[number - 1] elif (number <= len(self._memobj.channels) + len(self._memobj.limit_channels)): _mem = self._memobj.limit_channels[number - len(self._memobj.channels) - 1] _flg = self._memobj.limit_flags[number - len(self._memobj.channels) - 1] else: _mem = self._memobj.hyper_channels[ number - len(self._memobj.channels) - len(self._memobj.limit_channels) - 1] _flg = None return _mem, _flg def _get_dcs_index(self, _mem, which): base = getattr(_mem, '%scode' % which) extra = getattr(_mem, '%sdcsextra' % which) return (int(extra) << 8) | int(base) def _set_dcs_index(self, _mem, which, index): base = getattr(_mem, '%scode' % which) extra = getattr(_mem, '%sdcsextra' % which) base.set_value(index & 0xFF) extra.set_value(index >> 8) def get_raw_memory(self, number): _mem, _flg = self._get_memobjs(number) return repr(_mem) + repr(_flg) def set_bank(self, number, bank_index): _mem, _flg = self._get_memobjs(number) _flg.group = bank_index def clr_bank(self, number): _mem, _flg = self._get_memobjs(number) _flg.group = BANK_CHOICES.index('Off') def get_bank(self, number): _mem, _flg = self._get_memobjs(number) if (_flg.group < 0xa): return _flg.group return None def get_memory(self, number): mem = chirp_common.Memory() is_limit_chan = is_hyper_chan = False if isinstance(number, str): if number in LIMIT_NAMES: is_limit_chan = True mem.number = (len(self._memobj.channels) + LIMIT_NAMES.index(number) + 1) elif number in HYPER_NAMES: is_hyper_chan = True mem.number = (len(self._memobj.channels) + len(LIMIT_NAMES) + HYPER_NAMES.index(number) + 1) mem.extd_number = number elif number > (len(self._memobj.channels) + len(self._memobj.limit_channels)): is_hyper_chan = True mem.number = number mem.extd_number = HYPER_NAMES[number - len(self._memobj.channels) - len(self._memobj.limit_channels) - 1] elif number > len(self._memobj.channels): is_limit_chan = True mem.number = number mem.extd_number = LIMIT_NAMES[number - len(self._memobj.channels) - 1] else: mem.number = number _mem, _flg = self._get_memobjs(mem.number) if not is_hyper_chan and _flg.unused: mem.empty = True _mem.fill_raw(b"\x00") _mem.name = SEVEN_SPACES _flg.scan = 0 mem.freq = int(_mem.freq) mem.offset = int(_mem.offset) * 100 mem.name = (str(_mem.name).rstrip() if not is_limit_chan and not is_hyper_chan else SEVEN_SPACES) mem.duplex = DUPLEXES[_mem.duplex] mem.mode = _mem.is_am and "AM" or MODES[_mem.channel_width] mem.tuning_step = TUNING_STEPS[_mem.tune_step] if _mem.txoff: mem.duplex = DUPLEXES[3] rxtone = txtone = None rxmode = T_MODES[_mem.rxtmode] if _mem.sql_mode == 0 or _mem.sql_mode == 2: rxmode = T_MODES.index('') txmode = T_MODES[_mem.txtmode] if txmode == "Tone": # If custom tone is being used, show as 88.5 and set checkbox in # extras. # Future: Improve chirp_common, so I can add "CUSTOM" into TONES if _mem.txtone == len(TONES): txtone = 88.5 else: txtone = TONES[_mem.txtone] elif txmode == "DTCS": txtone = chirp_common.ALL_DTCS_CODES[self._get_dcs_index(_mem, 'tx')] if rxmode == "Tone": # If custom tone is being used, show as 88.5 and set checkbox in # extras. # Future: Improve chirp_common, so I can add "CUSTOM" into TONES if _mem.rxtone == len(TONES): rxtone = 88.5 else: rxtone = TONES[_mem.rxtone] elif rxmode == "DTCS": rxtone = chirp_common.ALL_DTCS_CODES[self._get_dcs_index(_mem, 'rx')] rxpol = _mem.rxdcsinv and "R" or "N" txpol = _mem.txdcsinv and "R" or "N" chirp_common.split_tone_decode(mem, (txmode, txtone, txpol), (rxmode, rxtone, rxpol)) mem.skip = chirp_common.SKIP_VALUES[_flg.scan if not is_hyper_chan else 0] mem.power = POWER_LEVELS[_mem.power] mem.extra = RadioSettingGroup("Extra", "extra") rs = RadioSetting("rev", "Reverse", RadioSettingValueBoolean(_mem.rev)) mem.extra.append(rs) rs = RadioSetting("compander", "Compander", RadioSettingValueBoolean(_mem.compander)) mem.extra.append(rs) rs = RadioSetting("talkaround", "Talkaround", RadioSettingValueBoolean(_mem.talkaround)) mem.extra.append(rs) rs = RadioSetting("pttid", "PTT ID", RadioSettingValueList(PTT_IDS, current_index=_mem.pttid)) mem.extra.append(rs) rs = RadioSetting("bclo", "Busy Channel Lockout", RadioSettingValueList(BCLO, current_index=_mem.bclo)) mem.extra.append(rs) rs = RadioSetting("optsig", "Optional Signaling", RadioSettingValueList(OPT_SIGS, current_index=_mem.optsig)) mem.extra.append(rs) rs = RadioSetting("OPTSIGSQL", "Squelch w/Opt Signaling", RadioSettingValueList( OPT_SIG_SQL, SQL_MODES[_mem.sql_mode] if SQL_MODES[_mem.sql_mode] in OPT_SIG_SQL else "Off")) mem.extra.append(rs) rs = RadioSetting( "dtmf_enc_num", "DTMF", RadioSettingValueList( DTMF_SLOTS, current_index=_mem.dtmf_enc_num)) mem.extra.append(rs) rs = RadioSetting("twotone", "2-Tone", RadioSettingValueList(TONE2_SLOTS, current_index=_mem.twotone)) mem.extra.append(rs) rs = RadioSetting("fivetone", "5-Tone", RadioSettingValueList(TONE5_SLOTS, current_index=_mem.fivetone)) mem.extra.append(rs) rs = RadioSetting("scramble", "Scrambler Switch", RadioSettingValueList(SCRAMBLE_CODES, current_index=_mem.scramble)) mem.extra.append(rs) # Memory properties dialog is only capable of Boolean and List # RadioSettingValue classes, so cannot configure custtone. # rs = RadioSetting("custtone", "Custom CTCSS", # RadioSettingValueFloat( # min(TONES), # max(TONES), # _mem.custtone and _mem.custtone / 10 or 151.1, # 0.1, # 1)) # mem.extra.append(rs) custtone_str = chirp_common.format_freq(_mem.custtone) rs = RadioSetting("CUSTTONETX", "Use Custom CTCSS (%s) for Tx" % custtone_str, RadioSettingValueBoolean(_mem.txtone == len(TONES))) mem.extra.append(rs) rs = RadioSetting("CUSTTONERX", "Use Custom CTCSS (%s) for Rx" % custtone_str, RadioSettingValueBoolean(_mem.rxtone == len(TONES))) mem.extra.append(rs) return mem def set_memory(self, mem): is_limit_chan = is_hyper_chan = False if mem.number > (len(self._memobj.channels) + len(self._memobj.limit_channels)): is_hyper_chan = True elif mem.number > len(self._memobj.channels): is_limit_chan = True _mem, _flg = self._get_memobjs(mem.number) if mem.empty: if is_hyper_chan: raise errors.InvalidValueError("Hyper memories may not be " "empty.") _mem.set_raw("\x00" * 32) _flg.unused = 1 _flg.group = BANK_CHOICES.index('Off') return if mem.mode == "AM" and (mem.freq < LEFT_BANDS[0][0] or mem.freq > LEFT_BANDS[0][1]): raise errors.InvalidValueError("AM modulation only allowed in 2m " "band.") _mem.set_raw("\x00" * 32) if _flg: # Only non-special memories track this _flg.unused = 0 _mem.freq = mem.freq for band_idx in range(0, len(LEFT_BANDS)): if (mem.freq >= LEFT_BANDS[band_idx][0] and mem.freq <= LEFT_BANDS[band_idx][1]): _mem.band = band_idx break if _flg: # Only non-special memories have this _flg.left_only = 0 if (not is_hyper_chan): if mem.mode == "AM": _flg.left_only = 1 else: _flg.left_only = 1 for low, high in RIGHT_BANDS: if mem.freq >= low and mem.freq <= high: _flg.left_only = 0 break _mem.offset = mem.offset / 100 if is_hyper_chan: _mem.name = SEVEN_SPACES else: _mem.name = mem.name.ljust(7) if mem.duplex == "off": _mem.duplex = DUPLEXES.index("") _mem.txoff = 1 else: _mem.duplex = DUPLEXES.index(mem.duplex) _mem.txoff = 0 _mem.is_am = mem.mode == "AM" _mem.tune_step = TUNING_STEPS.index(mem.tuning_step) try: _mem.channel_width = MODES.index(mem.mode) except ValueError: _mem.channel_width = 0 ((txmode, txtone, txpol), (rxmode, rxtone, rxpol)) = chirp_common.split_tone_encode(mem) _mem.txtmode = T_MODES.index(txmode) _mem.rxtmode = T_MODES.index(rxmode) if rxmode != '': _mem.sql_mode = SQL_MODES.index("CTCSS/DCS") else: _mem.sql_mode = SQL_MODES.index("Carrier") if txmode == "Tone": _mem.txtone = TONES.index(txtone) elif txmode == "DTCS": self._set_dcs_index(_mem, 'tx', chirp_common.ALL_DTCS_CODES.index(txtone)) if rxmode == "Tone": _mem.rxtone = TONES.index(rxtone) elif rxmode == "DTCS": self._set_dcs_index(_mem, 'rx', chirp_common.ALL_DTCS_CODES.index(rxtone)) _mem.txdcsinv = txpol == "R" _mem.rxdcsinv = rxpol == "R" if not is_hyper_chan: _flg.scan = chirp_common.SKIP_VALUES.index(mem.skip) if mem.power: _mem.power = POWER_LEVELS.index(mem.power) else: _mem.power = 0 for setting in mem.extra: if setting.get_name() == "ignore": LOG.debug("*** ignore: %s" % str(setting.value)) # Future: elif setting.get_name() == "custtone": # Future: setattr(_mem, "custtone", setting.value.get_value() * 10) elif setting.get_name() == "OPTSIGSQL": if str(setting.value) != "Off": _mem.sql_mode = SQL_MODES.index(str(setting.value)) elif setting.get_name() == "CUSTTONETX": if setting.value: _mem.txtone = len(TONES) elif setting.get_name() == "CUSTTONERX": if setting.value: _mem.rxtone = len(TONES) else: setattr(_mem, setting.get_name(), setting.value) return mem def get_settings(self): allGroups = RadioSettings() _settings = self._memobj.settings basic = RadioSettingGroup("basic", "Basic") basic.append( RadioSetting( "welcome", "Welcome Message", RadioSettingValueString( 0, 7, _filter(self._memobj.welcome, chirp_common.CHARSET_ASCII)))) basic.append( RadioSetting( "display", "Display", RadioSettingValueList( DISPLAY, current_index=_settings.display))) basic.append( RadioSetting( "disp_chan_lock", "Lock Chan# Disp", RadioSettingValueBoolean(_settings.disp_chan_lock))) basic.append( RadioSetting( "scan_tot", "Scan Pause Time", RadioSettingValueList( SCAN_PAUSES, current_index=_settings.scan_tot))) basic.append( RadioSetting( "scan_mode", "Scan Mode", RadioSettingValueList( SCAN_MODES, current_index=_settings.scan_mode))) basic.append( RadioSetting( "ptt_tot", "Talk Timeout", RadioSettingValueList(TOT, current_index=_settings.ptt_tot))) basic.append( RadioSetting( "apo", "Auto Power Off", RadioSettingValueList(APO, current_index=_settings.apo))) basic.append( RadioSetting( "beep_on", "Beep", RadioSettingValueBoolean(_settings.beep_on))) basic.append( RadioSetting( "beep_hi", "Beep Volume", RadioSettingValueList( BEEP_VOL, current_index=_settings.beep_hi))) basic.append( RadioSetting( "ext_spk_on", "External Speaker", RadioSettingValueBoolean(_settings.ext_spk_on))) basic.append( RadioSetting( "deputy_mute", "Deputy Chan Mute", RadioSettingValueList( DEPUTY_CHAN_MUTES, current_index=_settings.deputy_mute))) basic.append( RadioSetting( "tail_elim_type", "Tail Eliminator", RadioSettingValueList( TAIL_ELIM_TYPES, current_index=_settings.tail_elim_type))) basic.append( RadioSetting( "no_tone_elim_tail", "Eliminate SQL Tail When\nNo Tone Signaling", RadioSettingValueBoolean(_settings.no_tone_elim_tail))) allGroups.append(basic) sqls = RadioSettingGroup("sql", "Squelch Levels") sqls.append( RadioSetting( "sql_left", "Left Squelch", RadioSettingValueInteger(0, 20, _settings.sql_left))) sqls.append( RadioSetting( "sql_right", "Right Squelch", RadioSettingValueInteger(0, 20, _settings.sql_right))) sqls.append( RadioSetting( "rfsql_left", "Left RF Squelch", RadioSettingValueList( RF_SQUELCHES, current_index=_settings.rfsql_left))) sqls.append( RadioSetting( "rfsql_right", "Right RF Squelch", RadioSettingValueList( RF_SQUELCHES, current_index=_settings.rfsql_right))) allGroups.append(sqls) keys = RadioSettingGroup("keys", "Keys / Buttons") keys.append( RadioSetting( "sql_key_mode", "SQL Key Mode", RadioSettingValueList( SQL_BTN_MODES, current_index=_settings.sql_key_mode))) keys.append( RadioSetting( "key_a_func", "PA Key Function", RadioSettingValueList( KEY_FUNCS, current_index=_settings.key_a_func))) keys.append( RadioSetting( "key_b_func", "PB Key Function", RadioSettingValueList( KEY_FUNCS, current_index=_settings.key_b_func))) keys.append( RadioSetting( "key_c_func", "PC Key Function", RadioSettingValueList( KEY_FUNCS, current_index=_settings.key_c_func))) keys.append( RadioSetting( "key_d_func", "PD Key Function", RadioSettingValueList( KEY_FUNCS, current_index=_settings.key_d_func))) keys.append( RadioSetting( "key_bright", "Key Light Level", _RadioSettingValueOffsetInt( 1, 32, _settings.key_bright + 1, 1, -1))) keys.append( RadioSetting( "ptt_lockout", "PTT Key Lock", RadioSettingValueList( PTT_KEY_LOCKS, current_index=_settings.ptt_lockout))) keys.append( RadioSetting( "keypad_lock", "Keypad Lockout", RadioSettingValueBoolean(_settings.keypad_lock))) allGroups.append(keys) bgColor = RadioSettingGroup("bgColor", "Background Color") bgColor.append( RadioSetting( "bg_red", "Red", _RadioSettingValueOffsetInt( 1, 32, _settings.bg_red + 1, 1, -1))) bgColor.append( RadioSetting( "bg_green", "Green", _RadioSettingValueOffsetInt( 1, 32, _settings.bg_green + 1, 1, -1))) bgColor.append( RadioSetting( "bg_blue", "Blue", _RadioSettingValueOffsetInt( 1, 32, _settings.bg_blue + 1, 1, -1))) allGroups.append(bgColor) pwdGroup = RadioSettingGroup("pwdGroup", "Boot Password") pwdGroup.append( RadioSetting( "boot_pw_on", "Enable", RadioSettingValueBoolean(_settings.boot_pw_on))) pwdGroup.append( RadioSetting( "boot_password", "Password (digits)", RadioSettingValueString( 0, 7, _filter(_settings.boot_password, string.digits), False, string.digits))) allGroups.append(pwdGroup) advanced = RadioSettingGroup("advanced", "Advanced") advanced.append( RadioSetting( "tbst_freq", "TBST Freq", RadioSettingValueList( TBST_FREQS, current_index=_settings.tbst_freq))) advanced.append( RadioSetting( "long_key_time", "Long Key Time", RadioSettingValueList( LONG_KEY_TIMES, current_index=int((_settings.long_key_time-100) / 50)))) advanced.append( RadioSetting( "clk_shift", "CLK Shift", RadioSettingValueBoolean(_settings.clk_shift))) advanced.append( RadioSetting( "alt_key_mode", "Keypad Alt Mode", RadioSettingValueBoolean(_settings.alt_key_mode))) advanced.append( RadioSetting( "no_init", "Prohibit Initialization", RadioSettingValueBoolean(_settings.no_init))) allGroups.append(advanced) hyperGroups = [ RadioSettingGroup("hyper1", "Hyper 1"), RadioSettingGroup("hyper2", "Hyper 2") ] for idx in range(0, len(hyperGroups)): _hyperSettings = _settings.hyper_settings[idx] hyperGroup = hyperGroups[idx] hyperGroup.append( RadioSetting( "main", "Main Band", RadioSettingValueList( HYPER_MAINS, current_index=_hyperSettings.main))) hyperGroup.append( RadioSetting( "sub_display", "Sub Display", RadioSettingValueList( HYPER_SUB_DISPLAYS, current_index=_hyperSettings.sub_display))) hyperGroup.append( RadioSetting( "spkr_mode", "Speakers", RadioSettingValueList( HYPER_SPKR_MODES, current_index=_hyperSettings.spkr_mode))) hyperGroup.append( RadioSetting( "vfo_band_edge", "VFO Band Lock", RadioSettingValueBoolean(_hyperSettings.vfo_band_edge))) hyperGroup.append( RadioSetting( "auto_am", "Auto AM", RadioSettingValueBoolean(_hyperSettings.auto_am))) hyperGroup.append( RadioSetting( "auto_rpt_shift", "Auto Repeater Shift", RadioSettingValueBoolean(_hyperSettings.auto_rpt_shift))) hyperGroup.append( RadioSetting( "auto_hyper_save", "Auto Hyper Save", RadioSettingValueBoolean(_hyperSettings.auto_hyper_save))) hyperGroup.append( RadioSetting( "vfo_tracked", "VFO Tracking", RadioSettingValueBoolean(_hyperSettings.vfo_tracked))) hyperGroup.append( RadioSetting( "left_mode", "Left Mode", RadioSettingValueList( HYPER_MODES, current_index=_hyperSettings.left_mode))) hyperGroup.append( RadioSetting( "right_mode", "Right Mode", RadioSettingValueList( HYPER_MODES, current_index=_hyperSettings.right_mode))) hyperGroup.append( RadioSetting( "left_channel", "Left Channel", _RadioSettingValueOffsetInt( 1, 750, _hyperSettings.left_channel + 1, 1, -1))) hyperGroup.append( RadioSetting( "right_channel", "Right Channel", _RadioSettingValueOffsetInt( 1, 750, _hyperSettings.right_channel + 1, 1, -1))) hyperGroup.append( RadioSetting( "left_vfo_band", "Left VFO Band", RadioSettingValueList( HYPER_L_VFOS, current_index=_hyperSettings.left_vfo_band))) hyperGroup.append( RadioSetting( "right_vfo_band", "Right VFO Band", RadioSettingValueList( HYPER_R_VFOS, current_index=_hyperSettings.right_vfo_band))) hyperGroup.append( RadioSetting( "left_work_bank", "Left Work Bank", RadioSettingValueList( BANK_CHOICES, current_index=_hyperSettings.left_work_bank))) hyperGroup.append( RadioSetting( "right_work_bank", "Right Work Bank", RadioSettingValueList( BANK_CHOICES, current_index=_hyperSettings.right_work_bank))) hyperGroup.append( RadioSetting( "left_bank_mode", "Left Bank Mode", RadioSettingValueList( HYPER_BANK_MODES, current_index=_hyperSettings.left_bank_mode))) hyperGroup.append( RadioSetting( "right_bank_mode", "Right Bank Mode", RadioSettingValueList( HYPER_BANK_MODES, current_index=_hyperSettings.right_bank_mode))) hyperGroup.append( RadioSetting( "left_bank_sw", "Left Bank Switch", RadioSettingValueBoolean(_hyperSettings.left_bank_sw))) hyperGroup.append( RadioSetting( "right_bank_sw", "Right Bank Switch", RadioSettingValueBoolean(_hyperSettings.right_bank_sw))) for bank in BANK_CHOICES[0:-1]: hyperGroup.append( RadioSetting( "link_bank_%s" % bank, "Bank %s" % bank, RadioSettingValueBoolean( getattr(_hyperSettings, "link_bank_%s" % bank)))) allGroups.append(hyperGroup) notes = RadioSettingGroup("notes", "Comm Notes") for idx in range(0, 100): notes.append( RadioSetting( "note_call_ids.%d" % idx, "Call ID %d (5 digits)" % idx, RadioSettingValueString( 0, 5, _filter(self._memobj.note_call_ids[idx].call_id, string.digits)[idx*8:idx*8+5], False, string.digits))) notes.append( RadioSetting( "note_names.%d" % idx, "Name %d (7 ALPHAnumeric)" % idx, RadioSettingValueString( 0, 7, _filter(self._memobj.note_names[idx].name, chirp_common.CHARSET_UPPER_NUMERIC) [idx*8:idx*8+7], True, chirp_common.CHARSET_UPPER_NUMERIC))) allGroups.append(notes) _emergency = self._memobj.emergency emer = RadioSettingGroup("emergency", "Emergency Info") emer.append( RadioSetting( "mode", "Alarm Mode", RadioSettingValueList( EMER_MODES, current_index=_emergency.mode))) emer.append( RadioSetting( "eni_type", "ENI Type", RadioSettingValueList( EMER_ENI_TYPES, current_index=_emergency.eni_type))) emer.append( RadioSetting( "emergency.id", "DTMF ID", RadioSettingValueList( DTMF_SLOTS, DTMF_SLOTS[_emergency.id] if EMER_ENI_TYPES[_emergency.eni_type] == 'DTMF' else DTMF_SLOTS[0]))) emer.append( RadioSetting( "5ToneId", "5Tone ID", RadioSettingValueInteger( 0, 99, _emergency.id if EMER_ENI_TYPES[_emergency.eni_type] == '5Tone' else 0))) emer.append( RadioSetting( "alarm_time", "Alarm Time (sec)", RadioSettingValueInteger(1, 255, _emergency.alarm_time))) emer.append( RadioSetting( "tx_time", "TX Duration (sec)", RadioSettingValueInteger(1 if _emergency.tx_time > 0 else 0, 255, _emergency.tx_time))) emer.append( RadioSetting( "rx_time", "RX Duration (sec)", RadioSettingValueInteger(1 if _emergency.rx_time > 0 else 0, 255, _emergency.rx_time))) emer.append( RadioSetting( "chan_select", "ENI Channel", RadioSettingValueList( EMER_CHAN_SEL, current_index=_emergency.chan_select))) emer.append( RadioSetting( "channel", "Assigned Channel", _RadioSettingValueOffsetInt( 1, 750, _emergency.channel + 1, 1, -1))) emer.append( RadioSetting( "cycle", "Cycle", RadioSettingValueList( EMER_CYCLES, current_index=_emergency.cycle))) allGroups.append(emer) dtmfGroup = RadioSettingGroup("dtmf", "DTMF") for idx in range(0, 16): dtmfGroup.append( RadioSetting( "dtmf_encodings.%d" % idx, "Encode Tone %s" % DTMF_SLOTS[idx], RadioSettingValueString( 0, 12, _dtmf_decode( self._memobj.dtmf_encodings[idx].dtmf, self._memobj.dtmf_encodings[idx].num_tones), False, DTMF_CHARS))) dtmfGroup.append( RadioSetting( "dtmf_preload_time", "Encode Preload Time", RadioSettingValueList( DTMF_PRELOADS, current_index=_settings.dtmf_preload_time))) dtmfGroup.append( RadioSetting( "dtmf_speed", "Speed", RadioSettingValueList( DTMF_SPEEDS, current_index=_settings.dtmf_speed))) dtmfGroup.append( RadioSetting( "dtmf_interval_char", "Interval Character", RadioSettingValueList( list(DTMF_INTCHARS), DTMF_CHARS[_settings.dtmf_interval_char]))) dtmfGroup.append( RadioSetting( "dtmf_group_code", "Group Code", RadioSettingValueList( DTMF_GROUPS, DTMF_GROUPS[_settings.dtmf_group_code-0x0a] if _settings.dtmf_group_code < 0xff else DTMF_GROUPS[6]))) dtmfGroup.append( RadioSetting( "dtmf_dec_resp", "Decode Response", RadioSettingValueList( DTMF_RESPONSES, current_index=_settings.dtmf_dec_resp))) dtmfGroup.append( RadioSetting( "dtmf_first_dig_time", "1st Digit Time (ms)", RadioSettingValueInteger( 0, 2500, _settings.dtmf_first_dig_time * 10, 10))) dtmfGroup.append( RadioSetting( "dtmf_auto_rst_time", "Auto Reset Time (0-25 sec by 0.1)", RadioSettingValueFloat( 0, 25, _settings.dtmf_auto_rst_time / 10, 0.1, 1))) dtmfGroup.append( RadioSetting( "dtmf_self_id", "Self ID", RadioSettingValueString( 3, 3, (DTMF_CHARS[_settings.dtmf_self_id[0] & 0x0F] + DTMF_CHARS[_settings.dtmf_self_id[1] & 0x0F] + DTMF_CHARS[_settings.dtmf_self_id[2] & 0x0F]), True, DTMF_CHARS))) dtmfGroup.append( RadioSetting( "dtmf_sidetone_on", "Use Side Tone", RadioSettingValueBoolean(_settings.dtmf_sidetone_on))) dtmfGroup.append( RadioSetting( "dtmf_enc_delay", "Encode Delay (ms)", RadioSettingValueInteger( 10, 2500, _settings.dtmf_enc_delay * 10, 10))) dtmfGroup.append( RadioSetting( "dtmf_pttid_delay", "PTT ID Delay", RadioSettingValueList( DTMF_PTTDELAYS, DTMF_PTTDELAYS[_settings.dtmf_pttid_delay-4] if _settings.dtmf_pttid_delay >= 5 else DTMF_PTTDELAYS[0]))) dtmfGroup.append( RadioSetting( "dtmf_kill", "Remote Kill", RadioSettingValueString( 0, 7, _dtmf_decode(_settings.dtmf_kill.dtmf, _settings.dtmf_kill.num_tones), False, DTMF_CHARS))) dtmfGroup.append( RadioSetting( "dtmf_stun", "Remote Stun", RadioSettingValueString( 0, 7, _dtmf_decode(_settings.dtmf_stun.dtmf, _settings.dtmf_stun.num_tones), False, DTMF_CHARS))) dtmfGroup.append( RadioSetting( "dtmf_pttid_bot", "PTT ID Start (BOT)", RadioSettingValueString( 0, 12, _dtmf_decode(self._memobj.dtmf_pttid_bot.dtmf, self._memobj.dtmf_pttid_bot.num_tones), False, DTMF_CHARS))) dtmfGroup.append( RadioSetting( "dtmf_pttid_eot", "PTT ID Start (EOT)", RadioSettingValueString( 0, 12, _dtmf_decode(self._memobj.dtmf_pttid_eot.dtmf, self._memobj.dtmf_pttid_eot.num_tones), False, DTMF_CHARS))) allGroups.append(dtmfGroup) # TODO: 2-Tone settings # TODO: 5-Tone settings return allGroups def _set_hyper_settings(self, settings, _hyperSettings): for element in settings: name = element.get_name() if not isinstance(element, RadioSetting): self._set_hyper_settings(element, _hyperSettings) continue elif isinstance(element.value, _RadioSettingValueOffsetInt): setattr(_hyperSettings, name, int(element.value) + element.value.offset) else: setattr(_hyperSettings, name, element.value) def _set_pwd_settings(self, settings): _settings = self._memobj.settings use_pw = pwd = None for element in settings: name = element.get_name() if not isinstance(element, RadioSetting): self._set_pwd_settings(element) continue elif name == "boot_pw_on": use_pw = bool(element.value) elif name == "boot_password": pwd = _filter(str(element.value), string.digits) if (pwd is not None and (len(pwd) or use_pw is False)): setattr(_settings, "boot_pw_on", use_pw and 1 or 0) pwd = pwd + "\x00"*(7-len(pwd)) setattr(_settings, "boot_password", pwd) elif (use_pw is not None and use_pw is True): raise errors.InvalidValueError("Password may not be enabled " "without being set.") def _set_notes_settings(self, settings): _noteCalls = self._memobj.note_call_ids _noteNames = self._memobj.note_names for element in settings: name = str(element.get_name()) if not isinstance(element, RadioSetting): self._set_notes_settings(element) continue elif name.startswith("note_call_ids."): idx = int(name[len("note_call_ids."):]) setattr(self._memobj.note_call_ids[idx], "call_id", str(element.value).ljust(5, "\x00")) elif name.startswith("note_names."): idx = int(name[len("note_names."):]) setattr(self._memobj.note_names[idx], "name", str(element.value).ljust(7)) def _set_emer_settings(self, settings): _emergency = self._memobj.emergency dtmf_id = fivetone_id = None for element in settings: name = str(element.get_name()) if not isinstance(element, RadioSetting): self._set_emer_settings(element) continue elif name == "emergency.id": dtmf_id = element.value elif name == "5ToneId": fivetone_id = element.value elif name == "channel": zb_mem_num = int(element.value) + element.value.offset _mem, _flg = self._get_memobjs(zb_mem_num + 1) if _flg.unused: raise errors.InvalidValueError("Assigned emergency ENI " "channel is empty.") else: setattr(_emergency, name, zb_mem_num) elif isinstance(element.value, _RadioSettingValueOffsetInt): setattr(_emergency, name, int(element.value) + element.value.offset) else: setattr(_emergency, name, element.value) if EMER_ENI_TYPES[_emergency.eni_type] == 'DTMF': setattr(_emergency, "id", dtmf_id) elif EMER_ENI_TYPES[_emergency.eni_type] == '5Tone': setattr(_emergency, "id", fivetone_id) def _set_dtmf_settings(self, settings): def _setDtmf(dtmfStruct, newVal): dtmfStruct.num_tones.set_value(len(newVal)) dtmfStruct.dtmf.set_value(_dtmf_encode(newVal, len(dtmfStruct.dtmf))) for element in settings: name = str(element.get_name()) if not isinstance(element, RadioSetting): self._set_dtmf_settings(element) continue if name.startswith("dtmf_encodings."): idx = int(name[len("dtmf_encodings."):]) _setDtmf(self._memobj.dtmf_encodings[idx], str(element.value)) elif name == "dtmf_interval_char": setattr(self._memobj.settings.dtmf_interval_char, name, DTMF_CHARS.index(str(element.value))) elif name == "dtmf_group_code": setattr(self._memobj.settings, name, int(element.value)+0x0a if int(element.value) != 6 else 0xff) elif name == "dtmf_first_dig_time" or name == "dtmf_enc_delay": setattr(self._memobj.settings, name, int(element.value) / 10) elif name == "dtmf_auto_rst_time": setattr(self._memobj.settings, name, int(element.value) * 10) elif name == "dtmf_self_id": newStr = str(element.value) newVal = [] for charIdx in range(0, 3): newVal.append(DTMF_CHARS.index(newStr[charIdx])) setattr(self._memobj.settings, name, newVal) elif name == "dtmf_pttid_delay": setattr(self._memobj.settings, name, DTMF_PTTDELAYS.index(str(element.value))+4 if element.value != DTMF_PTTDELAYS[0] else 0) elif name == "dtmf_kill" or name == "dtmf_stun": _setDtmf(getattr(self._memobj.settings, name), str(element.value)) elif name == "dtmf_pttid_bot" or name == "dtmf_pttid_eot": _setDtmf(getattr(self._memobj, name), str(element.value)) else: setattr(self._memobj.settings, name, element.value) def set_settings(self, settings): _root = self._memobj _settings = self._memobj.settings for element in settings: name = element.get_name() if isinstance(element, RadioSettingGroup): if name == "hyper1": self._set_hyper_settings( element, _settings.hyper_settings[0]) continue elif name == "hyper2": self._set_hyper_settings( element, _settings.hyper_settings[1]) continue elif name == "pwdGroup": self._set_pwd_settings(element) continue elif name == "notes": self._set_notes_settings(element) continue elif name == "emergency": self._set_emer_settings(element) continue elif name == "dtmf": self._set_dtmf_settings(element) continue if not isinstance(element, RadioSetting): self.set_settings(element) continue if name == "ignore": LOG.debug("*** ignore: %s" % str(element.value)) elif name == "welcome": setattr(_root, "welcome", element.value) elif name == "long_key_time": setattr(_settings, name, 100 + 50 * int(element.value)) elif isinstance(element.value, _RadioSettingValueOffsetInt): setattr(_settings, name, (int(element.value) + element.value.offset)) else: setattr(_settings, name, element.value) chirp-20261030/chirp/drivers/ap510.py0000644000175000017500000007753415172631605016043 0ustar hibbyhibby# Copyright 2014 Tom Hayward # # 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 . import struct from time import sleep import logging from chirp import chirp_common, directory, errors, util from chirp.settings import RadioSetting, RadioSettingGroup, \ RadioSettingValueInteger, RadioSettingValueList, \ RadioSettingValueBoolean, RadioSettingValueString, \ InvalidValueError, RadioSettings LOG = logging.getLogger(__name__) def chunks(s, t): """ Yield chunks of s in sizes defined in t.""" i = 0 for n in t: yield s[i:i+n] i += n def encode_base100(v): return (v / 100 << 8) + (v % 100) def decode_base100(u16): return 100 * (u16 >> 8 & 0xff) + (u16 & 0xff) def drain(pipe): """Chew up any data waiting on @pipe""" for x in range(3): buf = pipe.read(4096) if not buf: return raise errors.RadioError('Your pipes are clogged.') def enter_setup(pipe): """Put AP510 in configuration mode.""" for x in range(30): if x % 2: pipe.write("@SETUP") else: pipe.write("\r\nSETUP\r\n") s = pipe.read(64) if s and "\r\nSETUP" in s: return True elif s and "SETUP" in s: return False raise errors.RadioNoResponse() def download(radio): status = chirp_common.Status() drain(radio.pipe) status.msg = " Power on AP510 now, waiting " radio.status_fn(status) new = enter_setup(radio.pipe) status.cur = 1 status.max = 5 status.msg = "Downloading" radio.status_fn(status) if new: radio.pipe.write("\r\nDISP\r\n") else: radio.pipe.write("@DISP") buf = "" for status.cur in range(status.cur, status.max): buf += radio.pipe.read(1024) if buf.endswith("\r\n"): status.cur = status.max radio.status_fn(status) break radio.status_fn(status) else: raise errors.RadioError("Incomplete data received.") LOG.debug("%04i P7H", self._memobj[self.ATTR_MAP['smartbeacon']])) )) def set_smartbeacon(self, d): self._memobj[self.ATTR_MAP['smartbeacon']] = \ struct.pack(">7H", encode_base100(d['lowspeed']), encode_base100(d['slowrate']), encode_base100(d['highspeed']), encode_base100(d['fastrate']), encode_base100(d['turnslope']), encode_base100(d['turnangle']), encode_base100(d['turntime']), ) class AP510Memory20141215(AP510Memory): """Compatible with firmware version 20141215""" ATTR_MAP = dict(list(AP510Memory.ATTR_MAP.items()) + list({ 'tx_volume': '21', # 1-6 'rx_volume': '22', # 1-9 'tx_power': '23', # 1: 1 watt, 0: 0.5 watt 'tx_serial_ui_out': '24', 'path1': '25', 'path2': '26', 'path3': '27', # like "WIDE1 1" else "0" 'multiple': '28', 'auto_on': '29', }.items())) def get_multiple(self): return dict(zip( ( 'mice_message', # conveniently matches APRS spec Mic-E messages 'voltage', # voltage in comment 'temperature', # temperature in comment 'tfx', # not sure what the TF/X toggle does 'squelch', # squelch level 0-8 (0 = disabled) 'blueled', # 0: squelch LED on GPS lock # 1: light LED on GPS lock 'telemetry', # 1: enable 'telemetry_every', # two-digit int 'timeslot_enable', # 1: enable Is this implemented in firmware? 'timeslot', # int 00-59 'dcd', # 0: Blue LED displays squelch, # 1: Blue LED displays software DCD 'tf_card' # 0: KML, 1: WPL ), map(int, chunks(self._memobj[self.ATTR_MAP['multiple']], (1, 1, 1, 1, 1, 1, 1, 2, 1, 2, 1, 1))) )) def set_multiple(self, d): self._memobj[self.ATTR_MAP['multiple']] = "%(mice_message)1d" \ "%(voltage)1d" \ "%(temperature)1d" \ "%(tfx)1d" \ "%(squelch)1d" \ "%(blueled)1d" \ "%(telemetry)1d" \ "%(telemetry_every)02d" \ "%(timeslot_enable)1d" \ "%(timeslot)02d" \ "%(dcd)1d" \ "%(tf_card)1d" % d def get_smartbeacon(self): # raw: 18=0100300060010240028005 # chunks: 18=010 0300 060 010 240 028 005 return dict(zip(( 'lowspeed', 'slowrate', 'highspeed', 'fastrate', 'turnslope', 'turnangle', 'turntime', ), map(int, chunks( self._memobj[self.ATTR_MAP['smartbeacon']], (3, 4, 3, 3, 3, 3, 3))) )) def set_smartbeacon(self, d): self._memobj[self.ATTR_MAP['smartbeacon']] = "%(lowspeed)03d" \ "%(slowrate)04d" \ "%(highspeed)03d" \ "%(fastrate)03d" \ "%(turnslope)03d" \ "%(turnangle)03d" \ "%(turntime)03d" % d PTT_DELAY = ['60 ms', '120 ms', '180 ms', '300 ms', '480 ms', '600 ms', '1000 ms'] OUTPUT = ['KISS', 'Waypoint out', 'UI out'] PATH = [ '(None)', 'WIDE1-1', 'WIDE1-1,WIDE2-1', 'WIDE1-1,WIDE2-2', 'TEMP1-1', 'TEMP1-1,WIDE 2-1', 'WIDE2-1', ] TABLE = r"/\#&0>AW^_acnsuvz" SYMBOL = "".join(map(chr, range(ord("!"), ord("~")+1))) BEACON = ['manual', 'auto', 'auto + manual', 'smart', 'smart + manual'] ALIAS = ['WIDE1-N', 'WIDE2-N', 'WIDE1-N + WIDE2-N'] CHARSET = "".join(map(chr, range(0, 256))) MICE_MESSAGE = ['Emergency', 'Priority', 'Special', 'Committed', 'Returning', 'In Service', 'En Route', 'Off Duty'] TF_CARD = ['WPL', 'KML'] POWER_LEVELS = [chirp_common.PowerLevel("0.5 watt", watts=0.50), chirp_common.PowerLevel("1 watt", watts=1.00)] RP_IMMUTABLE = ["number", "skip", "bank", "extd_number", "name", "rtone", "ctone", "dtcs", "tmode", "dtcs_polarity", "skip", "duplex", "offset", "mode", "tuning_step", "bank_index"] @directory.register class AP510Radio(chirp_common.CloneModeRadio): """Sainsonic AP510""" BAUD_RATE = 9600 VENDOR = "Sainsonic" MODEL = "AP510" NEEDS_COMPAT_SERIAL = True _model = b"AVRT5" mem_upper_limit = 0 def get_features(self): rf = chirp_common.RadioFeatures() rf.has_settings = True rf.valid_modes = ["FM"] rf.valid_tmodes = [""] rf.valid_characters = "" rf.valid_duplexes = [""] rf.valid_name_length = 0 rf.valid_power_levels = POWER_LEVELS 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.valid_bands = [(136000000, 174000000)] rf.memory_bounds = (0, 0) return rf def sync_in(self): try: data = download(self) except errors.RadioError: raise except Exception as e: raise errors.RadioError("Failed to communicate with radio: %s" % e) # _mmap isn't a Chirp MemoryMap, but since AP510Memory implements # get_packed(), the standard Chirp save feature works. if data.startswith('\r\n00=%s 20141215' % self._model): self._mmap = AP510Memory20141215(data) else: self._mmap = AP510Memory(data) def process_mmap(self): self._mmap.process_data() def sync_out(self): try: upload(self) except errors.RadioError: raise except Exception as e: raise errors.RadioError("Failed to communicate with radio: %s" % e) def load_mmap(self, filename): """Load the radio's memory map from @filename""" mapfile = open(filename, "rb") data = mapfile.read() if data.startswith('\r\n00=%s 20141215' % self._model): self._mmap = AP510Memory20141215(data) else: self._mmap = AP510Memory(data) mapfile.close() def get_raw_memory(self, number): return self._mmap.get_packed() def get_memory(self, number): if number != 0: raise errors.InvalidMemoryLocation("AP510 has only one slot") mem = chirp_common.Memory() mem.number = 0 mem.freq = float(self._mmap.freq) * 1000000 mem.name = "TX/RX" mem.mode = "FM" mem.offset = 0.0 try: mem.power = POWER_LEVELS[int(self._mmap.tx_power)] except NotImplementedError: mem.power = POWER_LEVELS[1] mem.immutable = RP_IMMUTABLE return mem def set_memory(self, mem): if mem.number != 0: raise errors.InvalidMemoryLocation("AP510 has only one slot") self._mmap.freq = "%8.4f" % (mem.freq / 1000000.0) if mem.power: try: self._mmap.tx_power = str(POWER_LEVELS.index(mem.power)) except NotImplementedError: pass def get_settings(self): china = RadioSettingGroup("china", "China Map Fix") smartbeacon = RadioSettingGroup("smartbeacon", "Smartbeacon") aprs = RadioSettingGroup("aprs", "APRS", china, smartbeacon) digipeat = RadioSettingGroup("digipeat", "Digipeat") system = RadioSettingGroup("system", "System") settings = RadioSettings(aprs, digipeat, system) aprs.append(RadioSetting("callsign", "Callsign", RadioSettingValueString(0, 6, self._mmap.callsign[:6]))) aprs.append(RadioSetting("ssid", "SSID", RadioSettingValueInteger( 0, 15, ord(self._mmap.callsign[6]) - 0x30))) pttdelay = PTT_DELAY[int(self._mmap.pttdelay) - 1] aprs.append(RadioSetting("pttdelay", "PTT Delay", RadioSettingValueList(PTT_DELAY, pttdelay))) output = OUTPUT[int(self._mmap.output) - 1] aprs.append(RadioSetting("output", "Output", RadioSettingValueList(OUTPUT, output))) aprs.append(RadioSetting("mice", "Mic-E", RadioSettingValueBoolean(strbool(self._mmap.mice)))) try: mice_msg = MICE_MESSAGE[int(self._mmap.multiple['mice_message'])] aprs.append(RadioSetting("mice_message", "Mic-E Message", RadioSettingValueList(MICE_MESSAGE, mice_msg))) except NotImplementedError: pass try: aprs.append(RadioSetting("path1", "Path 1", RadioSettingValueString(0, 6, self._mmap.path1[:6], autopad=True, charset=CHARSET))) ssid1 = ord(self._mmap.path1[6]) - 0x30 aprs.append(RadioSetting("ssid1", "SSID 1", RadioSettingValueInteger(0, 7, ssid1))) aprs.append(RadioSetting("path2", "Path 2", RadioSettingValueString(0, 6, self._mmap.path2[:6], autopad=True, charset=CHARSET))) ssid2 = ord(self._mmap.path2[6]) - 0x30 aprs.append(RadioSetting("ssid2", "SSID 2", RadioSettingValueInteger(0, 7, ssid2))) aprs.append(RadioSetting("path3", "Path 3", RadioSettingValueString(0, 6, self._mmap.path3[:6], autopad=True, charset=CHARSET))) ssid3 = ord(self._mmap.path3[6]) - 0x30 aprs.append(RadioSetting("ssid3", "SSID 3", RadioSettingValueInteger(0, 7, ssid3))) except NotImplementedError: aprs.append(RadioSetting("path", "Path", RadioSettingValueList( PATH, current_index=int(self._mmap.path)))) aprs.append(RadioSetting("table", "Table or Overlay", RadioSettingValueList(TABLE, self._mmap.symbol[1]))) aprs.append(RadioSetting("symbol", "Symbol", RadioSettingValueList(SYMBOL, self._mmap.symbol[0]))) aprs.append(RadioSetting("beacon", "Beacon Mode", RadioSettingValueList( BEACON, current_index=int(self._mmap.beacon) - 1))) aprs.append(RadioSetting("rate", "Beacon Rate (seconds)", RadioSettingValueInteger(10, 9999, self._mmap.rate))) aprs.append(RadioSetting("comment", "Comment", RadioSettingValueString(0, 34, self._mmap.comment, autopad=False, charset=CHARSET))) try: voltage = self._mmap.multiple['voltage'] aprs.append(RadioSetting("voltage", "Voltage in comment", RadioSettingValueBoolean(voltage))) temperature = self._mmap.multiple['temperature'] aprs.append(RadioSetting("temperature", "Temperature in comment", RadioSettingValueBoolean(temperature))) except NotImplementedError: pass aprs.append(RadioSetting("status", "Status", RadioSettingValueString( 0, 34, self._mmap.status, autopad=False, charset=CHARSET))) try: telemetry = self._mmap.multiple['telemetry'] aprs.append(RadioSetting("telemetry", "Telemetry", RadioSettingValueBoolean(telemetry))) telemetry_every = self._mmap.multiple['telemetry_every'] aprs.append(RadioSetting("telemetry_every", "Telemetry every", RadioSettingValueInteger(1, 99, telemetry_every))) timeslot_enable = self._mmap.multiple['telemetry'] aprs.append(RadioSetting("timeslot_enable", "Timeslot", RadioSettingValueBoolean(timeslot_enable))) timeslot = self._mmap.multiple['timeslot'] aprs.append(RadioSetting("timeslot", "Timeslot (second of minute)", RadioSettingValueInteger(0, 59, timeslot))) except NotImplementedError: pass fields = [ ("chinamapfix", "China map fix", RadioSettingValueBoolean(strbool(self._mmap.chinamapfix[0]))), ("chinalat", "Lat", RadioSettingValueInteger( -45, 45, ord(self._mmap.chinamapfix[2]) - 80)), ("chinalon", "Lon", RadioSettingValueInteger( -45, 45, ord(self._mmap.chinamapfix[1]) - 80)), ] for field in fields: china.append(RadioSetting(*field)) try: # Sometimes when digipeat is disabled, alias is 0xFF alias = ALIAS[int(self._mmap.digipeat[1]) - 1] except ValueError: alias = ALIAS[0] fields = [ ("digipeat", "Digipeat", RadioSettingValueBoolean(strbool(self._mmap.digipeat[0]))), ("alias", "Digipeat Alias", RadioSettingValueList( ALIAS, alias)), ("virtualgps", "Static Position", RadioSettingValueBoolean(strbool(self._mmap.virtualgps[0]))), ("btext", "Static Position BTEXT", RadioSettingValueString( 0, 27, self._mmap.virtualgps[1:], autopad=False, charset=CHARSET)), ] for field in fields: digipeat.append(RadioSetting(*field)) sb = self._mmap.smartbeacon fields = [ ("lowspeed", "Low Speed"), ("highspeed", "High Speed"), ("slowrate", "Slow Rate (seconds)"), ("fastrate", "Fast Rate (seconds)"), ("turnslope", "Turn Slope"), ("turnangle", "Turn Angle"), ("turntime", "Turn Time (seconds)"), ] for field in fields: smartbeacon.append(RadioSetting( field[0], field[1], RadioSettingValueInteger(0, 9999, sb[field[0]]) )) system.append(RadioSetting("version", "Version (read-only)", RadioSettingValueString(0, 14, self._mmap.version))) system.append(RadioSetting("autooff", "Auto off (after 90 minutes)", RadioSettingValueBoolean(strbool(self._mmap.autooff)))) system.append(RadioSetting("beep", "Beep on transmit", RadioSettingValueBoolean(strbool(self._mmap.beep)))) system.append(RadioSetting("highaltitude", "High Altitude", RadioSettingValueBoolean( strbool(self._mmap.highaltitude)))) system.append(RadioSetting("busywait", "Wait for clear channel before transmit", RadioSettingValueBoolean( strbool(self._mmap.busywait)))) try: system.append(RadioSetting("tx_volume", "Transmit volume", RadioSettingValueList( map(str, range(1, 7)), self._mmap.tx_volume))) system.append(RadioSetting("rx_volume", "Receive volume", RadioSettingValueList( map(str, range(1, 10)), self._mmap.rx_volume))) system.append(RadioSetting("squelch", "Squelch", RadioSettingValueList( map(str, range(0, 9)), str(self._mmap.multiple['squelch'])))) system.append(RadioSetting("tx_serial_ui_out", "Tx serial UI out", RadioSettingValueBoolean( strbool(self._mmap.tx_serial_ui_out)))) system.append(RadioSetting("auto_on", "Auto-on with 5V input", RadioSettingValueBoolean( strbool(self._mmap.auto_on[0])))) system.append(RadioSetting( "auto_on_delay", "Auto-off delay after 5V lost (seconds)", RadioSettingValueInteger( 0, 9999, int(self._mmap.auto_on[1:])) )) system.append(RadioSetting("tfx", "TF/X", RadioSettingValueBoolean( self._mmap.multiple['tfx']))) system.append(RadioSetting("blueled", "Light blue LED on GPS lock", RadioSettingValueBoolean( self._mmap.multiple['blueled']))) system.append(RadioSetting("dcd", "Blue LED shows software DCD", RadioSettingValueBoolean( self._mmap.multiple['dcd']))) system.append(RadioSetting("tf_card", "TF card format", RadioSettingValueList( TF_CARD, current_index=( int(self._mmap.multiple['tf_card']))))) except NotImplementedError: pass return settings def set_settings(self, settings): for setting in settings: if not isinstance(setting, RadioSetting): self.set_settings(setting) continue if not setting.changed(): continue try: name = setting.get_name() if name == "callsign": self.set_callsign(callsign=setting.value) elif name == "ssid": self.set_callsign(ssid=int(setting.value)) elif name == "pttdelay": self._mmap.pttdelay = PTT_DELAY.index( str(setting.value)) + 1 elif name == "output": self._mmap.output = OUTPUT.index(str(setting.value)) + 1 elif name in ('mice', 'autooff', 'beep', 'highaltitude', 'busywait', 'tx_serial_ui_out'): setattr(self._mmap, name, boolstr(setting.value)) elif name == "mice_message": multiple = self._mmap.multiple multiple['mice_message'] = MICE_MESSAGE.index( str(setting.value)) self._mmap.multiple = multiple elif name == "path": self._mmap.path = PATH.index(str(setting.value)) elif name == "path1": self._mmap.path1 = "%s%s" % ( setting.value, self._mmap.path1[6]) elif name == "ssid1": self._mmap.path1 = "%s%s" % ( self._mmap.path1[:6], setting.value) elif name == "path2": self._mmap.path2 = "%s%s" % ( setting.value, self._mmap.path2[6]) elif name == "ssid2": self._mmap.path2 = "%s%s" % ( self._mmap.path2[:6], setting.value) elif name == "path3": self._mmap.path3 = "%s%s" % ( setting.value, self._mmap.path3[6]) elif name == "ssid3": self._mmap.path3 = "%s%s" % ( self._mmap.path3[:6], setting.value) elif name == "table": self.set_symbol(table=setting.value) elif name == "symbol": self.set_symbol(symbol=setting.value) elif name == "beacon": self._mmap.beacon = BEACON.index(str(setting.value)) + 1 elif name == "rate": self._mmap.rate = "%04d" % setting.value elif name == "comment": self._mmap.comment = str(setting.value) elif name == "voltage": multiple = self._mmap.multiple multiple['voltage'] = int(setting.value) self._mmap.multiple = multiple elif name == "temperature": multiple = self._mmap.multiple multiple['temperature'] = int(setting.value) self._mmap.multiple = multiple elif name == "status": self._mmap.status = str(setting.value) elif name in ("telemetry", "telemetry_every", "timeslot_enable", "timeslot", "tfx", "blueled", "dcd"): multiple = self._mmap.multiple multiple[name] = int(setting.value) self._mmap.multiple = multiple elif name == "chinamapfix": self.set_chinamapfix(enable=setting.value) elif name == "chinalat": self.set_chinamapfix(lat=int(setting.value)) elif name == "chinalon": self.set_chinamapfix(lon=int(setting.value)) elif name == "digipeat": self.set_digipeat(enable=setting.value) elif name == "alias": self.set_digipeat( alias=str(ALIAS.index(str(setting.value)) + 1)) elif name == "virtualgps": self.set_virtualgps(enable=setting.value) elif name == "btext": self.set_virtualgps(btext=str(setting.value)) elif name == "lowspeed": self.set_smartbeacon(lowspeed=int(setting.value)) elif name == "highspeed": self.set_smartbeacon(highspeed=int(setting.value)) elif name == "slowrate": self.set_smartbeacon(slowrate=int(setting.value)) elif name == "fastrate": self.set_smartbeacon(fastrate=int(setting.value)) elif name == "turnslope": self.set_smartbeacon(turnslope=int(setting.value)) elif name == "turnangle": self.set_smartbeacon(turnangle=int(setting.value)) elif name == "turntime": self.set_smartbeacon(turntime=int(setting.value)) elif name in ("tx_volume", "rx_volume", "squelch"): setattr(self._mmap, name, "%1d" % setting.value) elif name == "auto_on": self._mmap.auto_on = "%s%05d" % ( bool(setting.value) and '1' or 'i', int(self._mmap.auto_on[1:])) elif name == "auto_on_delay": self._mmap.auto_on = "%s%05d" % ( self._mmap.auto_on[0], setting.value) elif name == "tf_card": multiple = self._mmap.multiple multiple['tf_card'] = TF_CARD.index(str(setting.value)) self._mmap.multiple = multiple except Exception: LOG.debug(setting.get_name()) raise def set_callsign(self, callsign=None, ssid=None): if callsign is None: callsign = self._mmap.callsign[:6] if ssid is None: ssid = ord(self._mmap.callsign[6]) - 0x30 self._mmap.callsign = str(callsign) + chr(ssid + 0x30) def set_symbol(self, table=None, symbol=None): if table is None: table = self._mmap.symbol[1] if symbol is None: symbol = self._mmap.symbol[0] self._mmap.symbol = str(symbol) + str(table) def set_chinamapfix(self, enable=None, lat=None, lon=None): if enable is None: enable = strbool(self._mmap.chinamapfix[0]) if lat is None: lat = ord(self._mmap.chinamapfix[2]) - 80 if lon is None: lon = ord(self._mmap.chinamapfix[1]) - 80 self._mmapchinamapfix = boolstr(enable) + chr(lon + 80) + chr(lat + 80) def set_digipeat(self, enable=None, alias=None): if enable is None: enable = strbool(self._mmap.digipeat[0]) if alias is None: alias = self._mmap.digipeat[1] self._mmap.digipeat = boolstr(enable) + alias def set_virtualgps(self, enable=None, btext=None): if enable is None: enable = strbool(self._mmap.virtualgps[0]) if btext is None: btext = self._mmap.virtualgps[1:] self._mmap.virtualgps = boolstr(enable) + btext def set_smartbeacon(self, **kwargs): sb = self._mmap.smartbeacon sb.update(kwargs) if sb['lowspeed'] > sb['highspeed']: raise InvalidValueError("Low speed must be less than high speed") if sb['slowrate'] < sb['fastrate']: raise InvalidValueError("Slow rate must be greater than fast rate") self._mmap.smartbeacon = sb @classmethod def match_model(cls, filedata, filename): return filedata.startswith(b'\r\n00=' + cls._model) chirp-20261030/chirp/drivers/ar8200.py0000644000175000017500000017243715250742640016126 0ustar hibbyhibby# Copyright 2026 Darius Rad # # 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 . import logging import re import threading import time from chirp import chirp_common, errors, directory from chirp.settings import RadioSetting, RadioSettingGroup, \ RadioSettingSubGroup, RadioSettingValueBoolean, RadioSettingValueFloat, \ RadioSettingValueInteger, RadioSettingValueList, RadioSettingValueMap, \ RadioSettingValueString, RadioSettings LOG = logging.getLogger(__name__) # CHANNEL NUMBERING # # CHIRP identifies memories with sequentially numbered indices. The # memory locations in the AR8200 are mapped to CHIRP memories as # follows. # # Memories 0 through 999 # # Scan memories by bank: A, a, B, b, ... J, j. Each pair of banks # (A and a, B and b, etc.) occupies 100 channels. The split between # the banks in the pair is determined by the bank size configured # under settings. # # Memories 1000 through 1999 # # Scan pass frequencies by bank: A, a, B, b, ... J, j. There are 50 # pass frequencies per bank. # # Memories 2000 through 2099 # # VFO pass frequencies. # # Memories 2100 through 2139 # # One channel per search bank: A through T, then a through t. # # Special channels # # VFO: Single VFO mode channel configuration # VFO-A and VFO-B: Twin VFO mode channel configuration # QM0 through QM9: VFO quick memories # ISSUES AND LIMITATIONS # # After adjusting any of the following settings, it is necessary to # refresh certain other memory locations: # # - Bank split size # - Add or remove search pass frequencies # - Change offset frequencies # - Removing channels from select scan list # # The radio supports three AM modes, but CHIRP only knows about two. # Mode is set to AM for all, and an extra field specifies which AM # mode is used. # # Search pass frequencies cannot be modified, they can only be added # or removed. When a pass frequency is removed, all others in the # same search bank are moved up. # # This driver does not provide a bank model because (1) CHIRP does not # support banks for live radios, and (2) the banks on the radio don't # easily map to the bank model provided by CHIRP. # # Quick memory channel details beyond the frequency are not available, # as reading this from the serial port is not supported. # # Adding to the select scan list is not supported over the serial # port. # # Auto mode also lets the radio control duplex and offset, tuning # step, and step adjust. # # Radio must be power cycled after using CHIRP. The serial protocol # supports a command to terminate the session and release the keypad, # but there is no suitable place in CHIRP to make this call. By the # time the __del__ method is called, the serial port is already # closed. # # Radio supports additional characters not presently reflected in the # valid character set, including arrows, Japanese characters (yen, # katakana), Greek letters, other European letters and accents, and # math symbols. @directory.register class AR8200Radio(chirp_common.LiveRadio): """AOR AR8200""" BAUD_RATE = 19200 VENDOR = "AOR" MODEL = "AR8200" # These need to match the indices of the MD command. _MODES = [('WFM', 0), ('FM', 1), ('AM', 2), ('USB', 3), ('LSB', 4), ('CW', 5), ('NFM', 6), ('WAM', 7), ('NAM', 8), ('Auto', 0xf)] _AM_MODES = ['AM', 'WAM', 'NAM'] _SCAN_MEMORIES = 1000 _SEARCH_PASS_MEMORIES = 1100 _SCAN_BANKS = 'AaBbCcDdEeFfGgHhIiJj' _SEARCH_BANKS = 'ABCDEFGHIJKLMNOPQRSTabcdefghijklmnopqrst' _SPECIAL_CHANS = { 'VFO': -13, 'VFO-A': -12, 'VFO-B': -11, 'QM0': -10, 'QM1': -9, 'QM2': -8, 'QM3': -7, 'QM4': -6, 'QM5': -5, 'QM6': -4, 'QM7': -3, 'QM8': -2, 'QM9': -1} # Skip reading settings that require selecting each stored memory # channel, which significantly slows download time. _SKIP_SELECT = False _upper = _SCAN_MEMORIES + _SEARCH_PASS_MEMORIES + len(_SEARCH_BANKS) - 1 _scan_bank_sizes = {} _GS = None _OL_LIST = None _GR_LIST = None _command_cache = {} _rf = None def __init__(self, *args, **kwargs): chirp_common.LiveRadio.__init__(self, *args, **kwargs) self._memcache = {} self.LOCK = threading.Lock() if self.pipe: self.pipe.timeout = 2 self.pipe.stopbits = 2 self._pipe_reset() if not self._connect(): raise Exception("Error connecting to radio") # set VFO mode because documentation says not to run MX # command (write data) while scanning or searching self._command('VF') # set squelch closed to avoid noise while changing channels self._command('MC1') def get_features(self): if self._rf: return self._rf rf = chirp_common.RadioFeatures() rf.has_dtcs = False rf.has_dtcs_polarity = False rf.has_bank = False rf.has_ctone = False rf.has_cross = False rf.has_nostep_tuning = True rf.has_settings = True rf.valid_modes = ['WFM', 'FM', 'AM', 'USB', 'LSB', 'CW', 'NFM', 'Auto'] rf.valid_bands = [(100000, 2040000000)] rf.valid_skips = ['', 'S'] rf.valid_characters = chirp_common.CHARSET_ASCII rf.valid_name_length = 12 rf.valid_special_chans = list(self._SPECIAL_CHANS.keys()) rf.valid_tones = [] rf.memory_bounds = (0, self._upper) self._rf = rf return rf def _decode_number(self, number): if number < -10: if number in list(self._SPECIAL_CHANS.values()): return ('vfo', None, number) return (None, None, None) if number < 0: return ('qm', None, number + 10) if number < self._SCAN_MEMORIES: bank = (number // 100) * 2 chan = number % 100 if chan >= self._scan_bank_size(self._SCAN_BANKS[bank]): chan = chan - self._scan_bank_size(self._SCAN_BANKS[bank]) bank += 1 return ('scan', self._SCAN_BANKS[bank], chan) number -= self._SCAN_MEMORIES if number < self._SEARCH_PASS_MEMORIES: if number > len(self._SEARCH_BANKS) * 50: bank = 'V' chan = number - len(self._SEARCH_BANKS) * 50 else: bank = self._SEARCH_BANKS[number // 50] chan = number % 50 return ('pass', bank, chan) number -= self._SEARCH_PASS_MEMORIES if number < len(self._SEARCH_BANKS): return ('search', self._SEARCH_BANKS[number], None) return (None, None, None) def _encode_number(self, region, bank, chan): if region == 'vfo': return self._SPECIAL_CHANS.get(chan, None) elif region == 'qm': return chan - 10 if chan < 10 else None elif region == 'scan': if bank not in self._SCAN_BANKS or \ chan >= self._scan_bank_size(bank): return None offset = 100 * (self._SCAN_BANKS.index(bank) // 2) if bank.islower(): offset += self._scan_bank_size(bank.upper()) return offset + chan elif region == 'pass': if bank == 'V': offset = self._SCAN_MEMORIES + len(self._SEARCH_BANKS) * 50 size = 100 elif bank in self._SEARCH_BANKS: offset = self._SCAN_MEMORIES + \ self._SEARCH_BANKS.index(bank) * 50 size = 50 return offset + chan if chan < size else None elif region == 'search': if bank not in self._SEARCH_BANKS: return None return self._SCAN_MEMORIES + self._SEARCH_PASS_MEMORIES + \ self._SEARCH_BANKS.index(bank) return None def _connect(self): """Connect to the radio""" bauds = [19200, 9600, 4800] for b in bauds: self.pipe.baudrate = b self._command('', '[?]') # no real detection mechanism, to just try the VR command res = self._command('VR', 'VR([0-9]+)') if res: return True raise errors.RadioError("No response from radio") def _pipe_reset(self): self.pipe.reset_input_buffer() self.pipe.reset_output_buffer() self.pipe.write('\r'.encode('ascii')) self.pipe.readline() def _command(self, cmd, pattern='$', lines=1, cache=False, varlist=False, retries=5, timeout=None): retry_wait = .25 cmd += '\r' old_timeout = None if timeout is not None: old_timeout = self.pipe.timeout self.pipe.timeout = timeout if cache: pattern = '([A-Za-z0-9]+) (?:(?P---)$|.*)' for retry in range(retries): self.pipe.write(cmd.encode('ascii')) LOG.debug("tx {}".format(cmd.strip())) for n in range(lines if cache else 1): res = self.pipe.readline().decode('ascii', errors='replace') res = res.strip() LOG.debug("rx {}".format(res)) m = re.match(pattern, res) if not m: LOG.error("Unexpected response from radio: {} -> {}" "".format(cmd.strip(), res)) break if cache: if varlist and m['empty']: m = True break if m.lastindex > 0: LOG.debug("command cache {} -> {}".format(m[1], m[0])) self._command_cache[m[1]] = m[0] else: if cache: m = True else: for n in range(lines - 1): res = self.pipe.readline().strip() LOG.debug("rx {}".format(res)) if m: break LOG.debug("Retrying...") time.sleep(retry_wait) retry_wait *= 2 retry_wait = min(retry_wait, 5) self._pipe_reset() else: m = None if old_timeout is not None: self.pipe.timeout = old_timeout return m def _command_read_WM(self, bank, retries=5): retry_wait = .25 cmd = 'WM{}\r'.format(bank) for n in range(retries): self.pipe.write(cmd.encode('ascii')) LOG.debug("tx {}".format(cmd.strip())) ret = () for b in (bank, bank.lower()): res = self.pipe.readline().decode('ascii', errors='replace') res = res.strip() LOG.debug("rx {}".format(res)) mat = re.match('WM ([A-Ja-j])([01])', res) if not mat or mat[1] != b: LOG.error("Unexpected response from radio: {} -> {}" "".format(cmd.strip(), res)) break ret += (bool(int(mat[2])),) else: return ret LOG.debug("Retrying...") time.sleep(retry_wait) retry_wait *= 2 retry_wait = min(retry_wait, 5) self._pipe_reset() return None def _command_update_cache(self, cmd, lines=10, varlist=False, retries=5): return self._command(cmd=cmd, lines=lines, cache=True, varlist=varlist, retries=retries) def _command_delete_cache(self, key, pattern=False): if not pattern: self._command_cache.pop(key, None) else: r = re.compile(key) for k in list(filter(r.match, self._command_cache.keys())): self._command_cache.pop(k, None) def _command_cached(self, key, pattern, retries=5): retry_wait = .25 for n in range(retries): if key not in self._command_cache: cmd = re.match('(OL|MX|QM|GM|GS|PR|GR)', key) if not cmd: LOG.debug("Response not found in cache: {}".format(key)) return None if cmd[1] == 'OL': for n in range(5): self._command_update_cache('OL') elif cmd[1] == 'MX': bank = key[2] size = self._scan_bank_size(bank) self._command_update_cache('MA{}'.format(bank)) for n in range(-(size // -10) - 1): self._command_update_cache('MA') elif cmd[1] == 'QM': self._command_update_cache('QM') elif cmd[1] in ('GM', 'GS'): self._command_update_cache(cmd[1], 2) elif cmd[1] == 'PR': for n in range(100 if key[2] == 'V' else 50): cachekey = '{}{:02d}'.format(key[:3], n) val = '{} ---'.format(cachekey) self._command_cache[cachekey] = val self._command_update_cache( key[:3], 100 if key[2] == 'V' else 50, varlist=True) elif cmd[1] == 'GR': self._command_update_cache('GR', 100, varlist=True) else: LOG.debug("Response not found in cache: {}".format(key)) return None if key in self._command_cache: res = re.match(pattern, self._command_cache[key]) if res: return res LOG.debug("Unexpected result from radio {}" "".format(self._command_cache[key])) del self._command_cache[key] else: LOG.debug("Response not found in cache: {}".format(key)) LOG.debug("Retrying...") time.sleep(retry_wait) retry_wait *= 2 return None def _scan_bank_size(self, bank): if bank not in self._SCAN_BANKS: return None if bank.upper() not in self._scan_bank_sizes: res = self._command('MW{}'.format(bank.upper()), 'MW [A-J]:([1-9]0) [a-j]:([1-9]0)') self._scan_bank_sizes[bank.upper()] = int(res[1]) if bank.isupper(): return self._scan_bank_sizes[bank] else: return 100 - self._scan_bank_sizes[bank.upper()] def _get_ol_list_uncached(self): self._OL_LIST = [0] for n in range(1, 48): res = self._command_cached('OL{:02d}'.format(n), 'OL([0-4][0-9]) RF(0[0-9]{7}00)') if n != int(res[1]): raise errors.RadioError( "Unexpected result returned from radio") self._OL_LIST.append(int(res[2])) LOG.info("Offset frequency {} is {}".format(n, res[2])) return self._OL_LIST def _get_ol_list(self, locked=False): if self._OL_LIST: return self._OL_LIST if locked: with self.LOCK: return self._get_ol_list_uncached() else: return self._get_ol_list_uncached() def _set_pp(self, number): (region, bank, chan) = self._decode_number(number) self._command('PP{}{:02d}'.format(bank, chan)) def _get_gr_list(self): if isinstance(self._GR_LIST, list): return self._GR_LIST self._GR_LIST = [] for n in range(100): res = self._command_cached( 'GR{:02d}'.format(n), ('GR(?P[0-9]{2}) (?:(?P---)$|' 'MX(?P[A-Ja-j])(?P[0-9]{2}))')) if not res: break if n != int(res['num']): raise errors.RadioError( "Unexpected result returned from radio") self._GR_LIST.append((res['bank'], int(res['chan']))) LOG.info("Select scan entry {} is {}{:02d}" "".format(n, res['bank'], int(res['chan']))) return self._GR_LIST def _save_gs(self): if self._GS: return res = self._command('GS', 'GS([0-9])', 2) self._GS = int(res[1]) def _restore_gs(self): self._command('GS{}'.format(self._GS)) def _invalidate_scan_bank_size(self): self._scan_bank_sizes = {} def _invalidate_ol_list(self): self._OL_LIST = None self._command_delete_cache('OL([0-9]{2})', True) def _invalidate_gr_list(self): self._GR_LIST = None self._command_delete_cache('GR([0-9]{2})', True) def _invalidate_memory(self, region=None, bank=None, chan=None, number=None, emptyonly=False): if number is not None: (region, bank, chan) = self._decode_number(number) else: number = self._encode_number(region, bank, chan) if number in self._memcache: if emptyonly and not self._memcache[number].empty: return del self._memcache[number] if region == 'qm': self._command_delete_cache('QM{:1d}'.format(chan), True) elif region == 'scan': self._command_delete_cache('MX{}{:02d}'.format(bank, chan), True) elif region == 'pass': self._command_delete_cache('PR{}{:02d}'.format(bank, chan), True) elif region == 'search': # commands not cached pass def get_memory(self, number): if number in self._memcache: return self._memcache[number] with self.LOCK: return self._get_memory_unlocked(number) def _get_memory_unlocked(self, number): mem = chirp_common.Memory() mem.extra = RadioSettingGroup('extra', 'Extra') AM_LIST = ['AM', 'NAM', 'WAM'] extra_am = RadioSettingValueList(AM_LIST) rs = RadioSetting('am', 'AM Mode', extra_am) mem.extra.append(rs) lv = min(list(zip(*self._rf.valid_bands))[0]) / 1000000 uv = max(list(zip(*self._rf.valid_bands))[1]) / 1000000 extra_upper = RadioSettingValueFloat(lv, uv, lv, 0.00005, 5) rs = RadioSetting('upper', 'Upper Frequency', extra_upper) mem.extra.append(rs) extra_adjust = RadioSettingValueFloat(0, 999.95, 0, 0.05, 2) rs = RadioSetting('adjust', 'Step Adjust', extra_adjust) mem.extra.append(rs) extra_att = RadioSettingValueBoolean(False) rs = RadioSetting('att', 'Attenuator', extra_att) mem.extra.append(rs) extra_limit = RadioSettingValueBoolean(False) rs = RadioSetting('limit', 'Noise Limiter', extra_limit) mem.extra.append(rs) extra_protect = RadioSettingValueBoolean(False) rs = RadioSetting('protect', 'Protect', extra_protect) mem.extra.append(rs) extra_select = RadioSettingValueBoolean(False) rs = RadioSetting('select', 'Select Scan', extra_select) mem.extra.append(rs) extra_selectindex = RadioSettingValueInteger(0, 99, 0) rs = RadioSetting('selectindex', 'Select Scan Index', extra_selectindex) mem.extra.append(rs) if isinstance(number, str): mem.number = self._SPECIAL_CHANS[number] mem.extd_number = number else: mem.number = number (region, bank, chan) = self._decode_number(mem.number) if region is None: raise errors.InvalidMemoryLocation( "No such memory {}".format(number)) if region == 'vfo': if chan == self._SPECIAL_CHANS['VFO']: self._command('VF') mem.name = 'VFO' elif chan == self._SPECIAL_CHANS['VFO-A']: self._command('VA') mem.name = 'VFO-A' elif chan == self._SPECIAL_CHANS['VFO-B']: self._command('VB') mem.name = 'VFO-B' ol_list = self._get_ol_list() res = self._command( 'RX', ('V[ABF] RF(?P[0-9]{10}) ' 'ST(?P[0-9]{6})(?P[+]?) AU(?P[01]) ' 'MD(?P[0-8]) AT(?P[01])')) if not res: pass else: mem.freq = int(res['freq']) if int(res['auto']): mem.mode = 'Auto' else: radio_mode = [mode[0] for mode in self._MODES if mode[1] == int(res['mode'])][0] if radio_mode in self._AM_MODES: mem.mode = 'AM' extra_am.set_value(radio_mode) else: mem.mode = radio_mode mem.tuning_step = float(res['step']) / 1000 extra_att.set_value(int(res['att'])) res = self._command('OF', 'OF([0-4][0-9])([ +-])') mem.offset = ol_list[int(res[1])] if res else 0 mem.duplex = res[2] if res and res[2] in ('+', '-') else '' res = self._command('SH', 'SH([0-9]{6})([+]?)') if res: extra_adjust.set_value( int(res[1]) / 1000 if res[2] == '+' else 0) res = self._command('NL', 'NL([01])') if res: extra_limit.set_value(int(res[1])) extra_protect.set_mutable(False) extra_upper.set_mutable(False) mem.immutable += ['name', 'skip', 'extra.upper', 'extra.protect', 'extra.select'] elif region == 'qm': res = self._command_cached('QM{:1d}'.format(chan), 'QM([0-9]) RF([0-9]{10})') if not res: mem.empty = True else: if chan != int(res[1]): raise errors.RadioError( "Unexpected result returned from radio") mem.freq = int(res[2]) mem.name = 'QM{:1d}'.format(chan) extra_am.set_mutable(False) extra_upper.set_mutable(False) extra_adjust.set_mutable(False) extra_att.set_mutable(False) extra_limit.set_mutable(False) extra_protect.set_mutable(False) mem.immutable += ['empty', 'freq', 'name', 'offset', 'mode', 'tuning_step', 'skip', 'duplex', 'extra.upper', 'extra.adjust', 'extra.att', 'extra.limit', 'extra.protect', 'extra.select'] elif region == 'scan': ol_list = self._get_ol_list() gr_list = self._get_gr_list() res = self._command_cached( 'MX{}{:02d}'.format(bank, chan), ('MX(?P[A-Ja-j])(?P[0-9]{2}) ' '(?:(?P---)$|MP(?P[01]) RF(?P[0-9]{10}) ' 'ST(?P[0-9]{6})(?P[+]?) AU(?P[01]) ' 'MD(?P[0-8]) AT(?P[01]) TM(?P.{0,12}))')) if not res: pass elif res['empty']: mem.empty = True else: if res['bank'] != bank or int(res['chan']) != chan: raise errors.RadioError( "Unexpected result returned from radio") mem.freq = int(res['freq']) mem.name = res['name'] if int(res['auto']): mem.mode = 'Auto' else: radio_mode = [mode[0] for mode in self._MODES if mode[1] == int(res['mode'])][0] if radio_mode in self._AM_MODES: mem.mode = 'AM' extra_am.set_value(radio_mode) else: mem.mode = radio_mode mem.tuning_step = float(res['step']) / 1000 mem.skip = 'S' if int(res['pass']) else '' extra_att.set_value(int(res['att'])) if not self._SKIP_SELECT: self._command('MR{}{:02d}'.format(bank, chan)) res = self._command('OF', 'OF([0-4][0-9])([ +-])') mem.offset = ol_list[int(res[1])] if res else 0 mem.duplex = res[2] if res and res[2] in ('+', '-') else '' res = self._command('SH', 'SH([0-9]{6})([+]?)') if res: extra_adjust.set_value( int(res[1]) / 1000 if res[2] == '+' else 0) res = self._command('NL', 'NL([01])') if res: extra_limit.set_value(int(res[1])) res = self._command('PC', 'PC([01])') if res: extra_protect.set_value(int(res[1])) if (bank, chan) in gr_list: extra_select.set_value(True) extra_selectindex.set_value( gr_list.index((bank, chan))) else: mem.immutable += ['extra.select'] else: extra_adjust.set_mutable(False) extra_limit.set_mutable(False) extra_protect.set_mutable(False) mem.immutable += ['offset', 'duplex', 'extra.adjust', 'extra.limit', 'extra.protect', 'extra.select'] extra_upper.set_mutable(False) mem.immutable += ['extra.upper'] elif region == 'pass': res = self._command_cached( 'PR{}{:02d}'.format(bank, chan), ('PR(?P[A-Ta-tV])(?P[0-9]{2}) ' '(?:(?P---)$|(?P[0-9]{10}))')) if not res or res['empty']: mem.empty = True else: if res['bank'] != bank or int(res['chan']) != chan: raise errors.RadioError( "Unexpected result returned from radio") mem.freq = int(res['freq']) mem.name = 'PR{}{:02d}'.format(bank, chan) mem.immutable += ['freq'] mem.tuning_step = 20.0 mem.skip = 'S' extra_am.set_mutable(False) extra_upper.set_mutable(False) extra_adjust.set_mutable(False) extra_att.set_mutable(False) extra_limit.set_mutable(False) extra_protect.set_mutable(False) mem.immutable += ['name', 'offset', 'mode', 'tuning_step', 'skip', 'duplex', 'extra.upper', 'extra.adjust', 'extra.att', 'extra.limit', 'extra.protect', 'extra.select'] elif region == 'search': ol_list = self._get_ol_list() res = self._command( 'SR{}'.format(bank), ('SR(?P[A-Ta-t]) (?:(?P---)$|' 'SL(?P[0-9]{10}) SU(?P[0-9]{10}) ' 'ST(?P[0-9]{6})(?P[+]?) AU(?P[01]) ' 'MD(?P[0-8]) AT(?P[01]) TT(?P.{0,12}))')) if not res: pass elif res['empty']: mem.empty = True else: if res['bank'] != bank: raise errors.RadioError( "Unexpected result returned from radio") mem.freq = int(res['lower']) mem.name = res['name'] if int(res['auto']): mem.mode = 'Auto' else: radio_mode = [mode[0] for mode in self._MODES if mode[1] == int(res['mode'])][0] if radio_mode in self._AM_MODES: mem.mode = 'AM' extra_am.set_value(radio_mode) else: mem.mode = radio_mode mem.tuning_step = float(res['step']) / 1000 extra_upper.set_value(int(res['upper']) / 1000000) extra_att.set_value(int(res['att'])) self._save_gs() self._command('GS0') self._command('SS{}'.format(bank)) res = self._command('OF', 'OF([0-4][0-9])([ +-])') mem.offset = ol_list[int(res[1])] if res else 0 mem.duplex = res[2] if res and res[2] in ('+', '-') else '' res = self._command('SH', 'SH([0-9]{6})([+]?)') if res: v = int(res[1]) / 1000 if res[2] == '+' else 0 extra_adjust.set_value(v) res = self._command('NL', 'NL([01])') if res: extra_limit.set_value(int(res[1])) res = self._command('BP', 'BP([01])') if res: extra_protect.set_value(int(res[1])) self._restore_gs() self._command('VF') mem.immutable += ['skip', 'extra.select'] else: mem.empty = True extra_select.set_mutable(False) mem.immutable += ['extra.selectindex', 'tmode', 'rtone'] self._memcache[mem.number] = mem return mem def validate_memory(self, memory): msgs = [] ol_list = self._get_ol_list(True) (region, bank, chan) = self._decode_number(memory.number) if region in ('scan', 'search'): if memory.offset not in ol_list: msg = chirp_common.ValidationWarning( "Offset frequency is not valid, see table in settings " "for valid values") msgs.append(msg) if memory.tuning_step == 0.0 or memory.tuning_step >= 1000.0 or \ (int(memory.tuning_step * 1000) % 50) != 0: msg = chirp_common.ValidationWarning( "Tuning step must be non-zero, less than 1000 kHz, and " "divisible by 0.05 kHz") msgs.append(msg) if 'adjust' in memory.extra: if float(memory.extra['adjust'].value) >= memory.tuning_step: msg = chirp_common.ValidationWarning( "Step adjust must be smaller than tuning step") msgs.append(msg) if region == 'search' and 'upper' in memory.extra: upper = int(float(memory.extra['upper'].value) * 1000000) if memory.freq > upper: msg = chirp_common.ValidationWarning( "Upper search frequency is not greater than lower " "frequency") msgs.append(msg) valid = False for (lv, uv) in self._rf.valid_bands: if upper >= lv and upper <= uv: valid = True break if not valid: msg = chirp_common.ValidationError( "Upper search frequency {} is not in a valid band" "".format(float(memory.extra['upper'].value))) msgs.append(msg) if region == 'pass': if memory.skip != 'S': msg = chirp_common.ValidationWarning( "Search pass frequencies must be marked skip") msgs.append(msg) msg = chirp_common.ValidationWarning( "Memories must be refreshed after modifying pass channels") msgs.append(msg) return msgs + super().validate_memory(memory) def set_memory(self, memory): with self.LOCK: return self._set_memory_unlocked(memory) def _set_memory_unlocked(self, memory): self._invalidate_memory(number=memory.number) (region, bank, chan) = self._decode_number(memory.number) if region is None: raise errors.InvalidMemoryLocation( "No such memory {}".format(memory.number)) if region == 'vfo': if chan == self._SPECIAL_CHANS['VFO']: self._command('VF') elif chan == self._SPECIAL_CHANS['VFO-A']: self._command('VA') elif chan == self._SPECIAL_CHANS['VFO-B']: self._command('VB') ol_list = self._get_ol_list() freq = memory.freq auto = int(memory.mode == 'Auto') step = int(memory.tuning_step * 1000) if memory.mode == 'Auto': mode = 0 elif memory.mode == 'AM': ammode = memory.extra['am'] if 'am' in memory.extra else 'AM' mode = [mode[1] for mode in self._MODES if mode[0] == ammode][0] else: mode = [mode[1] for mode in self._MODES if mode[0] == memory.mode][0] att = int(memory.extra['att'].value) \ if 'att' in memory.extra else 0 duplex = memory.duplex if memory.duplex in ('+', '-') else '' offset = ol_list.index(memory.offset) \ if duplex and memory.offset in ol_list else 0 adjust = float(memory.extra['adjust'].value) \ if 'adjust' in memory.extra else 0 limit = int(memory.extra['limit'].value) \ if 'limit' in memory.extra else 0 self._command('RF{:010d}'.format(freq)) self._command('AT{:1d}'.format(att)) self._command('AU{:1d}'.format(auto)) if not auto: self._command('MD{:1d}'.format(mode)) self._command('ST{:06d}'.format(step)) self._command('OF{:02d}{}'.format(offset, duplex)) # step tuning enable can only be toggled, so get the # current value first toggle = '' res = self._command('SH', 'SH([0-9]{6})([+]?)') if res: toggle = '+' if (res[2] == '+') != (adjust > 0) else '' self._command('SH{:06d}{}'.format(int(adjust * 1000), toggle)) self._command('NL{:1d}'.format(limit)) elif region == 'qm': raise errors.InvalidDataError( "Quick memory locations are read-only") elif region == 'scan': self._command_delete_cache('MX{:1}{:02d}'.format(bank, chan)) ol_list = self._get_ol_list() gr_list = self._get_gr_list() freq = memory.freq auto = int(memory.mode == 'Auto') step = int(memory.tuning_step * 1000) if memory.mode == 'Auto': mode = 0 elif memory.mode == 'AM': ammode = memory.extra['am'] if 'am' in memory.extra else 'AM' mode = [mode[1] for mode in self._MODES if mode[0] == ammode][0] else: mode = [mode[1] for mode in self._MODES if mode[0] == memory.mode][0] att = int(memory.extra['att'].value) \ if 'att' in memory.extra else 0 duplex = memory.duplex if memory.duplex in ('+', '-') else '' offset = ol_list.index(memory.offset) \ if duplex and memory.offset in ol_list else 0 adjust = float(memory.extra['adjust'].value) \ if 'adjust' in memory.extra else 0 limit = int(memory.extra['limit'].value) \ if 'limit' in memory.extra else 0 protect = int(memory.extra['protect'].value) \ if 'protect' in memory.extra else 0 select = int(memory.extra['select'].value) \ if 'select' in memory.extra else 0 name = memory.name mp = 1 if memory.skip == 'S' else 0 offsetstr = 'OF{:02d}{} '.format(offset, duplex) \ if not self._SKIP_SELECT else '' res = self._command( ('MX{}{:02d} RF{:010d} MP{:1d} ST{:06d} MD{:1d} AT{:1d} ' '{}AU{:1d} TM{:12}'.format(bank, chan, freq, mp, step, mode, att, offsetstr, auto, name)), '(?:$|{:c}{:c})'.format(0x13, 0x11)) if not res: raise errors.InvalidDataError( "Radio refused {}".format(memory.number)) if not self._SKIP_SELECT: self._command('MR{}{:02d}'.format(bank, chan)) if not auto: # step tuning enable can only be toggled, so get # the current value first toggle = '' res = self._command('SH', 'SH([0-9]{6})([+]?)') if res: toggle = '+' if (res[2] == '+') != (adjust > 0) else '' self._command( 'SH{:06d}{}'.format(int(adjust * 1000), toggle)) self._command('NL{:1d}'.format(limit)) self._command('PC{:1d}'.format(protect)) if not select and (bank, chan) in gr_list: self._command( 'GD{:02d}'.format(gr_list.index((bank, chan)))) self._invalidate_gr_list() elif region == 'pass': self._command_delete_cache('PR{}[0-9]{{2}}'.format(bank), True) freq = memory.freq res = self._command('PW{}{:010d}'.format(bank, freq)) if not res: raise errors.InvalidDataError( "Radio refused {}".format(memory.number)) for n in range(100 if bank == 'V' else 50): self._invalidate_memory(region, bank, n, emptyonly=True) elif region == 'search': self._command_delete_cache('SR{}'.format(bank)) ol_list = self._get_ol_list() lower = memory.freq upper = int(float(memory.extra['upper'].value) * 1000000) \ if 'upper' in memory.extra else lower auto = int(memory.mode == 'Auto') step = int(memory.tuning_step * 1000) if memory.mode == 'Auto': mode = 0 elif memory.mode == 'AM': ammode = memory.extra['am'] if 'am' in memory.extra else 'AM' mode = [mode[1] for mode in self._MODES if mode[0] == ammode][0] else: mode = [mode[1] for mode in self._MODES if mode[0] == memory.mode][0] att = int(memory.extra['att'].value) \ if 'att' in memory.extra else 0 duplex = memory.duplex if memory.duplex in ('+', '-') else '' offset = ol_list.index(memory.offset) \ if duplex and memory.offset in ol_list else 0 adjust = float(memory.extra['adjust'].value) \ if 'adjust' in memory.extra else 0 limit = int(memory.extra['limit'].value) \ if 'limit' in memory.extra else 0 protect = int(memory.extra['protect'].value) \ if 'protect' in memory.extra else 0 name = memory.name # correct lower/upper out of order if lower > upper: (lower, upper) = (upper, lower) # fix out of range lower = max(lower, min(list(zip(*self._rf.valid_bands))[0])) upper = min(upper, max(list(zip(*self._rf.valid_bands))[1])) res = self._command( 'SE{} SL{:010d} SU{:010d} ST{:06d} MD{:1d} AT{:1d} ' 'OF{:02d}{:1} AU{:1d} TT{:12}'.format(bank, lower, upper, step, mode, att, offset, duplex, auto, name)) if not res: raise errors.InvalidDataError( "Radio refused {}".format(memory.number)) self._save_gs() self._command('GS0') self._command('SS{}'.format(bank)) if not auto: # step tuning enable can only be toggled, so get the # current value first toggle = '' res = self._command('SH', 'SH([0-9]{6})([+]?)') if res: toggle = '+' if (res[2] == '+') != (adjust > 0) else '' self._command('SH{:06d}{}'.format(int(adjust * 1000), toggle)) self._command('NL{:1d}'.format(limit)) self._command('BP{:1d}'.format(protect)) self._restore_gs() self._command('VF') def erase_memory(self, number): with self.LOCK: return self._erase_memory_unlocked(number) def _erase_memory_unlocked(self, number): if isinstance(number, str): if number not in self._SPECIAL_CHANS: return number = self._SPECIAL_CHANS[number] if number not in self._memcache: return (region, bank, chan) = self._decode_number(number) if region == 'qm': raise errors.InvalidDataError( "Quick memory locations are read-only") elif region == 'scan': self._command_delete_cache('MX{}{:02d}'.format(bank, chan)) self._command('MR{}{:02d}'.format(bank, chan)) self._command('MQ{:02d}'.format(chan)) elif region == 'pass': self._command_delete_cache('PR{}[0-9]{{2}}'.format(bank), True) self._command('PD{}{:02d}'.format(bank, chan)) for n in range(chan, 100 if bank == 'V' else 50): self._invalidate_memory(region, bank, n) elif region == 'search': self._command_delete_cache('SR{}'.format(bank)) self._command('QS{}'.format(bank)) del self._memcache[number] def get_settings(self): with self.LOCK: return self._get_settings_unlocked() def _get_settings_unlocked(self): main = RadioSettingGroup('main', 'Main') vfo = RadioSettingGroup('vfo', 'VFO') freq = RadioSettingGroup('freq', 'Offset') scan = RadioSettingGroup('scan', 'Scan') search = RadioSettingGroup('search', 'Search') scanbankprotect = RadioSettingGroup('scanbankprotect', 'Scan Bank Protect') top = RadioSettings(main, vfo, freq, scan, search, scanbankprotect) res = self._command('VR', 'VR([0-9]+)') rs = RadioSetting('VR', 'Firmware Version', RadioSettingValueString(0, 4, res[1])) rs.value.set_mutable(False) main.append(rs) res = self._command('AF', 'AF([01])') rs = RadioSetting('AF', 'Automatic Frequency Control', RadioSettingValueBoolean(int(res[1]))) rs.set_apply_callback( lambda x: self._command('AF{}'.format(int(x.value)))) main.append(rs) res = self._command('AP', 'AP([0-9][.][0-9])') rs = RadioSetting( 'AP', 'Automatic Power Off (hours)', RadioSettingValueFloat(0, 9.5, float(res[1]), 0.5, 1)) rs.set_apply_callback( lambda x: self._command('AP{}'.format(int(x.value)))) main.append(rs) res = self._command('DT', 'DT([01])') rs = RadioSetting('DT', 'Display Frequency Text', RadioSettingValueBoolean(int(res[1]))) rs.set_apply_callback( lambda x: self._command('DT{}'.format(int(x.value)))) main.append(rs) res = self._command('LB', 'LB([0-9]{2})') rs = RadioSetting('LB', 'LCD Contrast', RadioSettingValueInteger(0, 31, res[1])) rs.set_apply_callback( lambda x: self._command('LB{:02d}'.format(int(x.value)))) main.append(rs) # OM supports options for standard, no, and custom message, # but the code here always sets the configured text as a # custom message. res = self._command('OM', 'OM([012]) (.{0,48})') rs = RadioSetting('OM', 'Opening Message', RadioSettingValueString(0, 48, res[2])) rs.set_apply_callback( lambda x: self._command('OM2 {}'.format(str(x.value)))) main.append(rs) res = self._command('PA', 'PA([0-9]{2})') rs = RadioSetting('PA', 'Power Save Delay (seconds)', RadioSettingValueInteger(0, 99, res[1])) rs.set_apply_callback( lambda x: self._command('PA{:02d}'.format(int(x.value)))) main.append(rs) res = self._command('PI', 'PI([1-9].[05])') rs = RadioSetting( 'PI', 'Power Save Interval (seconds)', RadioSettingValueFloat(1.0, 9.5, float(res[1]), 0.5, 1)) rs.set_apply_callback( lambda x: self._command( 'PI{:02d}'.format(int(float(x.value) * 10)))) main.append(rs) res = self._command('PP', 'PP([A-Ja-j])([0-9]{2})') rs = RadioSetting( 'PP', 'Priority Scan Channel', RadioSettingValueInteger( 0, self._SCAN_MEMORIES - 1, self._encode_number('scan', res[1], int(res[2])))) rs.set_apply_callback(lambda x: self._set_pp(int(x.value))) main.append(rs) res = self._command('TI', 'TI([01][0-9])') rs = RadioSetting('TI', 'Priority Interval (seconds)', RadioSettingValueInteger(1, 19, res[1])) rs.set_apply_callback( lambda x: self._command('TI{:02d}'.format(int(x.value)))) main.append(rs) res = self._command('VL', 'VL([0-9])') rs = RadioSetting('VL', 'Beep Volume', RadioSettingValueInteger(0, 9, res[1])) rs.set_apply_callback( lambda x: self._command('VL{}'.format(int(x.value)))) main.append(rs) res = self._command('WP', 'WP([01])') rs = RadioSetting('WP', 'Write Protect', RadioSettingValueBoolean(int(res[1]))) rs.set_apply_callback( lambda x: self._command('WP{}'.format(int(x.value)))) main.append(rs) # vfo settings res = self._command('DA', 'DA[ +]([012][0-9]{2})') rs = RadioSetting('DA', 'Dial Audio Squelch', RadioSettingValueInteger(0, 255, res[1])) rs.set_apply_callback( lambda x: self._command('DA{:03d}'.format(int(x.value)))) vfo.append(rs) res = self._command('DB', 'DB[ +]([012][0-9]{2})') rs = RadioSetting('DB', 'Dial Level Squelch', RadioSettingValueInteger(0, 255, res[1])) rs.set_apply_callback( lambda x: self._command('DB{:03d}'.format(int(x.value)))) vfo.append(rs) res = self._command('DD', 'DD([0-9][.][0-9])') rs = RadioSetting('DDFF', 'Dial Search Hold', RadioSettingValueBoolean(res[1] == 'FF')) vfo.append(rs) value = float(res[1]) if res[1] != 'FF' else 0 rs = RadioSetting('DD', 'Dial Delay (seconds)', RadioSettingValueFloat(0, 9.9, value, 0.1, 1)) vfo.append(rs) res = self._command('DP', 'DP([0-9]{2})') rs = RadioSetting('DP', 'Dial Pause (seconds)', RadioSettingValueInteger(0, 60, res[1])) rs.set_apply_callback( lambda x: self._command('DP{:02d}'.format(int(x.value)))) vfo.append(rs) VT_MAP = [('Off', 0), ('On, auto-store to bank J', 1), ('On, erase bank J', 2)] res = self._command('VT', 'VT([012])') rs = RadioSetting('VT', 'VFO Auto-store', RadioSettingValueMap(VT_MAP, int(res[1]))) rs.set_apply_callback( lambda x: self._command('VT{}'.format(int(x.value)))) vfo.append(rs) # frequency offsets # 0 means off, 20-47 are read-only ol_list = self._get_ol_list() for n in range(0, 48): rs = RadioSetting( 'OL{:02d}'.format(n), 'Offset Frequency {} (MHz)'.format(n), RadioSettingValueFloat( 0.0, 999.9999, ol_list[n] / 1000000, 0.0001, 4)) rs.value.set_mutable(n > 0 and n < 20) rs.set_apply_callback( lambda x, y: self._command( ('OL{:02d} {:010d}' ''.format(y, int(float(x.value) * 1000000)))), n) rs.set_warning("Memories must be refreshed after changing " "offset frequencies.") freq.append(rs) # scan settings grp = RadioSettingSubGroup('scan general', 'Scan General') scan.append(grp) res = self._command('GM', 'GM([0-9])', 2) sel = int(res[1]) rs = RadioSetting('GM', 'Selected Scan Group', RadioSettingValueInteger(0, 9, sel)) rs.set_apply_callback( lambda x: self._command('GM{}'.format(int(x.value)))) grp.append(rs) grp = RadioSettingSubGroup('TB', 'Scan Bank Name') scan.append(grp) for bank in self._SCAN_BANKS: res = self._command('TB{}'.format(bank), 'TB([A-Ja-j])(.{0,8})') rs = RadioSetting('TB{}'.format(bank), 'Bank {} Name'.format(bank), RadioSettingValueString(0, 8, res[2])) rs.set_apply_callback( lambda x, y: self._command( 'TB{}{:8}'.format(y, str(x.value))), bank) grp.append(rs) grp = RadioSettingSubGroup('MW', 'Scan Bank Size') scan.append(grp) MW_MAP = [('10/90', 10), ('20/80', 20), ('30/70', 30), ('40/60', 40), ('50/50', 50), ('60/40', 60), ('70/30', 70), ('80/20', 80), ('90/10', 90)] for bank in self._SCAN_BANKS[::2]: rs = RadioSetting( 'MW{}'.format(bank), 'Bank {}/{}'.format(bank, bank.lower()), RadioSettingValueMap(MW_MAP, self._scan_bank_size(bank))) rs.set_apply_callback( lambda x, y: self._command( 'MW{}{:2d}'.format(y, int(x.value)), timeout=600), bank) rs.set_warning("Resizing scan banks takes a significant amount of " "time (one minute or longer). Memories must be " "refreshed after changing scan bank size.") grp.append(rs) for group in range(10): grp = RadioSettingSubGroup( 'GM{}'.format(group), 'Scan Group {}'.format(group)) scan.append(grp) self._command('GM{}'.format(group)) res = self._command_cached( 'GM{:1d}'.format(group), ('GM(?P[0-9]) XD(?P[0-9][.][0-9]) ' 'XB (?P[012][0-9]{2}) XA (?P