chirp-20261030/ 0000755 0001750 0001750 00000000000 15271057530 012037 5 ustar hibby hibby chirp-20261030/COPYING 0000644 0001750 0001750 00000104513 14411666130 013073 0 ustar hibby hibby 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.in 0000644 0001750 0001750 00000000506 14665027664 013611 0 ustar hibby hibby include 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-INFO 0000644 0001750 0001750 00000000755 15271057530 013143 0 ustar hibby hibby Metadata-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.md 0000644 0001750 0001750 00000000331 14665027664 013326 0 ustar hibby hibby # 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/ 0000755 0001750 0001750 00000000000 15271057527 013152 5 ustar hibby hibby chirp-20261030/chirp/__init__.py 0000644 0001750 0001750 00000000040 15271057526 015254 0 ustar hibby hibby CHIRP_VERSION = "next-20261030"
chirp-20261030/chirp/bandplan.py 0000644 0001750 0001750 00000016656 14767271777 015337 0 ustar hibby hibby # 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.py 0000644 0001750 0001750 00000011211 14720054223 015750 0 ustar hibby hibby # 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.py 0000644 0001750 0001750 00000020025 15141336500 016707 0 ustar hibby hibby # 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.py 0000644 0001750 0001750 00000015044 14720054223 016716 0 ustar hibby hibby # 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.py 0000644 0001750 0001750 00000012532 14720054223 016716 0 ustar hibby hibby # 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.py 0000644 0001750 0001750 00000032562 14720054223 015755 0 ustar hibby hibby # 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.py 0000644 0001750 0001750 00000113611 15242013013 015151 0 ustar hibby hibby # 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.py 0000644 0001750 0001750 00000005575 15067711504 016707 0 ustar hibby hibby # 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.py 0000644 0001750 0001750 00000003514 15161444633 015325 0 ustar hibby hibby # 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.py 0000644 0001750 0001750 00000220223 15257672455 016211 0 ustar hibby hibby # 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/ 0000755 0001750 0001750 00000000000 15271057527 013721 5 ustar hibby hibby chirp-20261030/chirp/cli/__init__.py 0000644 0001750 0001750 00000000000 14411666130 016007 0 ustar hibby hibby chirp-20261030/chirp/cli/experttune.py 0000644 0001750 0001750 00000022210 15017704131 016457 0 ustar hibby hibby #!/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.py 0000644 0001750 0001750 00000034033 15270732464 015221 0 ustar hibby hibby #!/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.py 0000644 0001750 0001750 00000021035 15146031702 015515 0 ustar hibby hibby # 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/ 0000755 0001750 0001750 00000000000 15271057527 014630 5 ustar hibby hibby chirp-20261030/chirp/drivers/__init__.py 0000644 0001750 0001750 00000022046 15271057526 016744 0 ustar hibby hibby from 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.py 0000644 0001750 0001750 00000063641 15266622247 016462 0 ustar hibby hibby # 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.py 0000644 0001750 0001750 00000037364 15172631605 017567 0 ustar hibby hibby # 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.py 0000644 0001750 0001750 00000060554 15161444633 016665 0 ustar hibby hibby # 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.py 0000644 0001750 0001750 00000206726 15172631605 017510 0 ustar hibby hibby # 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.py 0000644 0001750 0001750 00000072331 15172631605 017353 0 ustar hibby hibby # 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.py 0000644 0001750 0001750 00000224267 15257672455 017535 0 ustar hibby hibby # 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.py 0000644 0001750 0001750 00000077534 15172631605 016043 0 ustar hibby hibby # 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.py 0000644 0001750 0001750 00000172437 15250742640 016126 0 ustar hibby hibby # 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[012][0-9]{2}) '
'XP(?P[0-9]{2}) XM(?P[0-8F])'))
if not res:
continue
rs = RadioSetting('XA{}'.format(group), 'Scan Audio Squelch',
RadioSettingValueInteger(0, 255, res['audio']))
rs.value.set_mutable(group != 0)
grp.append(rs)
rs = RadioSetting('XB{}'.format(group), 'Scan Level Squelch',
RadioSettingValueInteger(0, 255, res['level']))
rs.value.set_mutable(group != 0)
grp.append(rs)
rs = RadioSetting(
'XD{}'.format(group), 'Scan Delay (seconds)',
RadioSettingValueFloat(0, 9.9, float(res['delay']), 0.1, 1))
rs.value.set_mutable(group != 0)
grp.append(rs)
rs = RadioSetting('XP{}'.format(group), 'Scan Pause (seconds)',
RadioSettingValueInteger(0, 99, res['pause']))
rs.value.set_mutable(group != 0)
grp.append(rs)
rs = RadioSetting(
'XM{}'.format(group), 'Scan Mode',
RadioSettingValueMap(self._MODES, int(res['mode'], 16)))
rs.value.set_mutable(group != 0)
grp.append(rs)
res = self._command_cached('BM', 'BM ([A-Ja-j-]{20})')
for bank in self._SCAN_BANKS:
rs = RadioSetting(
'BM{}{}'.format(group, bank), 'Bank {}'.format(bank),
RadioSettingValueBoolean(bank in res[1]))
rs.value.set_mutable(group != 0)
grp.append(rs)
self._command('GM{}'.format(sel))
# search settings
grp = RadioSettingSubGroup('search general', 'Search General')
search.append(grp)
res = self._command('GS', 'GS([0-9])', 2)
sel = int(res[1])
rs = RadioSetting('GS', 'Selected Search Group',
RadioSettingValueInteger(0, 9, sel))
rs.set_apply_callback(
lambda x: self._command('GS{}'.format(int(x.value))))
grp.append(rs)
self._save_gs()
for group in range(10):
grp = RadioSettingSubGroup('GS{}'.format(group),
'Search Group {}'.format(group))
search.append(grp)
self._command('GS{}'.format(group))
res = self._command_cached(
'GS{:1d}'.format(group),
('GS(?P[0-9]) SD(?P[0-9][.][0-9]) '
'SB (?P[012][0-9]{2}) SA (?P[012][0-9]{2}) '
'SP(?P[0-9]{2}) AS(?P[012])'))
if not res:
continue
rs = RadioSetting('SA{}'.format(group), 'Search Audio Squelch',
RadioSettingValueInteger(0, 255, res['audio']))
rs.value.set_mutable(group != 0)
grp.append(rs)
rs = RadioSetting('SB{}'.format(group), 'Search Level Squelch',
RadioSettingValueInteger(0, 255, res['level']))
rs.value.set_mutable(group != 0)
grp.append(rs)
if group != 0:
rs = RadioSetting(
'SD{}FF'.format(group), 'Search Hold',
RadioSettingValueBoolean(res['delay'] == 'FF'))
grp.append(rs)
value = float(res['delay']) if res['delay'] != 'FF' else 0
rs = RadioSetting('SD{}'.format(group), 'Search Delay (seconds)',
RadioSettingValueFloat(0, 9.9, value, 0.1, 1))
rs.value.set_mutable(group != 0)
grp.append(rs)
rs = RadioSetting('SP{}'.format(group), 'Search Pause (seconds)',
RadioSettingValueInteger(0, 99, res['pause']))
rs.value.set_mutable(group != 0)
grp.append(rs)
rs = RadioSetting('AS{}'.format(group), 'Search Auto-store',
RadioSettingValueMap(VT_MAP, int(res['auto'])))
rs.value.set_mutable(group != 0)
grp.append(rs)
res = self._command_cached('BS', 'BS ([A-Ta-t-]{40})')
for bank in self._SEARCH_BANKS:
rs = RadioSetting(
'BS{}{}'.format(group, bank), 'Bank {}'.format(bank),
RadioSettingValueBoolean(bank in res[1]))
rs.value.set_mutable(group != 0)
grp.append(rs)
self._restore_gs()
# scan bank protect
for bank in self._SCAN_BANKS[::2]:
res = self._command_read_WM(bank)
for (b, v) in list(zip((bank, bank.lower()), res)):
rs = RadioSetting(
'WM{}'.format(b), 'Scan Bank {} Protect'.format(b),
RadioSettingValueBoolean(v))
rs.set_apply_callback(
lambda x, y: self._command(
'WM{}{}'.format(y, int(x.value))), b)
scanbankprotect.append(rs)
return top
def _apply_settings_callbacks(self, settings):
for element in settings:
if not isinstance(element, RadioSetting):
self._apply_settings_callbacks(element)
elif element.changed() and element.has_apply_callback():
LOG.debug("running callback for {}".format(element))
element.run_apply_callback()
def set_settings(self, settings):
with self.LOCK:
return self._set_settings_unlocked(settings)
def _set_settings_unlocked(self, settings):
self._apply_settings_callbacks(settings)
setdict = {}
for element in settings:
if isinstance(element, RadioSettingGroup):
setdict[element.get_name()] = element
if setdict['vfo']['DDFF'].value.changed() or \
setdict['vfo']['DD'].value.changed():
if int(setdict['vfo']['DDFF'].value):
self._command('DDFF')
else:
v = int(float(setdict['vfo']['DD'].value) * 10)
self._command('DD{:02d}'.format(v))
# BM/BS flags can only be cleared or toggled, so clear all
# first, then toggle the flags which are set
for group in range(1, 10):
parent = setdict['scan']['GM{}'.format(group)]
self._command('GM{}'.format(group))
if parent['XA{}'.format(group)].value.changed():
v = int(parent['XA{}'.format(group)].value)
self._command('XA{:03d}'.format(v))
if parent['XB{}'.format(group)].value.changed():
v = int(parent['XB{}'.format(group)].value)
self._command('XB{:03d}'.format(v))
if parent['XD{}'.format(group)].value.changed():
v = int(float(parent['XD{}'.format(group)].value) * 10)
self._command('XD{:02d}'.format(v))
if parent['XP{}'.format(group)].value.changed():
v = int(parent['XP{}'.format(group)].value)
self._command('XP{:02d}'.format(v))
if parent['XM{}'.format(group)].value.changed():
v = int(parent['XM{}'.format(group)].value)
self._command('XM{:01x}'.format(v))
if any(parent['BM{}{}'.format(group, bank)].value.changed()
for bank in self._SCAN_BANKS):
v = ''
for bank in self._SCAN_BANKS:
if int(parent['BM{}{}'.format(group, bank)].value):
v += bank
self._command('BM%%')
self._command('BM{}'.format(v), 'BM [A-Ja-j-]{20}')
v = int(setdict['scan']['scan general']['GM'].value)
self._command('GM{}'.format(v))
for group in range(1, 10):
parent = setdict['search']['GS{}'.format(group)]
self._command('GS{}'.format(group))
if parent['SA{}'.format(group)].value.changed():
v = int(parent['SA{}'.format(group)].value)
self._command('SA{:03d}'.format(v))
if parent['SB{}'.format(group)].value.changed():
v = int(parent['SB{}'.format(group)].value)
self._command('SB{:03d}'.format(v))
if parent['SD{}FF'.format(group)].value.changed() or \
parent['SD{}'.format(group)].value.changed():
if int(parent['SD{}FF'.format(group)].value):
self._command('SDFF')
else:
v = int(float(parent['SD{}'.format(group)].value) * 10)
self._command('SD{:02d}'.format(v))
if parent['SP{}'.format(group)].value.changed():
v = int(parent['SP{}'.format(group)].value)
self._command('SP{:02d}'.format(v))
if parent['AS{}'.format(group)].value.changed():
v = int(parent['AS{}'.format(group)].value)
self._command('AS{}'.format(v))
if any(parent['BS{}{}'.format(group, bank)].value.changed()
for bank in self._SEARCH_BANKS):
v = ''
for bank in self._SEARCH_BANKS:
if int(parent['BS{}{}'.format(group, bank)].value):
v += bank
self._command('BS%%')
self._command('BS{}'.format(v), 'BS [A-Ta-t-]{40}')
v = int(setdict['search']['search general']['GS'].value)
self._command('GS{}'.format(v))
for n in range(1, 20):
if setdict['freq']['OL{:02d}'.format(n)].value.changed():
self._invalidate_ol_list()
for n in range(self._SCAN_MEMORIES):
self._invalidate_memory(number=n)
for bank in self._SEARCH_BANKS:
self._invalidate_memory('search', bank, None)
break
changed = False
for bank in self._SCAN_BANKS[::2]:
if setdict['scan']['MW']['MW{}'.format(bank)].value.changed():
changed = True
for b in (bank.upper(), bank.lower()):
for n in range(self._scan_bank_size(b)):
self._invalidate_memory('scan', b, n)
if changed:
self._invalidate_scan_bank_size()
chirp-20261030/chirp/drivers/baofeng_common.py 0000644 0001750 0001750 00000071172 15172631605 020156 0 ustar hibby hibby # Copyright 2016:
# * Jim Unroe KC9HI,
#
# 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 .
"""common functions for Baofeng (or similar) handheld radios"""
import time
import struct
import logging
from chirp import chirp_common, memmap
from chirp import errors, util
from chirp import bandplan_na
from chirp.settings import RadioSettingGroup, RadioSetting, \
RadioSettingValueBoolean, InternalError, \
RadioSettingValueList
LOG = logging.getLogger(__name__)
STIMEOUT = 1.5
TXPOWER_HIGH = 0x00
TXPOWER_LOW = 0x02
def _clean_buffer(radio):
radio.pipe.timeout = 0.005
junk = radio.pipe.read(256)
radio.pipe.timeout = STIMEOUT
if junk:
LOG.debug("Got %i bytes of junk before starting" % len(junk))
def _rawrecv(radio, amount):
"""Raw read from the radio device"""
data = ""
try:
data = radio.pipe.read(amount)
except:
msg = "Generic error reading data from radio; check your cable."
raise errors.RadioError(msg)
if not data:
LOG.debug('No response from radio')
raise errors.RadioNoContactLikelyK1()
elif len(data) != amount:
LOG.debug('Wanted %i, got %i: %s',
amount, len(data), util.hexprint(data))
msg = "Error reading data from radio: not the amount of data we want."
raise errors.RadioError(msg)
return data
def _rawsend(radio, data):
"""Raw send to the radio device"""
try:
radio.pipe.write(data)
except:
raise errors.RadioError("Error sending data to radio")
def _make_frame(cmd, addr, length, data=""):
"""Pack the info in the header format"""
frame = struct.pack(">BHB", ord(cmd), addr, length)
# add the data if set
if len(data) != 0:
frame += data
# return the data
return frame
def _recv(radio, addr, length):
"""Get data from the radio """
# read 4 bytes of header
hdr = _rawrecv(radio, 4)
# read data
data = _rawrecv(radio, length)
# DEBUG
LOG.info("Response:")
LOG.debug(util.hexprint(hdr + data))
c, a, l = struct.unpack(">BHB", hdr)
if a != addr or l != length or c != ord("X"):
LOG.error("Invalid answer for block 0x%04x:" % addr)
LOG.debug("CMD: %s ADDR: %04x SIZE: %02x" % (c, a, l))
raise errors.RadioError("Unknown response from the radio")
return data
def _read_from_data(data, data_start, data_stop):
data = data[data_start:data_stop]
return data
def _get_data_from_image(radio, _data_start, _data_stop):
image_data = _read_from_data(
radio.get_mmap().get_byte_compatible(),
_data_start,
_data_stop)
return image_data
def _read_block(radio, start, size, first_command=False):
msg = struct.pack(">BHB", ord("S"), start, size)
radio.pipe.write(msg)
if radio._ack_block and first_command is False:
ack = radio.pipe.read(1)
if ack != b"\x06":
raise errors.RadioError(
"Radio refused to send second block 0x%04x" % start)
answer = radio.pipe.read(4)
if len(answer) != 4:
raise errors.RadioError("Radio refused to send block 0x%04x" % start)
cmd, addr, length = struct.unpack(">BHB", answer)
if cmd != ord("X") or addr != start or length != size:
LOG.error("Invalid answer for block 0x%04x:" % start)
LOG.debug("CMD: %s ADDR: %04x SIZE: %02x" % (cmd, addr, length))
raise errors.RadioError("Unknown response from radio")
chunk = radio.pipe.read(size)
if not chunk:
raise errors.RadioError("Radio did not send block 0x%04x" % start)
elif len(chunk) != size:
LOG.error("Chunk length was 0x%04i" % len(chunk))
raise errors.RadioError("Radio sent incomplete block 0x%04x" % start)
radio.pipe.write(b"\x06")
time.sleep(0.05)
return chunk
def _get_aux_data_from_radio(radio):
block0 = _read_block(radio, 0x1E80, 0x40, True)
block1 = _read_block(radio, 0x1EC0, 0x40, False)
block2 = _read_block(radio, 0x1F00, 0x40, False)
block3 = _read_block(radio, 0x1F40, 0x40, False)
block4 = _read_block(radio, 0x1F80, 0x40, False)
block5 = _read_block(radio, 0x1FC0, 0x40, False)
version = block1[48:62]
area1 = block2 + block3[0:32]
area2 = block3[48:64]
area3 = block4[16:64]
# check for dropped byte
dropped_byte = block5[15:16] == b"\xFF" # True/False
return version, area1, area2, area3, dropped_byte
def _get_radio_firmware_version(radio):
# New radios will reply with 'alternative' (aka: bad) data if the Aux
# memory area is read without first reading a block from another area
# of memory. Reading any block that is outside of the Aux memory area
# (which starts at 0x1EC0) prior to reading blocks in the Aux mem area
# turns out to be a workaround for this problem.
# read and disregard block0
block0 = _read_block(radio, 0x1E80, 0x40, True)
block1 = _read_block(radio, 0x1EC0, 0x40, False)
block2 = _read_block(radio, 0x1FC0, 0x40, False)
# get firmware version
version = block1[48:62] # firmware version
# Some new radios will drop the byte at 0x1FCF when read in 0x40 byte
# blocks. This results in the next 0x30 bytes being moved forward one
# position (putting 0xFF in position 0x1FCF and pads the now missing byte
# at the end, 0x1FFF, with 0x80).
# detect dropped byte
dropped_byte = (block2[15:16] == b"\xFF") # dropped byte?
return version, dropped_byte
def _image_ident_from_data(data, start, stop):
return data[start:stop]
def _get_image_firmware_version(radio):
return _image_ident_from_data(radio.get_mmap(), radio._fw_ver_start,
radio._fw_ver_start + 0x0E)
def _do_ident(radio, magic):
"""Put the radio in PROGRAM mode"""
# set the serial discipline
radio.pipe.baudrate = 9600
radio.pipe.parity = "N"
radio.pipe.timeout = STIMEOUT
# flush input buffer
_clean_buffer(radio)
# send request to enter program mode
_rawsend(radio, magic)
ack = _rawrecv(radio, 1)
if not ack:
raise errors.RadioNoResponse()
if ack != b"\x06":
if ack:
LOG.debug(repr(ack))
raise errors.RadioError("Unexpected response from radio")
_rawsend(radio, b"\x02")
# Ok, get the response
ident = _rawrecv(radio, radio._magic_response_length)
# check if response is OK
if not ident.startswith(b"\xaa") or not ident.endswith(b"\xdd"):
# bad response
msg = "Unexpected response, got this:"
msg += util.hexprint(ident)
LOG.debug(msg)
raise errors.RadioError("Unexpected response from radio.")
# DEBUG
LOG.info("Valid response, got this:")
LOG.debug(util.hexprint(ident))
_rawsend(radio, b"\x06")
ack = _rawrecv(radio, 1)
if ack != b"\x06":
if ack:
LOG.debug(repr(ack))
raise errors.RadioError("Radio refused clone")
return ident
def _ident_radio(radio):
for magic in radio._magic:
error = None
try:
data = _do_ident(radio, magic)
return data
except errors.RadioError as e:
print(e)
error = e
time.sleep(2)
if error:
raise error
raise errors.RadioNoResponse()
def _download(radio):
"""Get the memory map"""
# put radio in program mode
ident = _ident_radio(radio)
# identify radio
radio_ident, has_dropped_byte = _get_radio_firmware_version(radio)
LOG.info("Radio firmware version:")
LOG.debug(util.hexprint(radio_ident))
LOG.info("Radio has dropped byte issue: %s" % repr(has_dropped_byte))
if radio_ident == "\xFF" * 16:
ident += radio.MODEL.ljust(8)
elif radio.MODEL in ("GMRS-V1", "GMRS-V2", "MURS-V1", "MURS-V2"):
# check if radio_ident is OK
if not radio_ident[:7] in radio._fileid:
msg = "Incorrect model ID, got this:\n\n"
msg += util.hexprint(radio_ident)
LOG.debug(msg)
raise errors.RadioError("Incorrect 'Model' selected.")
# UI progress
status = chirp_common.Status()
status.cur = 0
status.max = radio._mem_size // radio._recv_block_size
status.msg = "Cloning from radio..."
radio.status_fn(status)
start = 0
end = radio._mem_size
blocksize = radio._recv_block_size
passes = 1
if has_dropped_byte:
passes = 2
end = radio._mem_size - radio._recv_block_size
data = b""
for i in range(0, passes):
if i == 1:
start = radio._mem_size - radio._recv_block_size
end = radio._mem_size
blocksize = 0x10
for addr in range(start, end, blocksize):
frame = _make_frame("S", addr, blocksize)
# DEBUG
LOG.info("Request sent:")
LOG.debug(util.hexprint(frame))
# sending the read request
_rawsend(radio, frame)
if radio._ack_block:
ack = _rawrecv(radio, 1)
if ack != b"\x06":
raise errors.RadioError(
"Radio refused to send block 0x%04x" % addr)
# now we read
d = _recv(radio, addr, blocksize)
_rawsend(radio, b"\x06")
time.sleep(0.05)
# aggregate the data
data += d
# UI Update
status.cur = addr // blocksize
status.msg = "Cloning from radio..."
radio.status_fn(status)
data += ident
return data
def _upload(radio):
"""Upload procedure"""
# put radio in program mode
_ident_radio(radio)
# identify radio
radio_ident, aux_r1, aux_r2, aux_r3, \
has_dropped_byte = _get_aux_data_from_radio(radio)
LOG.info("Radio firmware version:")
LOG.debug(util.hexprint(radio_ident))
LOG.info("Radio has dropped byte issue: %s" % repr(has_dropped_byte))
# identify image
image_ident = _get_image_firmware_version(radio)
LOG.info("Image firmware version:")
LOG.debug(util.hexprint(image_ident))
if radio.MODEL in ("GMRS-V1", "MURS-V1"):
# check if radio_ident is OK
if radio_ident != image_ident:
msg = "Incorrect model ID, got this:\n\n"
msg += util.hexprint(radio_ident)
LOG.debug(msg)
raise errors.RadioError("Image not supported by radio")
aux_i1 = _get_data_from_image(radio, 0x1F00, 0x1F60)
aux_i2 = _get_data_from_image(radio, 0x1F70, 0x1F80)
aux_i3 = _get_data_from_image(radio, 0x1F90, 0x1FC0)
# check if Aux memory of image matches Aux memory of radio
aux_matched = False
if aux_i1 != aux_r1:
# Area 1 does not match
# The safest thing to do is to skip uploading Aux mem area.
LOG.info("Aux memory mismatch")
LOG.info("Aux area 1 from image is %s" % repr(aux_i1))
LOG.info("Aux area 1 from radio is %s" % repr(aux_r1))
elif aux_i2 != aux_r2:
# Area 2 does not match
# The safest thing to do is to skip uploading Aux mem area.
LOG.info("Aux memory mismatch")
LOG.info("Aux area 2 from image is %s" % repr(aux_i2))
LOG.info("Aux area 2 from radio is %s" % repr(aux_r2))
elif aux_i3 != aux_r3:
# Area 3 does not match
# The safest thing to do is to skip uploading Aux mem area.
LOG.info("Aux memory mismatch")
LOG.info("Aux area 3 from image is %s" % repr(aux_i3))
LOG.info("Aux area 3 from radio is %s" % repr(aux_r3))
else:
# All areas matched
# Uploading full Aux mem area is permitted
aux_matched = True
if has_dropped_byte and not aux_matched:
msg = ("Image not supported by radio. You must...\n"
"1. Download from radio.\n"
"2. Make changes.\n"
"3. Upload back to same radio.")
raise errors.RadioError(msg)
if has_dropped_byte or (aux_matched and radio.VENDOR != "BTECH"):
_ranges = [(0x0000, 0x0DF0),
(0x0E00, 0x1800),
(0x1E80, 0x2000),
]
else:
_ranges = radio._ranges
# UI progress
status = chirp_common.Status()
status.cur = 0
status.max = radio._mem_size // radio._send_block_size
status.msg = "Cloning to radio..."
radio.status_fn(status)
# the fun start here
for start, end in _ranges:
for addr in range(start, end, radio._send_block_size):
# sending the data
data = radio.get_mmap()[addr:addr + radio._send_block_size]
frame = _make_frame("X", addr, radio._send_block_size, data)
_rawsend(radio, frame)
time.sleep(0.05)
# receiving the response
ack = _rawrecv(radio, 1)
if ack != b"\x06":
msg = "Bad ack writing block 0x%04x" % addr
raise errors.RadioError(msg)
# UI Update
status.cur = addr // radio._send_block_size
status.msg = "Cloning to radio..."
radio.status_fn(status)
def bcd_decode_freq(bytes):
real_freq = 0
for byte in bytes:
if byte > 9:
msg = ("Found Invalid BCD Encoding")
raise InternalError(msg)
real_freq = (real_freq * 10) + byte
return chirp_common.format_freq(real_freq * 10)
class BaofengCommonHT(chirp_common.CloneModeRadio,
chirp_common.ExperimentalRadio):
"""Baofeng HT Style Radios"""
VENDOR = "Baofeng"
MODEL = ""
IDENT = ""
_gmrs = False
_bw_shift = False
_tri_band = False
def sync_in(self):
"""Download from radio"""
try:
data = _download(self)
except errors.RadioError:
# Pass through any real errors we raise
raise
except:
# If anything unexpected happens, make sure we raise
# a RadioError and log the problem
LOG.exception('Unexpected error during download')
raise errors.RadioError('Unexpected error communicating '
'with the radio')
self._mmap = memmap.MemoryMapBytes(data)
self.process_mmap()
def sync_out(self):
"""Upload to radio"""
try:
_upload(self)
except errors.RadioError:
raise
except Exception:
# If anything unexpected happens, make sure we raise
# a RadioError and log the problem
LOG.exception('Unexpected error during upload')
raise errors.RadioError('Unexpected error communicating '
'with the radio')
def get_features(self):
"""Get the radio's features"""
rf = chirp_common.RadioFeatures()
rf.has_settings = True
rf.has_bank = False
rf.has_tuning_step = False
rf.can_odd_split = True
rf.has_name = True
rf.has_offset = True
rf.has_mode = True
rf.has_dtcs = True
rf.has_rx_dtcs = True
rf.has_dtcs_polarity = True
rf.has_ctone = True
rf.has_cross = True
rf.valid_modes = self.MODES
rf.valid_characters = self.VALID_CHARS
rf.valid_name_length = self.LENGTH_NAME
if self._gmrs:
rf.valid_duplexes = ["", "+", "off"]
else:
rf.valid_duplexes = ["", "-", "+", "split", "off"]
rf.valid_tmodes = ['', 'Tone', 'TSQL', 'DTCS', 'Cross']
rf.valid_cross_modes = [
"Tone->Tone",
"DTCS->",
"->DTCS",
"Tone->DTCS",
"DTCS->Tone",
"->Tone",
"DTCS->DTCS"]
rf.valid_skips = self.SKIP_VALUES
rf.valid_dtcs_codes = self.DTCS_CODES
rf.memory_bounds = (0, 127)
rf.valid_power_levels = self.POWER_LEVELS
rf.valid_bands = self.VALID_BANDS
rf.valid_tuning_steps = [2.5, 5.0, 6.25, 10.0, 12.5, 20.0, 25.0, 50.0]
return rf
def _is_txinh(self, _mem):
raw_tx = b""
for i in range(0, 4):
raw_tx += _mem.txfreq[i].get_raw()
return raw_tx in (b"\xFF\xFF\xFF\xFF", b"\x00\x00\x00\x00")
def get_memory(self, number):
_mem = self._memobj.memory[number]
_nam = self._memobj.names[number]
mem = chirp_common.Memory()
mem.number = number
if _mem.get_raw()[:1] == b"\xff":
mem.empty = True
return mem
mem.freq = int(_mem.rxfreq) * 10
if self._is_txinh(_mem):
# TX freq not set
mem.duplex = "off"
mem.offset = 0
else:
# TX freq set
offset = (int(_mem.txfreq) * 10) - mem.freq
if offset != 0:
if chirp_common.is_split(self.get_features().valid_bands,
mem.freq, int(_mem.txfreq) * 10):
mem.duplex = "split"
mem.offset = int(_mem.txfreq) * 10
elif offset < 0:
mem.offset = abs(offset)
mem.duplex = "-"
elif offset > 0:
mem.offset = offset
mem.duplex = "+"
else:
mem.offset = 0
for char in _nam.name:
if str(char) == "\xFF":
char = " " # The OEM software may have 0xFF mid-name
mem.name += str(char)
mem.name = mem.name.rstrip()
dtcs_pol = ["N", "N"]
if _mem.txtone in [0, 0xFFFF]:
txmode = ""
elif _mem.txtone >= 0x0258:
txmode = "Tone"
mem.rtone = int(_mem.txtone) / 10.0
elif _mem.txtone <= 0x0258:
txmode = "DTCS"
if _mem.txtone > 0x69:
index = _mem.txtone - 0x6A
dtcs_pol[0] = "R"
else:
index = _mem.txtone - 1
mem.dtcs = self.DTCS_CODES[index]
else:
LOG.warn("Bug: txtone is %04x" % _mem.txtone)
if _mem.rxtone in [0, 0xFFFF]:
rxmode = ""
elif _mem.rxtone >= 0x0258:
rxmode = "Tone"
mem.ctone = int(_mem.rxtone) / 10.0
elif _mem.rxtone <= 0x0258:
rxmode = "DTCS"
if _mem.rxtone >= 0x6A:
index = _mem.rxtone - 0x6A
dtcs_pol[1] = "R"
else:
index = _mem.rxtone - 1
mem.rx_dtcs = self.DTCS_CODES[index]
else:
LOG.warn("Bug: rxtone is %04x" % _mem.rxtone)
if txmode == "Tone" and not rxmode:
mem.tmode = "Tone"
elif txmode == rxmode and txmode == "Tone" and mem.rtone == mem.ctone:
mem.tmode = "TSQL"
elif txmode == rxmode and txmode == "DTCS" and mem.dtcs == mem.rx_dtcs:
mem.tmode = "DTCS"
elif rxmode or txmode:
mem.tmode = "Cross"
mem.cross_mode = "%s->%s" % (txmode, rxmode)
mem.dtcs_polarity = "".join(dtcs_pol)
if not _mem.scan:
mem.skip = "S"
levels = self.POWER_LEVELS
if self._tri_band:
if _mem.lowpower == TXPOWER_HIGH:
mem.power = levels[0]
elif _mem.lowpower == TXPOWER_LOW:
mem.power = levels[1]
else:
LOG.error("Radio reported invalid power level %s (in %s)" %
(_mem.power, levels))
mem.power = levels[0]
else:
try:
mem.power = levels[_mem.lowpower]
except IndexError:
LOG.error("Radio reported invalid power level %s (in %s)" %
(_mem.power, levels))
mem.power = levels[0]
mem.mode = _mem.wide and "FM" or "NFM"
mem.extra = RadioSettingGroup("Extra", "extra")
rs = RadioSetting("bcl", "BCL",
RadioSettingValueBoolean(_mem.bcl))
mem.extra.append(rs)
rs = RadioSetting("pttid", "PTT ID",
RadioSettingValueList(self.PTTID_LIST,
current_index=_mem.pttid))
mem.extra.append(rs)
rs = RadioSetting("scode", "S-CODE",
RadioSettingValueList(self.SCODE_LIST,
current_index=_mem.scode))
mem.extra.append(rs)
immutable = []
if self._gmrs:
if self.MODEL == "UV-9G":
if mem.number >= 1 and mem.number <= 30:
# 30 GMRS fixed channels
GMRS_30_FIXED_FREQS = \
bandplan_na.ALL_GMRS_FREQS + \
bandplan_na.GMRS_HIRPT
GMRS_FREQ = GMRS_30_FIXED_FREQS[mem.number - 1]
mem.freq = GMRS_FREQ
immutable = ["empty", "freq"]
if mem.number <= 22:
# GMRS simplex only channels
mem.duplex = ''
mem.offset = 0
immutable += ["duplex", "offset"]
if mem.number >= 8 and mem.number <= 14:
# GMRS 467 MHz interstitial channels
# must be narrow FM and low power
mem.mode = "NFM"
mem.power = self.POWER_LEVELS[2]
immutable += ["mode", "power"]
if mem.number > 22:
# GMRS repeater only channels
mem.duplex = '+'
mem.offset = 5000000
immutable += ["duplex", "offset"]
elif mem.freq in bandplan_na.ALL_GMRS_FREQS:
# 98 GMRS customizable channels
if mem.freq in bandplan_na.GMRS_LOW:
# GMRS 462 MHz interstitial frequencies
mem.duplex = ''
mem.offset = 0
immutable = ["duplex", "offset"]
if mem.freq in bandplan_na.GMRS_HHONLY:
# GMRS 467 MHz interstitial frequencies
mem.duplex = ''
mem.offset = 0
mem.mode = "NFM"
mem.power = self.POWER_LEVELS[2]
immutable = ["duplex", "offset", "mode", "power"]
if mem.freq in bandplan_na.GMRS_HIRPT:
# GMRS 462 MHz main frequencies
# GMRS 467 MHz main frequencies (repeater input)
if mem.duplex == '':
mem.offset = 0
if mem.duplex == '+':
mem.offset = 5000000
else:
# Not a GMRS frequency - disable TX
mem.duplex = 'off'
mem.offset = 0
immutable = ["duplex", "offset"]
mem.immutable = immutable
return mem
def set_memory(self, mem):
_mem = self._memobj.memory[mem.number]
_nam = self._memobj.names[mem.number]
if mem.empty:
_mem.set_raw("\xff" * 16)
_nam.set_raw("\xff" * 16)
return
_mem.set_raw("\x00" * 16)
_mem.rxfreq = mem.freq / 10
if mem.duplex == "off":
for i in range(0, 4):
_mem.txfreq[i].set_raw("\xFF")
elif mem.duplex == "split":
_mem.txfreq = mem.offset / 10
elif mem.duplex == "+":
_mem.txfreq = (mem.freq + mem.offset) / 10
elif mem.duplex == "-":
_mem.txfreq = (mem.freq - mem.offset) / 10
else:
_mem.txfreq = mem.freq / 10
_namelength = self.get_features().valid_name_length
for i in range(_namelength):
try:
_nam.name[i] = mem.name[i]
except IndexError:
_nam.name[i] = "\xFF"
rxmode = txmode = ""
if mem.tmode == "Tone":
_mem.txtone = int(mem.rtone * 10)
_mem.rxtone = 0
elif mem.tmode == "TSQL":
_mem.txtone = int(mem.ctone * 10)
_mem.rxtone = int(mem.ctone * 10)
elif mem.tmode == "DTCS":
rxmode = txmode = "DTCS"
_mem.txtone = self.DTCS_CODES.index(mem.dtcs) + 1
_mem.rxtone = self.DTCS_CODES.index(mem.dtcs) + 1
elif mem.tmode == "Cross":
txmode, rxmode = mem.cross_mode.split("->", 1)
if txmode == "Tone":
_mem.txtone = int(mem.rtone * 10)
elif txmode == "DTCS":
_mem.txtone = self.DTCS_CODES.index(mem.dtcs) + 1
else:
_mem.txtone = 0
if rxmode == "Tone":
_mem.rxtone = int(mem.ctone * 10)
elif rxmode == "DTCS":
_mem.rxtone = self.DTCS_CODES.index(mem.rx_dtcs) + 1
else:
_mem.rxtone = 0
else:
_mem.rxtone = 0
_mem.txtone = 0
if txmode == "DTCS" and mem.dtcs_polarity[0] == "R":
_mem.txtone += 0x69
if rxmode == "DTCS" and mem.dtcs_polarity[1] == "R":
_mem.rxtone += 0x69
_mem.scan = mem.skip != "S"
_mem.wide = mem.mode == "FM"
if self._tri_band:
levels = self.POWER_LEVELS
if mem.power is None:
_mem.lowpower = TXPOWER_HIGH
elif mem.power == levels[0]:
_mem.lowpower = TXPOWER_HIGH
elif mem.power == levels[1]:
_mem.lowpower = TXPOWER_LOW
else:
_mem.lowpower = TXPOWER_HIGH
else:
if mem.power:
_mem.lowpower = self.POWER_LEVELS.index(mem.power)
else:
_mem.lowpower = 0
# extra settings
if len(mem.extra) > 0:
# there are setting, parse
for setting in mem.extra:
setattr(_mem, setting.get_name(), setting.value)
else:
# there are no extra settings, load defaults
_mem.bcl = 0
_mem.pttid = 0
_mem.scode = 0
def set_settings(self, settings):
_settings = self._memobj.settings
_mem = self._memobj
for element in settings:
if not isinstance(element, RadioSetting):
if element.get_name() == "fm_preset":
self._set_fm_preset(element)
else:
self.set_settings(element)
continue
else:
try:
name = element.get_name()
if "." in name:
bits = name.split(".")
obj = self._memobj
for bit in bits[:-1]:
if "/" in bit:
bit, index = bit.split("/", 1)
index = int(index)
obj = getattr(obj, bit)[index]
else:
obj = getattr(obj, bit)
setting = bits[-1]
else:
obj = _settings
setting = element.get_name()
if element.has_apply_callback():
LOG.debug("Using apply callback")
element.run_apply_callback()
elif element.value.get_mutable():
LOG.debug("Setting %s = %s" % (setting, element.value))
setattr(obj, setting, element.value)
except Exception:
LOG.debug(element.get_name())
raise
def _set_fm_preset(self, settings):
for element in settings:
try:
val = element.value
if self._memobj.fm_presets <= 108.0 * 10 - 650:
value = int(val.get_value() * 10 - 650)
else:
value = int(val.get_value() * 10)
LOG.debug("Setting fm_presets = %s" % (value))
if self._bw_shift:
value = ((value & 0x00FF) << 8) | ((value & 0xFF00) >> 8)
self._memobj.fm_presets = value
except Exception:
LOG.debug(element.get_name())
raise
chirp-20261030/chirp/drivers/baofeng_digital.py 0000644 0001750 0001750 00000045147 15250742640 020305 0 ustar hibby hibby # Copyright 2024 Marcos Del Sol Vives
#
# 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 .
from collections import namedtuple
import logging
import struct
import time
from chirp import chirp_common, directory, errors, memmap
from chirp import bitwise
from chirp.drivers import baofeng_common
from chirp.settings import (RadioSetting, RadioSettingGroup,
RadioSettingValueBoolean,
RadioSettingValueList)
LOG = logging.getLogger(__name__)
CMD_ACK = b'\x06'
CMD_READ = b'R'
CMD_WRITE = b'W'
CMD_EXIT = b'E'
MemoryChunk = namedtuple('MemoryChunk', ['start', 'end', 'read_only'])
ProtoVersion = namedtuple('ProtoVersion',
['chan_count', 'serial_magic', 'mem_chunks'])
PROTO_A = ProtoVersion(
chan_count=16,
serial_magic=b'\x80\x82',
mem_chunks=[
MemoryChunk(start=0x000, end=0x10F, read_only=False),
MemoryChunk(start=0x2B0, end=0x2BF, read_only=False),
MemoryChunk(start=0x330, end=0x33F, read_only=True),
MemoryChunk(start=0x380, end=0x3DF, read_only=False),
],
)
PROTO_B = ProtoVersion(
chan_count=16,
serial_magic=b'\x80\x82',
mem_chunks=PROTO_A.mem_chunks,
)
PROTO_C = ProtoVersion(
chan_count=16,
serial_magic=b'\x20\x22',
mem_chunks=PROTO_A.mem_chunks,
)
PROTO_D = ProtoVersion(
chan_count=30,
serial_magic=b'\x80\x82',
mem_chunks=[
MemoryChunk(start=0x000, end=0x1EF, read_only=False),
MemoryChunk(start=0x2B0, end=0x2BF, read_only=False),
MemoryChunk(start=0x330, end=0x33F, read_only=True),
MemoryChunk(start=0x380, end=0x3DF, read_only=False),
],
)
class BaofengDigital(chirp_common.CloneModeRadio,
chirp_common.ExperimentalRadio):
"""Base for all digital radios from Fujian Baofeng Electronics Co. Ltd."""
VENDOR = "Baofeng"
BAUD_RATE = 9600
MEM_FORMAT = """
#seekto 0x0010;
struct {{
lbcd rx_freq[4];
lbcd tx_freq[4];
lbcd rx_tone[2];
lbcd tx_tone[2];
u8 hopping:1,
unused1:2,
scan:1,
high_power:1,
narrow:1,
digital:1,
bcl_disabled:1;
u8 unused2:3,
encryption_key:5;
u16 unused3;
}} memory[{chan_count}];
"""
_PREAMBLE_CMDS = [
(b'\x02', b'P3107\xf7\x00\x00'),
(CMD_ACK, CMD_ACK),
(b'R\x01\x30\x08', b'W\x01\x30\x08\xff\xff\xff\xff\xff\xff\xff\xff'),
(CMD_ACK, CMD_ACK),
]
_WRITE_PREAMBLE_CMDS = [
(b'E', b'F'),
(b'\x02\x80\x82OGRAM', CMD_ACK),
(b'\x02', b'P3107\xf7\x00\x00'),
(CMD_ACK, CMD_ACK),
]
_VOICES_PER_REGION = {
0x00: ('English', 'Chinese'),
0x01: ('Chinese', 'Russian'),
0x02: ('Russian', 'English'),
0x03: ('Arabic', 'English'),
0x04: ('Arabic', 'Chinese'),
0x05: ('Arabic', 'Russian'),
0x06: ('Chinese', 'Portuguese'),
0x07: ('English', 'Portuguese'),
0x08: ('Chinese', 'Spanish'),
0x09: ('English', 'Spanish'),
0x0A: ('Chinese', 'German'),
0x0B: ('English', 'German'),
0x0C: ('Chinese', 'French'),
0x0D: ('English', 'French'),
}
_ENC_OPTS = ['None (analog)'] + [str(x) for x in range(32)]
@property
def _memsize(self):
return self._proto.mem_chunks[-1].end + 1
def get_features(self):
rf = chirp_common.RadioFeatures()
rf.has_rx_dtcs = True
rf.has_bank = False
rf.has_name = False
rf.has_tuning_step = False
rf.can_odd_split = True
rf.has_cross = True
rf.memory_bounds = (1, self._proto.chan_count)
rf.valid_bands = self.VALID_BANDS
rf.valid_duplexes = ["", "+", "-", "split"]
rf.valid_modes = ['FM', 'NFM']
rf.valid_power_levels = self.POWER_LEVELS
rf.valid_tuning_steps = [2.5, 6.25, 5.0]
rf.valid_tmodes = ["", "Tone", "TSQL", "DTCS", "Cross"]
rf.valid_cross_modes = [
"Tone->Tone",
"Tone->DTCS",
"DTCS->Tone",
"DTCS->",
"->Tone",
"->DTCS",
"DTCS->DTCS",
]
return rf
def _get_radio_id(self):
for attempt in range(5):
try:
baofeng_common._clean_buffer(self)
self.pipe.write(b'\x02prOGRAM')
resp = self.pipe.read(4)
if len(resp) != 4:
raise errors.RadioError(
'Radio did not send identification')
return resp
except errors.RadioError:
# Don't wait if already last attempt
if attempt == 4:
raise
self._exit_programming()
time.sleep(1)
def _enter_programming(self, write):
radio_id = self._get_radio_id()
if radio_id[1] != self._serial_id:
raise errors.RadioError('Detected a different radio model')
self._metadata['fw_region'] = radio_id[0]
self._metadata['fw_version'] = radio_id[2] * 0x100 + radio_id[3]
# Issue protocol-specific command
self.pipe.write(b'\x02' + self._proto.serial_magic + b'OGRAM')
if self.pipe.read(1) != CMD_ACK:
raise errors.RadioError('No ACK on protocol-specific command')
# Issue fixed commands and check their values
if write:
preamble = self._PREAMBLE_CMDS + self._WRITE_PREAMBLE_CMDS
else:
preamble = self._PREAMBLE_CMDS
for step, cmd in enumerate(preamble, 1):
self.pipe.write(cmd[0])
if self.pipe.read(len(cmd[1])) != cmd[1]:
raise errors.RadioError(
f'Radio replied incorrectly to preamble step {step}')
def _clean_buffer(self):
self.pipe.timeout = 0.001
self.pipe.read(256)
self.pipe.timeout = 0.1
def _exit_programming(self):
try:
self.pipe.write(CMD_EXIT)
except Exception:
pass
def sync_in(self):
try:
self._enter_programming(False)
memory = memmap.MemoryMapBytes(b"\xFF" * self._memsize)
for chunk in self._proto.mem_chunks:
for addr in range(chunk.start, chunk.end, 8):
self.pipe.write(struct.pack('>cHB', CMD_READ, addr, 8))
reply = self.pipe.read(12)
if len(reply) != 12:
raise errors.RadioError(
f'Failed to read block at 0x{addr:03x}')
self.pipe.write(CMD_ACK)
if self.pipe.read(1) != CMD_ACK:
raise errors.RadioError(
f'No ACK after reading from 0x{addr:03x}')
memory.set(addr, reply[4:])
except errors.RadioError:
raise
except Exception as e:
raise errors.RadioError(
f'Failed to communicate with the radio: {e}')
finally:
self._exit_programming()
self._mmap = memory
self.process_mmap()
def sync_out(self):
try:
self._enter_programming(True)
for chunk in self._proto.mem_chunks:
if chunk.read_only:
continue
for addr in range(chunk.start, chunk.end, 8):
frame = (
struct.pack('>cHB', CMD_WRITE, addr, 8) +
self._mmap.get(addr, 8)
)
self.pipe.write(frame)
if self.pipe.read(1) != CMD_ACK:
raise errors.RadioError(
f'Not ACK after writing to 0x{addr:03x}')
except errors.RadioError:
raise
except Exception as e:
raise errors.RadioError(
f'Failed to communicate with the radio: {e}')
finally:
self._exit_programming()
def process_mmap(self):
mem_fmt = self.MEM_FORMAT.format(chan_count=self._proto.chan_count)
self._memobj = bitwise.parse(mem_fmt, self._mmap)
def get_raw_memory(self, number):
return repr(self._memobj.memory[number])
def _get_squelch(self, elem):
# Get raw and check if unset
if elem.get_raw() in (b'\x00\x00', b'\xFF\xFF'):
return ('', None, 'N')
# Get flags, unset them so we can parse as BCD, then restore them
flags = elem[1].get_bits(0xC0)
elem[1].clr_bits(0xC0)
value = int(elem)
elem[1].set_bits(flags)
# DTCS if bit 15 is set
if flags & 0x80:
return ('DTCS', value, flags & 0x40 and 'R' or 'N')
else:
# Parse as CTCSS frequency * 10
return ('Tone', value / 10.0, 'N')
def get_memory(self, number):
raw = self._memobj.memory[number - 1]
mem = chirp_common.Memory()
mem.number = number
mem.empty = raw.rx_freq.get_raw() in (
b'\x00\x00\x00\x00', b'\xFF\xFF\xFF\xFF'
)
bcl = hopping = False
enc_idx = 0
if not mem.empty:
rx_freq = int(raw.rx_freq) * 10
tx_freq = int(raw.tx_freq) * 10
mem.freq = rx_freq
offset = tx_freq - rx_freq
if offset == 0:
mem.duplex = ''
mem.offset = 0
elif abs(offset) < 10000000:
mem.duplex = '-' if offset < 0 else '+'
mem.offset = abs(offset)
else:
mem.duplex = 'split'
mem.offset = tx_freq
mem.mode = raw.narrow and 'NFM' or 'FM'
if raw.digital:
mem.tmode = ''
mem.dtcs_polarity = 'NN'
enc_idx = raw.encryption_key + 1
else:
hopping = raw.hopping
tx_tmode, tx_tvalue, tx_tpol = self._get_squelch(raw.tx_tone)
if tx_tmode == 'Tone':
mem.rtone = tx_tvalue
elif tx_tmode == 'DTCS':
mem.dtcs = tx_tvalue
rx_tmode, rx_tvalue, rx_tpol = self._get_squelch(raw.rx_tone)
if rx_tmode == 'Tone':
mem.ctone = rx_tvalue
elif rx_tmode == 'DTCS':
mem.rx_dtcs = rx_tvalue
if tx_tmode == 'Tone' and rx_tmode == '':
mem.tmode = 'Tone'
elif tx_tmode and tx_tmode == rx_tmode and \
tx_tvalue == rx_tvalue:
mem.tmode = tx_tmode == 'Tone' and 'TSQL' or 'DTCS'
elif tx_tmode or rx_tmode:
mem.tmode = 'Cross'
mem.cross_mode = f'{tx_tmode}->{rx_tmode}'
mem.dtcs_polarity = tx_tpol + rx_tpol
mem.power = self.POWER_LEVELS[raw.high_power]
mem.skip = '' if raw.scan else 'S'
bcl = not raw.bcl_disabled
mem.extra = RadioSettingGroup("Extra", "extra")
rs = RadioSetting("bcl", "BCL",
RadioSettingValueBoolean(bcl))
rs.set_doc('Prevents transmitting on a channel that is already in '
'use. Pressing PTT on a busy channel sounds an alert '
'tone instead of keying the transmitter.')
mem.extra.append(rs)
rs = RadioSetting("encryption_key", "Encryption key",
RadioSettingValueList(self._ENC_OPTS,
current_index=enc_idx))
rs.set_doc('Encryption key for the digital mode. ' +
'If none is selected, analog audio will be used.')
mem.extra.append(rs)
rs = RadioSetting("hopping", "Hopping",
RadioSettingValueBoolean(hopping))
rs.set_doc('Frequency hopping. Only supported on analog modes.')
mem.extra.append(rs)
return mem
def _set_no_tone(self, elem):
elem.set_raw(b'\xFF\xFF')
def _set_ctcss(self, freq, elem):
elem.set_value(int(freq * 10))
def _set_dtcs(self, code, polarity, elem):
elem.set_value(code)
elem[1].set_bits(0x80)
if polarity == 'R':
elem[1].set_bits(0x40)
def _set_squelch(self, mode, ctcss_freq, dtcs_code, dtcs_polarity, elem):
if mode == 'Tone':
self._set_ctcss(ctcss_freq, elem)
elif mode == 'DTCS':
self._set_dtcs(dtcs_code, dtcs_polarity, elem)
else:
self._set_no_tone(elem)
def set_memory(self, mem):
raw = self._memobj.memory[mem.number - 1]
if mem.empty:
raw.fill_raw(b'\xFF')
return
raw.fill_raw(b'\x00')
raw.rx_freq = mem.freq // 10
if mem.duplex == '':
raw.tx_freq = mem.freq // 10
elif mem.duplex == 'split':
raw.tx_freq = mem.offset // 10
elif mem.duplex == '-':
raw.tx_freq = (mem.freq - mem.offset) // 10
elif mem.duplex == '+':
raw.tx_freq = (mem.freq + mem.offset) // 10
else:
raise errors.RadioError('Unsupported duplex mode %r' % mem.duplex)
raw.narrow = mem.mode == 'NFM'
try:
raw.high_power = self.POWER_LEVELS.index(mem.power)
except ValueError:
raw.high_power = 1
raw.scan = mem.skip != 'S'
raw.bcl_disabled = 1
raw.digital = 0
for item in mem.extra:
if item.get_name() == 'bcl':
raw.bcl_disabled = not item.value
elif item.get_name() == 'encryption_key':
idx = self._ENC_OPTS.index(item.value)
if idx == 0:
raw.digital = 0
else:
raw.digital = 1
raw.encryption_key = idx - 1
elif item.get_name() == 'hopping' and not raw.digital:
raw.hopping = item.value
if raw.digital:
self._set_no_tone(raw.tx_tone)
self._set_no_tone(raw.rx_tone)
elif mem.tmode == 'Tone':
self._set_ctcss(mem.rtone, raw.tx_tone)
self._set_no_tone(raw.rx_tone)
elif mem.tmode == 'TSQL':
self._set_ctcss(mem.ctone, raw.tx_tone)
self._set_ctcss(mem.ctone, raw.rx_tone)
elif mem.tmode == 'DTCS':
# According to the documentation we should use ".rx_dtcs" because
# "has_rx_dtcs" is set. However, the code does not match the docs,
# and we have to use ".dtcs".
self._set_dtcs(mem.dtcs, mem.dtcs_polarity[0], raw.tx_tone)
self._set_dtcs(mem.dtcs, mem.dtcs_polarity[1], raw.rx_tone)
elif mem.tmode == 'Cross':
tx_tmode, rx_tmode = mem.cross_mode.split('->', 2)
self._set_squelch(tx_tmode, mem.rtone, mem.dtcs,
mem.dtcs_polarity[0], raw.tx_tone)
self._set_squelch(rx_tmode, mem.ctone, mem.rx_dtcs,
mem.dtcs_polarity[1], raw.rx_tone)
@directory.register
class BaofengW31D(BaofengDigital):
"""Baofeng W31D"""
MODEL = 'W31D'
VALID_BANDS = [(400000000, 480000000)]
POWER_LEVELS = [
chirp_common.PowerLevel("Low", watts=0.5),
chirp_common.PowerLevel("High", watts=2.00),
]
_serial_id = 0x04
_proto = PROTO_D
@directory.register
class BaofengBFT20D(BaofengDigital):
"""Baofeng BF-T20D"""
MODEL = 'BF-T20D'
VALID_BANDS = [(400000000, 480000000)]
POWER_LEVELS = [
chirp_common.PowerLevel("Low", watts=0.5),
chirp_common.PowerLevel("High", watts=2.00),
]
_serial_id = 0x05
_proto = PROTO_D
@directory.register
class BaofengBFV12D(BaofengDigital):
"""Baofeng BF-V12D"""
MODEL = 'BF-V12D'
VALID_BANDS = [(400000000, 480000000)]
POWER_LEVELS = [
chirp_common.PowerLevel("Low", watts=0.5),
chirp_common.PowerLevel("High", watts=2.00),
]
_serial_id = 0x0a
_proto = PROTO_A
def _get_radio_id(self):
# The V12D requires a ucbfpwd unlock sequence before entering
# programming mode, observed from factory software captures.
for attempt in range(5):
try:
baofeng_common._clean_buffer(self)
# Send unlock command
self.pipe.write(b'\x02ucbfpwd')
resp = self.pipe.read(1)
LOG.debug('ucbfpwd response: %s',
resp.hex() if resp else 'empty')
if resp != CMD_ACK:
raise errors.RadioError(
'No ACK on unlock command (got %s)'
% (resp.hex() if resp else 'empty'))
# Send unlock data and model identifier
# (observed from factory software captures)
self.pipe.write(b'\x10\xc5\xea\x35')
resp = self.pipe.read(1)
LOG.debug('unlock data response: %s',
resp.hex() if resp else 'empty')
if resp != CMD_ACK:
raise errors.RadioError(
'No ACK on unlock data')
# Base64-encoded model identifier from factory software
self.pipe.write(
b'Q041OTUwMA==\x00\x00\x00\x00')
resp = self.pipe.read(1)
LOG.debug('model id response: %s',
resp.hex() if resp else 'empty')
if resp != CMD_ACK:
raise errors.RadioError(
'No ACK on model identifier')
# Now request radio ID
self.pipe.write(b'\x02prOGRAM')
resp = self.pipe.read(4)
LOG.debug('radio ID: %s',
resp.hex() if resp else 'empty')
if len(resp) != 4:
raise errors.RadioError(
'Radio did not send identification')
return resp
except errors.RadioError:
if attempt == 4:
raise
self._exit_programming()
time.sleep(1)
chirp-20261030/chirp/drivers/baofeng_dm32uv.py 0000644 0001750 0001750 00000341111 15264365036 020003 0 ustar hibby hibby # Copyright 2026 chaffed
#
# 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 .
"""Baofeng DM-32UV (DMR) driver.
Channels, zones (as banks), DMR lists (radio IDs, contacts, RX groups),
scan lists and radio settings. Upload writes only the pages that differ
from the radio (see UPLOAD_TAGS), one whole aligned 4 KB page per write,
and reads every written page back. The layout was worked out from the
vendor CPS (v1.60) and radio firmware and checked against a real radio
(firmware DM32.01.01.047).
The radio keeps its codeplug in 4 KB flash pages whose last byte is a tag
naming the contents; pages move around as the radio rewrites them. The
image this driver stores is logical: one 4 KB slot per tag in IMAGE_TAGS,
in that order, each holding the page as read (tag byte included), or all
0xFF if the radio has no page with that tag.
The protocol has no checksums, so the driver doesn't try to recover from
a bad link: apart from the PSEARCH handshake, any bad or missing reply
stops the transfer, and the link must be sound. Some CH340-based
programming cables are not: they flip bit 7 of received bytes (1 in 700
to 1 in 50 on the tested cable), while an FTDI cable on the same radio
read 2 MB without an error. A reply that is visibly garbled (a page tag
or model ID with bit 7 set) stops the transfer with advice about the cable.
"""
import collections
import functools
import logging
import struct
import time
from chirp import bitwise, chirp_common, directory, errors, memmap
from chirp import kenwood_tone
from chirp.settings import (RadioSetting, RadioSettingGroup, RadioSettings,
RadioSettingValueBoolean,
RadioSettingValueInteger, RadioSettingValueList,
RadioSettingValueString)
LOG = logging.getLogger(__name__)
PAGE = 0x1000
# The tags the vendor CPS reads (0x12-0x41 are the channel pages).
IMAGE_TAGS = ([0x02, 0x03, 0x04, 0x06, 0x0A, 0x0B, 0x0F, 0x10, 0x11] +
list(range(0x12, 0x49)) + list(range(0x5C, 0x68)))
CH_TAG0 = 0x12
CH_BASE = IMAGE_TAGS.index(CH_TAG0) * PAGE
CH_SIZE, CH_PER_PAGE, CH_COUNT = 0x30, 85, 4000
VFO_OFFSETS = (0xF9F, 0xFCF) # in the last channel page
# Zones (tags 0x5C-0x64): the CPS zone accessors (0x482740-0x482cd0) put
# zone z at page (z-1)//28, slot (z-1)%28 of 0x91 bytes, after a 16-byte
# header on the first page only. At most 250 zones of 64 members.
ZONE_TAG0 = 0x5C
ZONE_BASE = IMAGE_TAGS.index(ZONE_TAG0) * PAGE
ZONE_PER_PAGE, ZONE_COUNT, ZONE_MEMBERS = 28, 250, 64
# Longest zone order text on the Settings tab: "1, 2, ..., 250".
ZONE_ORDER_MAX = len(', '.join(str(z) for z in range(1, ZONE_COUNT + 1)))
# Encryption key names (CPS 0x479630): tag, offset of entry 1, entry size,
# name length, entries. Keys are chosen per channel, not edited.
DMR_LISTS = {
'privacy': (0x10, 0x301, 0x2C, 10, 32),
'emergency': (0x10, 0x000, 0x14, 10, 8), # digital emergency systems
}
# DMR lists, from the CPS accessors (see PROTOCOL.md "DMR lists"):
# radio IDs (tag 0x67): count at 0, entry n at 16*n = u24 LE ID + 12-char name
RADIOID_TAG, RADIOID_MAX, RADIOID_NAME = 0x67, 250, 12
# contacts: index page 0x0B (u16 count, counts by call type, free-slot bitmap,
# name- and ID-sorted lists), 24-byte records on pages 0x44-0x48
CONTACT_INDEX_TAG, CONTACT_REC_TAG0 = 0x0B, 0x44
CONTACT_PER_PAGE, CONTACT_REC, CONTACT_MAX, CONTACT_NAME = 170, 0x18, 800, 16
CALL_TYPES = ['Private Call', 'Group Call', 'All Call']
# RX group lists (tag 0x0F): used-bitmap in bytes 0-3, group n at
# 0x6D*n - 0x5C: 11-char name, 32 member IDs (u24 LE)
RXG_TAG, RXG_MAX, RXG_NAME, RXG_MEMBERS, RXG_REC = 0x0F, 32, 11, 32, 0x6D
# Per-channel TX contact (CPS 0x480050), outside the channel record:
# 2 bytes per channel on tags 0x42/0x43, 12-bit contact number + digital flag
TXC_TAGS = (0x42, 0x43)
DMR_ID_MAX, ALL_CALL_ID = 16776415, 16777215
# Scan lists (tag 0x11; CPS 0x483050-0x484160): byte 0 = number of lists;
# list n at 0x39*n - 0x38: 11-char name, +0x0b member count, +0x0c CTC scan
# mode (low nibble) / TX mode (high nibble), +0x0d..+0x17 other options
# (kept as they are), +0x18 up to 16 u16 channel numbers. The members start
# in the first slot (checked on the radio's own display).
SCAN_TAG, SCAN_MAX, SCAN_REC, SCAN_NAME, SCAN_MEMBERS = 0x11, 32, 0x39, 11, 16
# [ScanCtcDcsMode]
SCAN_CTC_MODES = ['Not Detection CTC', 'Detection CTC Non Priority',
'Detection CTC Priority', 'Detection CTC']
SCAN_TX_MODES = ['Current Channel', 'Last Active Channel', # [ScanTxMode]
'Designed Channel']
# options of the radio's factory scan lists; bytes 3-4 = designed channel
SCAN_DEFAULT_OPTS = bytes.fromhex('030600010000000000' '0a007f')
# Some CH340 cables corrupt data from this radio (see the module docstring).
CABLE_ADVICE = ('Some CH340-based programming cables corrupt data from '
'this radio; a cable with an FTDI or CP2102 chip is '
'recommended.')
# The pages upload may write, in the order it writes them: lists before
# what refers to them (contact records before their index, lists before
# channels, zones last), so an interrupted upload leaves few dangling
# references. One whole aligned page per W frame: the firmware erases the
# sector on an aligned W and also erases the next sector if a W crosses
# into it. Tags 0x02 and 0x69 look like calibration: never written.
UPLOAD_TAGS = ([0x03, 0x04, 0x06, RADIOID_TAG] +
list(range(CONTACT_REC_TAG0, CONTACT_REC_TAG0 + 5)) +
[CONTACT_INDEX_TAG, RXG_TAG, SCAN_TAG] +
list(range(0x12, 0x42)) + list(TXC_TAGS) +
list(range(0x5C, 0x65)))
NEVER_WRITE = (0x02, 0x69)
# DTMF (tag 0x06; CPS dialog 0x421bc0, accessors 0x47bb10-0x47c920). Codes
# are one digit per byte (0-9, A-D = 10-13, * = 14, # = 15), ended by 0xFF.
# 0xA20 on is BDC1200 (not handled; kept as it is).
DTMF_TAG, DTMF_CODES, DTMF_CONTACTS = 0x06, 16, 64
DTMF_CHARS = '0123456789ABCD*#'
CODE_CHARS = {'dtmf': DTMF_CHARS, 'digits': '0123456789',
'hex': '0123456789ABCDEF'}
# Two-tone (tag 0x03; CPS dialogs 0x45a760 system, 0x45ac30 decode,
# 0x45ae20 encode; accessors 0x479df0-0x47a7a0). Times in 0.1 s,
# frequencies in 0.1 Hz. Five-tone is on the same page from 0x730.
SIGNAL_TAG, TT_ENCODE, TT_DECODE = 0x03, 32, 4
# Five-tone (tag 0x03 from 0x730; CPS dialogs 0x4254c0 system, 0x426240
# message codes, 0x426590 special calls). Codes are hex digits, one per
# byte, 0xFF-terminated.
FT_MSG, FT_SPECIAL = 8, 32
FT_STANDARDS = ['ZVEI1', 'ZVEI2', 'ZVEI3', 'CCIR1', 'CCIR2', 'CCIR3', 'EEA',
'EIA'] # [FiveTone*Standard]
FT_SPECIAL_TYPES = (['Off', 'ANI', 'Data Transmission'], # [SpecialCallType]
[0xFF, 0, 1])
FT_DELIMITERS = ['No Pause in mid', 'A', 'B', 'C', 'D', 'E', 'F']
FT_TONE_LONG = (['%d ms' % i for i in range(30, 101, 10)], 3)
TT_HZ = (2885, 31068) # frequency limits in 0.1 Hz (CPS 0x45a760)
# [TwoToneDecodeFormat] and the stored codes (CPS 0x47a1f0)
TT_DECODE_FORMATS = ['None', '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', 'Long D']
TT_DECODE_CODES = [0xFF, 0x01, 0x02, 0x03, 0x10, 0x12, 0x13, 0x20, 0x21,
0x23, 0x30, 0x31, 0x32, 0x0F, 0x1F, 0x2F, 0x3F]
# The page each settings struct lives on.
STRUCT_TAGS = {'dtmf': DTMF_TAG, 'twotone': SIGNAL_TAG,
'fivetone': SIGNAL_TAG}
# The image layout: one 4 KB slot per tag in IMAGE_TAGS order (see the
# module docstring), so every address below is fixed.
MEM_FORMAT = """
struct chan {
char name[16];
lbcd rxfreq[4];
lbcd txfreq[4];
u8 chtype:4, forbid_tx:1, power:2, lone_work:1;
u8 wide:1, auto_scan:1, scanlist:6;
u8 forbid_talkaround:1, tx_admit:3, unknown1a:1, aprs_rx:1, offset_dir:2;
u8 emerg_indicator:1, emerg_ack:1, unknown1b:1, emerg_system:5;
u8 squelch:4, aprs_report:2, aprs_ptt_analog:1, aprs_ptt_digital:1;
u8 private_confirm:1, short_data_confirm:1, tdma_direct:1, timeslot:1,
colorcode:4;
u8 privacy;
u8 unknown1f:1, encrypt:1, rxgroup:6;
u8 aprs_channel;
ul16 rxtone;
ul16 txtone;
u8 unknown25:2, compander:1, vox:1, unknown25b:4;
u8 ptt_id_display:1, rx_squelch_mode:3, signaling:3, unknown26:1;
u8 rx_signal:4, tx_signal:4;
u8 unknown28;
u8 step:4, ptt_id:2, unknown29:2;
u8 unknown2a;
u8 radio_id;
lbcd offset[4];
};
struct zone {
char name[16];
u8 count;
ul16 members[64];
};
// Tag 0x03, slot 0x1000: two-tone, then five-tone from +0x730.
#seekto 0x01001;
u8 tt_encode_count;
#seekto 0x01030;
struct {
u8 pre_carrier;
u8 first_tone;
u8 second_tone;
u8 long_tone;
u8 interval;
u8 unknown35;
u8 polite_wait;
ul16 freq_a;
ul16 freq_b;
ul16 freq_c;
ul16 freq_d;
u8 unknown3f:6, side_tone:1, idle_ack:1;
u8 auto_reset;
u8 dec1_format;
u8 dec1_call;
u8 dec1_unknown:7, dec1_reply:1;
u8 dec1_unknown2;
u8 dec2_format;
u8 dec2_call;
u8 dec2_unknown:7, dec2_reply:1;
u8 dec2_unknown2;
u8 dec3_format;
u8 dec3_call;
u8 dec3_unknown:7, dec3_reply:1;
u8 dec3_unknown2;
u8 dec4_format;
u8 dec4_call;
u8 dec4_unknown:7, dec4_reply:1;
u8 dec4_unknown2;
} twotone;
#seekto 0x01220;
struct {
u8 name[32];
u8 unknown20:7, single_tone:1;
u8 unknown21;
ul16 tone1;
ul16 tone2;
u8 unknown26[2];
} tt_encode[32];
#seekto 0x01730;
struct {
u8 self_id[5];
u8 decode_std;
u8 unknown736:4, decode_resp:4;
u8 unknown737[3];
u8 pre_carrier;
u8 auto_reset;
u8 send_delay;
u8 ptt_id_pause;
u8 first_delay;
u8 unknown73f:7, side_tone:1;
u8 unknown740[16];
u8 msg1_func;
u8 msg1_unknown:4, msg1_resp:4;
u8 msg1_code[12];
u8 msg1_pad[2];
u8 msg2_func;
u8 msg2_unknown:4, msg2_resp:4;
u8 msg2_code[12];
u8 msg2_pad[2];
u8 msg3_func;
u8 msg3_unknown:4, msg3_resp:4;
u8 msg3_code[12];
u8 msg3_pad[2];
u8 msg4_func;
u8 msg4_unknown:4, msg4_resp:4;
u8 msg4_code[12];
u8 msg4_pad[2];
u8 msg5_func;
u8 msg5_unknown:4, msg5_resp:4;
u8 msg5_code[12];
u8 msg5_pad[2];
u8 msg6_func;
u8 msg6_unknown:4, msg6_resp:4;
u8 msg6_code[12];
u8 msg6_pad[2];
u8 msg7_func;
u8 msg7_unknown:4, msg7_resp:4;
u8 msg7_code[12];
u8 msg7_pad[2];
u8 msg8_func;
u8 msg8_unknown:4, msg8_resp:4;
u8 msg8_code[12];
u8 msg8_pad[2];
u8 bot_id[16];
u8 bot_std;
u8 bot_long;
u8 unknown7e2[14];
u8 eot_id[16];
u8 eot_std;
u8 eot_long;
} fivetone;
#seekto 0x01820;
struct {
u8 type;
u8 code[5];
u8 delimiter;
u8 standard;
u8 tone_long;
u8 unknown9[7];
u8 data[16];
u8 name[16];
} ft_special[32];
// Tag 0x04, slot 0x2000: radio settings.
#seekto 0x02000;
struct {
u8 poweron_type;
char line1[14];
char line2[14];
u8 unknown1d:7, allow_reset:1;
u8 auto_power_off;
u8 unknown1f;
u8 radio_silent:1, key_tone:1, sms_alert:1, group_call_tone:1,
private_call_tone:1, call_end_tone:1, talk_permit_tone:1,
startup_sound:1;
u8 voice_prompt:1, battery_low:1, tx_end_tone:2, unknown21:4;
} set_power;
#seekto 0x02030;
struct {
u8 backlight;
u8 auto_backlight;
u8 menu_exit;
u8 unknown33a:3, volume_prompt:1, date_format:1, unknown33b:2,
time_display:1;
u8 call_color;
u8 standby_color;
u8 tx_backlight;
u8 rx_backlight;
u8 a_name_color;
u8 b_name_color;
u8 a_zone_color;
u8 b_zone_color;
} set_display;
#seekto 0x02040;
struct {
u8 unknown40:1, gps_format:1, speed_unit:2, gps_mode:2, distance_unit:1,
gps_switch:1;
u8 time_zone;
u8 measure_period;
u8 unknown43[2];
u8 unknown45:5, record_type:2, record_switch:1;
} set_gps;
#seekto 0x02060;
struct {
u8 unknown60:6, group_match:1, private_match:1;
u8 call_hold;
u8 active_wait;
u8 active_retries;
u8 pre_carrier;
u8 monitor_decode:1, disable_decode:1, check_decode:1, enable_decode:1,
alert_decode:1, data_service:2, missed_call_alert:1;
u8 monitor_time;
u8 name_format:2, unknown67a:2, send_tx_name:1, name_priority:1,
unknown67b:2;
} set_dmr;
#seekto 0x02080;
struct {
u8 dual_watch:2, main_line:1, b_display:1, a_display:1, b_mode:1,
a_mode:1, only_channel:1;
u8 dual_watch_hang;
u8 unknown82[3];
u8 unknown85:4, forbid_lock:1, side_lock:1, knob_lock:1, lock_mode:1;
u8 lock_delay;
u8 key_sk1_short;
u8 key_sk1_long;
u8 key_sk2_short;
u8 key_sk2_long;
u8 key_tk_short;
u8 key_tk_long;
u8 key_p1_short;
u8 key_p1_long;
u8 key_p2_short;
u8 key_p2_long;
u8 key_p3_short;
u8 key_p3_long;
u8 long_press;
} set_work;
#seekto 0x020a0;
struct {
u8 tot;
u8 tot_pre;
u8 vox_level;
u8 vox_delay;
u8 power_save:4, unknowna4:1, weather_alarm:1, language:1,
disable_leds:1;
u8 tbst:4, unknowna5:2, tail_mode:2;
u8 mic_analog;
u8 mic_digital;
} set_opts;
#seekto 0x02301;
struct {
u8 send_interval;
u8 unknown302:7, fixed_beacon:1;
u8 unknown303[3];
char latitude[9];
u8 lat_hemi;
char longitude[9];
u8 lon_hemi;
u8 unknown31a[4];
ul16 report_channel[8];
u8 unknown32e[2];
u8 active_delay;
u8 unknown331:7, call_type:1;
ul24 upload_number;
} set_aprs;
#seekto 0x02500;
struct {
u8 unknown500:6, new_zone:1, zone_list:1;
u8 unknown501:2, measure_period:1, radio_disable:1, radio_enable:1,
remote_monitor:1, radio_check:1, call_alert:1;
u8 display_mode:1, match_group:1, match_private:1, lang_select:1,
start_display:1, tx_power:1, alert_tone:1, talkaround:1;
u8 unknown503a:1, record:1, aprs:1, gps:1, power_save:1,
sub_channel_mode:1, unknown503b:2;
u8 unknown504:1, csv_contacts:1, manual_dial:1, functionality:1,
send_message:1, del_contact:1, edit_contact:1, add_contact:1;
u8 unknown505:4, del_log:1, sent_call:1, answered_call:1, missed_call:1;
u8 radio_name:1, radio_id:1, time_slot:1, color_code:1, tx_contact:1,
ctc_dcs:1, tx_freq:1, rx_freq:1;
u8 unknown507:3, channel_name:1, add_channel:1, rx_group:1,
tdma_direct:1, channel_type:1;
} set_menu;
// Tag 0x06, slot 0x3000: DTMF (BDC1200 from +0xA20 is not mapped).
#seekto 0x03000;
struct {
u8 code1[16];
u8 code2[16];
u8 code3[16];
u8 code4[16];
u8 code5[16];
u8 code6[16];
u8 code7[16];
u8 code8[16];
u8 code9[16];
u8 code10[16];
u8 code11[16];
u8 code12[16];
u8 code13[16];
u8 code14[16];
u8 code15[16];
u8 code16[16];
u8 pre_carrier;
u8 first_digit;
u8 duration;
u8 interval;
u8 auto_reset;
u8 unknown105:7, side_tone:1;
u8 self_id[3];
u8 group_code;
u8 interval_sign;
u8 auto_answer;
u8 ptt_id_pause;
u8 unknown10d;
u8 min_duration;
u8 unknown10f;
u8 ptt_id_up[16];
u8 ptt_id_down[16];
u8 stun_code[16];
u8 kill_code[16];
} dtmf;
#seekto 0x031ff;
u8 dtmf_contact_count;
struct {
char name[16];
u8 number[5];
u8 unknown[11];
} dtmf_contacts[64];
// Tags 0x12-0x41, slots 0x09000-0x38fff: channels. Page 0 starts with
// the channel count; 84 channels on page 0, 85 on the others.
#seekto 0x09000;
ul16 ch_count;
#seekto 0x09010;
struct chan page0[84];
#seekto 0x0a000;
struct chan page1[85];
#seekto 0x0b000;
struct chan page2[85];
#seekto 0x0c000;
struct chan page3[85];
#seekto 0x0d000;
struct chan page4[85];
#seekto 0x0e000;
struct chan page5[85];
#seekto 0x0f000;
struct chan page6[85];
#seekto 0x10000;
struct chan page7[85];
#seekto 0x11000;
struct chan page8[85];
#seekto 0x12000;
struct chan page9[85];
#seekto 0x13000;
struct chan page10[85];
#seekto 0x14000;
struct chan page11[85];
#seekto 0x15000;
struct chan page12[85];
#seekto 0x16000;
struct chan page13[85];
#seekto 0x17000;
struct chan page14[85];
#seekto 0x18000;
struct chan page15[85];
#seekto 0x19000;
struct chan page16[85];
#seekto 0x1a000;
struct chan page17[85];
#seekto 0x1b000;
struct chan page18[85];
#seekto 0x1c000;
struct chan page19[85];
#seekto 0x1d000;
struct chan page20[85];
#seekto 0x1e000;
struct chan page21[85];
#seekto 0x1f000;
struct chan page22[85];
#seekto 0x20000;
struct chan page23[85];
#seekto 0x21000;
struct chan page24[85];
#seekto 0x22000;
struct chan page25[85];
#seekto 0x23000;
struct chan page26[85];
#seekto 0x24000;
struct chan page27[85];
#seekto 0x25000;
struct chan page28[85];
#seekto 0x26000;
struct chan page29[85];
#seekto 0x27000;
struct chan page30[85];
#seekto 0x28000;
struct chan page31[85];
#seekto 0x29000;
struct chan page32[85];
#seekto 0x2a000;
struct chan page33[85];
#seekto 0x2b000;
struct chan page34[85];
#seekto 0x2c000;
struct chan page35[85];
#seekto 0x2d000;
struct chan page36[85];
#seekto 0x2e000;
struct chan page37[85];
#seekto 0x2f000;
struct chan page38[85];
#seekto 0x30000;
struct chan page39[85];
#seekto 0x31000;
struct chan page40[85];
#seekto 0x32000;
struct chan page41[85];
#seekto 0x33000;
struct chan page42[85];
#seekto 0x34000;
struct chan page43[85];
#seekto 0x35000;
struct chan page44[85];
#seekto 0x36000;
struct chan page45[85];
#seekto 0x37000;
struct chan page46[85];
#seekto 0x38000;
struct chan page47[6];
// VFO A and B, back to back in the last channel page.
#seekto 0x38f9f;
struct chan vfo0;
struct chan vfo1;
// Tags 0x5C-0x64, slots 0x40000-0x48fff: zones. Header bytes 1/3: position
// in the zone shown on lines A/B, bytes 5/7: the zone for A/B.
#seekto 0x40000;
struct {
u8 count;
u8 a_pos;
u8 unknown2;
u8 b_pos;
u8 unknown4;
u8 a_zone;
u8 unknown6;
u8 b_zone;
} zone_hdr;
#seekto 0x40010;
struct zone zpage0[28];
#seekto 0x41000;
struct zone zpage1[28];
#seekto 0x42000;
struct zone zpage2[28];
#seekto 0x43000;
struct zone zpage3[28];
#seekto 0x44000;
struct zone zpage4[28];
#seekto 0x45000;
struct zone zpage5[28];
#seekto 0x46000;
struct zone zpage6[28];
#seekto 0x47000;
struct zone zpage7[28];
#seekto 0x48000;
struct zone zpage8[28];
"""
# Radio settings on tag 0x04, from the CPS option dialogs (0x446770 power
# on, 0x4388d0 tones, 0x440260 display, 0x4656a0 work mode, 0x444f60
# options). Each entry: (struct, field, label, kind, extra). kind 'list':
# stored value = index (+ extra[1] offset); 'bool'; 'text' (length).
_SECONDS = ['%d s' % i for i in range(1, 31)]
_COLORS = ['White', 'Black', 'Orange', 'Red', 'Yellow', 'Green', 'Cyan',
'Blue'] # [DisplayColor]
RADIO_SETTINGS = [
('Power on', [
('set_power', 'poweron_type', 'Power-on screen',
'list', (['Power On Picture', 'Custom Message', 'Battery Volt'], 0)),
('set_power', 'line1', 'Power-on text line 1', 'text', 14),
('set_power', 'line2', 'Power-on text line 2', 'text', 14),
('set_power', 'auto_power_off', 'Auto power off',
'list', (['Off', '30 min', '60 min', '120 min', '240 min',
'480 min'], 0)),
('set_power', 'allow_reset', 'Allow reset', 'bool', None),
]),
('Tones', [
('set_power', 'key_tone', 'Key tone', 'bool', None),
('set_power', 'voice_prompt', 'Voice prompt', 'bool', None),
('set_power', 'startup_sound', 'Startup sound', 'bool', None),
('set_power', 'battery_low', 'Battery low alert', 'bool', None),
('set_power', 'talk_permit_tone', 'Talk permit tone', 'bool', None),
('set_power', 'call_end_tone', 'Call end tone', 'bool', None),
('set_power', 'group_call_tone', 'Group call tone', 'bool', None),
('set_power', 'private_call_tone', 'Private call tone', 'bool',
None),
('set_power', 'sms_alert', 'SMS alert', 'bool', None),
('set_power', 'radio_silent', 'Radio silent', 'bool', None),
('set_power', 'tx_end_tone', 'Analog TX end tone',
'list', (['Off', 'Tone', 'BDC'], 0)),
]),
('Display', [
('set_display', 'backlight', 'Backlight brightness',
'list', ([str(i) for i in range(1, 7)], 0)),
('set_display', 'menu_exit', 'Menu exit time',
'list', (['Off'] + ['%d s' % i for i in range(5, 61, 5)], 0)),
('set_display', 'tx_backlight', 'TX backlight delay',
'list', (['Off'] + _SECONDS, 0)),
('set_display', 'rx_backlight', 'RX backlight delay',
'list', (['Always'] + _SECONDS, 0)),
('set_display', 'time_display', 'Time display', 'bool', None),
('set_display', 'date_format', 'Date format',
'list', (['yyyy/m/d', 'd/m/yyyy'], 0)),
('set_display', 'volume_prompt', 'Volume change prompt', 'bool',
None),
('set_display', 'call_color', 'Call display colour',
'list', (_COLORS, 0)),
('set_display', 'standby_color', 'Standby text colour',
'list', (_COLORS, 0)),
('set_display', 'a_name_color', 'Channel name colour (A)',
'list', (_COLORS, 0)),
('set_display', 'b_name_color', 'Channel name colour (B)',
'list', (_COLORS, 0)),
('set_display', 'a_zone_color', 'Zone colour (A)',
'list', (_COLORS, 0)),
('set_display', 'b_zone_color', 'Zone colour (B)',
'list', (_COLORS, 0)),
]),
('Work mode', [
('set_work', 'only_channel', 'Only channel mode', 'bool', None),
('set_work', 'dual_watch', 'Dual watch',
'list', (['Single Mode', 'Double Wait', 'Single Wait'], 0)),
('set_work', 'dual_watch_hang', 'Dual watch hang time',
'list', (['%d ms' % i for i in range(0, 6501, 500)], 0)),
]),
('Options', [
('set_opts', 'tot', 'TX timeout (TOT)',
'list', (['Off'] + ['%d s' % i for i in range(15, 496, 5)], 0)),
('set_opts', 'tot_pre', 'TOT pre-alert',
'list', (['Off'] + ['%d s' % i for i in range(1, 11)], 0)),
('set_opts', 'vox_level', 'VOX level',
'list', ([str(i) for i in range(1, 6)], 0)),
('set_opts', 'vox_delay', 'VOX delay',
'list', (['%.1f s' % (i / 10) for i in range(3, 51)], 3)),
('set_opts', 'language', 'Language',
'list', (['Chinese', 'English'], 0)),
('set_opts', 'power_save', 'Power save',
'list', (['None', '1:1', '1:2', '1:4'], 0)),
('set_opts', 'tail_mode', 'Tail noise reduction',
'list', (['None', '120', '180', '55 Hz'], 0)),
('set_opts', 'tbst', 'TBST (tone burst)',
'list', (['1000 Hz', '1450 Hz', '1750 Hz', '2100 Hz'], 0)),
('set_opts', 'mic_analog', 'Analog mic level',
'list', ([str(i) for i in range(1, 6)], 0)),
('set_opts', 'disable_leds', 'Disable all LEDs', 'bool', None),
('set_opts', 'weather_alarm', 'Weather alarm', 'bool', None),
]),
]
_KEY_FUNCS = [
'None', 'Power Select', 'Volt', 'Talkaround', 'Digital Encrypt', 'Call',
'VOX', 'V/M', 'Alarm', 'One Touch Call 1', 'One Touch Call 2',
'One Touch Call 3', 'One Touch Call 4', 'One Touch Call 5', 'SMS',
'Contacts', 'Zone Up', 'Zone Down', 'Scan', 'Record Switch',
'Previous Record', 'Next Record', 'FM Radio', 'FM Search',
'GPS Information', 'Monitor', 'Switch Main Channel', 'Lone Work',
'Keypad Lock', 'Nuisance Channel Delete', 'TBST Send', 'APRS Send',
'Channel Type', 'Display Mode', 'CTC Scan', 'CTC Setting', 'Silent Tone',
'Roaming', 'Sub-PTT', 'Analog Scramble Switch', 'One Key Scan Freq',
'Flashlight', 'Man Down Alarm'] # [KeyFuncData]
_UTC = ['UTC %+d:00' % h if h else 'UTC' for h in range(-12, 14)]
_ONOFF = (['Off', 'On'], 0)
# Which key owns which byte comes from the CPS keys dialog (0x430760: the
# accessor that fills each control, and 0x430600: the control's label).
RADIO_SETTINGS.append(('Keys', [
('set_work', 'lock_mode', 'Keypad lock',
'list', (['Manual', 'Auto'], 0)),
('set_work', 'lock_delay', 'Auto keypad lock delay',
'list', (['%d s' % i for i in range(5, 61)], 0)),
('set_work', 'knob_lock', 'Knob lock', 'list', _ONOFF),
('set_work', 'side_lock', 'Side key lock', 'list', _ONOFF),
('set_work', 'forbid_lock', 'Forbid lock key', 'list', _ONOFF),
('set_work', 'long_press', 'Long press time',
'list', ([str(i) for i in range(1, 6)], 0)),
] + [('set_work', 'key_%s_%s' % (k, p), '%s %s press' % (k.upper(), p),
'list', (_KEY_FUNCS, 0))
for k in ('sk1', 'sk2', 'tk', 'p1', 'p2', 'p3')
for p in ('short', 'long')]))
RADIO_SETTINGS.append(('DMR', [
('set_dmr', 'private_match', 'Private call match', 'bool', None),
('set_dmr', 'group_match', 'Group call match', 'bool', None),
('set_dmr', 'call_hold', 'Call hold time',
'list', (['%d s' % i for i in range(1, 61)], 0)),
('set_dmr', 'active_wait', 'Active wait time',
'list', (['%d ms' % i for i in range(300, 4801, 30)], 1)),
('set_dmr', 'active_retries', 'Active retries',
'list', ([str(i) for i in range(1, 9)], 1)),
('set_dmr', 'pre_carrier', 'Pre-carrier time',
'list', (['%d ms' % i for i in range(120, 8641, 120)], 0)),
('set_dmr', 'monitor_time', 'Remote monitor time',
'list', (['%d s' % i for i in range(10, 121, 10)], 0)),
('set_dmr', 'data_service', 'SMS format',
'list', (['H-SMS', 'M-SMS', 'D-SMS'], 0)),
('set_dmr', 'missed_call_alert', 'Missed call alert', 'bool', None),
('set_dmr', 'monitor_decode', 'Remote monitor decode', 'bool', None),
('set_dmr', 'disable_decode', 'Radio disable decode', 'bool', None),
('set_dmr', 'check_decode', 'Radio check decode', 'bool', None),
('set_dmr', 'enable_decode', 'Radio enable decode', 'bool', None),
('set_dmr', 'alert_decode', 'Call alert decode', 'bool', None),
('set_dmr', 'name_format', 'Name data format',
'list', (['ISO 8 bit', '16 bit Unicode'], 0)),
('set_dmr', 'send_tx_name', 'Send TX name', 'bool', None),
('set_dmr', 'name_priority', 'Name display priority',
'list', (['Contact', 'Name'], 0)),
]))
RADIO_SETTINGS.append(('GPS and recording', [
('set_gps', 'gps_switch', 'GPS', 'bool', None),
('set_gps', 'gps_mode', 'GPS mode',
'list', (['GPS', 'BDS', 'GPS+BDS'], 0)),
('set_gps', 'time_zone', 'Time zone', 'list', (_UTC, 0)),
('set_gps', 'measure_period', 'GPS measure period',
'list', (['%d s' % i for i in range(5, 256)], 0)),
('set_gps', 'distance_unit', 'Distance unit',
'list', (['Metric', 'Imperial'], 0)),
('set_gps', 'speed_unit', 'Speed unit',
'list', (['km/h', 'mph', 'knots'], 0)),
('set_gps', 'gps_format', 'GPS display format',
'list', (['Degrees', 'Degrees/min/sec'], 0)),
('set_gps', 'record_switch', 'Recording', 'bool', None),
('set_gps', 'record_type', 'Recording type',
'list', (['Receive', 'Transmit', 'Receive+Transmit'], 0)),
]))
# APRS (CPS dialog 0x439c00, accessors 0x488580-0x488cb0). The radio
# reports its position over DMR: to the "upload number" (a DMR ID) as a
# private or group call, on one of 8 report channels chosen per channel.
# Coordinates are 9 ASCII characters, e.g. "23.000000" / "118.00000"; the
# hemisphere is separate.
RADIO_SETTINGS.append(('APRS', [
('set_aprs', 'send_interval', 'Scheduled send time',
'list', (['Off'] + ['%d s' % i for i in range(30, 7201, 30)], 0)),
('set_aprs', 'fixed_beacon', 'Fixed beacon (use the position below)',
'bool', None),
('set_aprs', 'latitude', 'Latitude (degrees)', 'coord', 90),
('set_aprs', 'lat_hemi', 'Latitude N/S', 'list', (['N', 'S'], 0)),
('set_aprs', 'longitude', 'Longitude (degrees)', 'coord', 180),
('set_aprs', 'lon_hemi', 'Longitude E/W', 'list', (['E', 'W'], 0)),
('set_aprs', 'upload_number', 'Upload number (DMR ID, 0 = none)',
'int', (0, DMR_ID_MAX)),
('set_aprs', 'call_type', 'Call type',
'list', (['Private', 'Group'], 0)), # [AprsCallType]
('set_aprs', 'active_delay', 'Repeater active delay',
'list', (['Off'] + ['%d ms' % i for i in range(100, 1001, 100)], 0)),
] + [('set_aprs', 'report_channel', 'Report channel %d' % (i + 1),
'channel', i) for i in range(8)]))
_MS = '%d ms'
RADIO_SETTINGS.append(('DTMF', [
('dtmf', 'self_id', 'Self ID code', 'digits', 3),
('dtmf', 'ptt_id_up', 'PTT ID up code (BOT)', 'dtmf', 16),
('dtmf', 'ptt_id_down', 'PTT ID down code (EOT)', 'dtmf', 16),
('dtmf', 'stun_code', 'Stun code', 'dtmf', 16),
('dtmf', 'kill_code', 'Kill code', 'dtmf', 16),
('dtmf', 'group_code', 'Group code', 'list', ( # [DtmfGroupCode]
['Off', 'A', 'B', 'C', 'D', '*', '#'], [0xFF] + list(range(10, 16)))),
('dtmf', 'interval_sign', 'DTMF interval sign', 'list', (
['A', 'B', 'C', 'D', '*', '#'], 10)), # [DtmfIntervalSign]
('dtmf', 'auto_answer', 'Auto answer', 'list', ( # [DtmfAutoAck]
['Off', 'Alert Tone', 'Alert Tone And Ack'], 0)),
('dtmf', 'side_tone', 'Side tone', 'bool', None),
('dtmf', 'pre_carrier', 'Pre-carrier time', 'list', (
[_MS % i for i in range(300, 5001, 50)], 15)),
('dtmf', 'first_digit', 'First digit time', 'list', (
[_MS % i for i in range(100, 1001, 50)], 0)),
('dtmf', 'duration', 'Send DTMF duration', 'list', (
[_MS % i for i in range(80, 2001, 10)], 0)),
('dtmf', 'interval', 'Send DTMF interval', 'list', (
[_MS % i for i in range(80, 2001, 10)], 0)),
('dtmf', 'min_duration', 'Dial code minimum duration', 'list', (
[_MS % i for i in range(25, 2501, 25)], 0)),
('dtmf', 'auto_reset', 'Auto reset time', 'list', (
['%d s' % i for i in range(1, 256)], 1)),
('dtmf', 'ptt_id_pause', 'PTT ID pause time', 'list', (
['Off'] + ['%d s' % i for i in range(5, 76)],
[0] + list(range(5, 76)))),
] + [('dtmf', 'code%d' % i, 'DTMF code %d' % i, 'dtmf', 16)
for i in range(1, DTMF_CODES + 1)]))
def _tenths(first, last, unit='s'):
return ['%.1f %s' % (i / 10, unit) for i in range(first, last + 1)]
RADIO_SETTINGS.append(('Two-tone', [
('twotone', 'freq_a', 'Tone A frequency (Hz)', 'hz', TT_HZ),
('twotone', 'freq_b', 'Tone B frequency (Hz)', 'hz', TT_HZ),
('twotone', 'freq_c', 'Tone C frequency (Hz)', 'hz', TT_HZ),
('twotone', 'freq_d', 'Tone D frequency (Hz)', 'hz', TT_HZ),
('twotone', 'pre_carrier', 'Pre-carrier time', 'list',
(_tenths(0, 50), 0)),
('twotone', 'first_tone', 'First tone duration', 'list',
(_tenths(5, 40), 5)),
('twotone', 'second_tone', 'Second tone duration', 'list',
(_tenths(5, 40), 5)),
('twotone', 'long_tone', 'Long tone duration', 'list',
(_tenths(5, 100), 5)),
('twotone', 'interval', 'Interval time', 'list', (_tenths(0, 20), 0)),
('twotone', 'polite_wait', 'Polite wait time', 'list',
(_tenths(0, 50), 0)),
('twotone', 'auto_reset', 'Auto reset time', 'list', (
['%d s' % i for i in range(1, 256)], 1)),
('twotone', 'idle_ack', 'Idle ack', 'bool', None),
('twotone', 'side_tone', 'Side tone', 'bool', None),
] + [entry for i in range(1, TT_DECODE + 1) for entry in (
('twotone', 'dec%d_format' % i, 'Decode %d: format' % i, 'list',
(TT_DECODE_FORMATS, TT_DECODE_CODES)),
('twotone', 'dec%d_call' % i, 'Decode %d: call type' % i, 'list', (
['None', 'Call Alert', 'Voice Call Alert', 'Select Call'], 0)),
('twotone', 'dec%d_reply' % i, 'Decode %d: reply' % i, 'bool', None))]))
_RESPONSES = ['None', 'Alert Tone', 'Alert Tone And ACK']
RADIO_SETTINGS.append(('Five-tone', [
('fivetone', 'self_id', 'Self ID code', 'hex', 5),
('fivetone', 'decode_std', 'Decode standard', 'list', (FT_STANDARDS, 0)),
('fivetone', 'decode_resp', 'Decode response', 'list', (_RESPONSES, 0)),
('fivetone', 'pre_carrier', 'Pre-carrier time', 'list', (
[_MS % i for i in range(300, 5001, 50)], 15)),
('fivetone', 'auto_reset', 'Auto reset time', 'list', (
['%d s' % i for i in range(1, 256)], 1)),
('fivetone', 'send_delay', 'Delay time after send code', 'list', (
[_MS % i for i in range(10, 2551, 10)], 1)),
('fivetone', 'ptt_id_pause', 'PTT ID pause time', 'list', (
['Off'] + ['%d s' % i for i in range(5, 76)],
[0] + list(range(5, 76)))),
('fivetone', 'first_delay', 'First delay time', 'list', (
[str(i) for i in range(10, 2551, 10)], 1)),
('fivetone', 'side_tone', 'Side tone', 'bool', None),
('fivetone', 'bot_id', 'PTT ID start (BOT): encode ID', 'hex', 16),
('fivetone', 'bot_std', 'PTT ID start (BOT): standard', 'list',
(FT_STANDARDS, 0)),
('fivetone', 'bot_long', 'PTT ID start (BOT): tone long', 'list',
FT_TONE_LONG),
('fivetone', 'eot_id', 'PTT ID end (EOT): encode ID', 'hex', 16),
('fivetone', 'eot_std', 'PTT ID end (EOT): standard', 'list',
(FT_STANDARDS, 0)),
('fivetone', 'eot_long', 'PTT ID end (EOT): tone long', 'list',
FT_TONE_LONG),
] + [entry for i in range(1, FT_MSG + 1) for entry in (
('fivetone', 'msg%d_code' % i, 'Message code %d: code' % i, 'hex', 12),
('fivetone', 'msg%d_func' % i, 'Message code %d: function' % i, 'list', (
['Squelch', 'All Call', 'Emergency Alarm', 'Stun', 'Kill', 'Wake Up',
'Group Call'], 0)), # [FiveToneMsgCodeFunc]
('fivetone', 'msg%d_resp' % i, 'Message code %d: response' % i, 'list',
(_RESPONSES, 0)))]))
def _pick(options, value):
return RadioSettingValueList(options, current_index=options.index(value))
def _list_index(stored, offset):
"""List position of a stored value; offset is added to the position,
or is the list of stored values."""
if isinstance(offset, list):
return offset.index(stored) if stored in offset else -1
return stored - offset
def _list_stored(index, offset):
return offset[index] if isinstance(offset, list) else index + offset
def _parse_hz(text, limits, label):
"""'321.7' -> 3217 (0.1 Hz), within limits."""
try:
tenths = round(float(text) * 10)
except ValueError:
tenths = -1
if not limits[0] <= tenths <= limits[1]:
raise errors.InvalidValueError('%s must be %.1f to %.1f Hz' % (
label, limits[0] / 10, limits[1] / 10))
return tenths
def _dtmf_text(raw, chars=DTMF_CHARS):
"""Code bytes (digit values, 0xFF-terminated) -> text."""
text = ''
for b in raw:
if b >= len(chars):
break
text += chars[b]
return text
def _dtmf_bytes(text, length, chars=DTMF_CHARS):
return bytes(chars.index(c) for c in text.upper()).ljust(length, b'\xFF')
def _format_coord(value, limit):
"""A coordinate as the CPS stores it (0x488750, 0x488950): 9 chars,
e.g. "05.500000", "23.000000", "118.00000"; at most `limit`."""
for decimals in (6, 5):
text = '%.*f' % (decimals, value)
if len(text) < 9:
text = '0' + text
if len(text) == 9:
break
if float(text) >= limit:
text = '%.*f' % (6 if limit < 100 else 5, limit)
return text
def _parse_coord(raw):
"""Stored coordinate bytes -> text as the CPS shows it, or ''."""
text = raw.split(b'\x00')[0].split(b'\xFF')[0].decode('latin-1')
try:
float(text)
except ValueError:
return ''
return text[1:] if text.startswith('0') and len(text) > 1 else text
_MENU = [
('zone_list', 'Zone list'), ('new_zone', 'New zone'),
('call_alert', 'Call alert'), ('radio_check', 'Radio check'),
('remote_monitor', 'Remote monitor'), ('radio_enable', 'Radio enable'),
('radio_disable', 'Radio disable'), ('measure_period', 'Measure period'),
('talkaround', 'Talkaround'), ('alert_tone', 'Alert tone'),
('tx_power', 'TX power'), ('start_display', 'Start display'),
('lang_select', 'Language'), ('match_private', 'Match private'),
('match_group', 'Match group'), ('display_mode', 'Display mode'),
('sub_channel_mode', 'Sub channel mode'), ('power_save', 'Power save'),
('gps', 'GPS'), ('aprs', 'APRS'), ('record', 'Record'),
('add_contact', 'Add contact'), ('edit_contact', 'Edit contact'),
('del_contact', 'Delete contact'), ('send_message', 'Send message'),
('functionality', 'Functionality'), ('manual_dial', 'Manual dial'),
('csv_contacts', 'CSV contacts'), ('missed_call', 'Missed calls'),
('answered_call', 'Answered calls'), ('sent_call', 'Sent calls'),
('del_log', 'Delete call log'), ('rx_freq', 'RX frequency'),
('tx_freq', 'TX frequency'), ('ctc_dcs', 'CTC/DCS'),
('tx_contact', 'TX contact'), ('color_code', 'Color code'),
('time_slot', 'Time slot'), ('radio_id', 'Radio ID'),
('radio_name', 'Radio name'), ('channel_type', 'Channel type'),
('tdma_direct', 'TDMA direct mode'), ('rx_group', 'RX group list'),
('add_channel', 'Add channel'), ('channel_name', 'Channel name')]
RADIO_SETTINGS.append(('Menu items', [
('set_menu', f, 'Menu: %s' % label, 'bool', None) for f, label in _MENU]))
# Bits of 0x80 that follow the radio's current display (A/B mode, display
# mode, main line); upload keeps the radio's own values for these.
WORK_STATE_MASK = 0x3E
def zone_offset(z):
"""(page, index) of zone z (1-250), as the CPS computes it (0x482830)."""
return (z - 1) // ZONE_PER_PAGE, (z - 1) % ZONE_PER_PAGE
def channel_offset(n):
"""(page, index) of channel n, as the CPS computes it (0x47dcc0)."""
if n < CH_PER_PAGE:
return 0, n - 1
return n // CH_PER_PAGE, n % CH_PER_PAGE
CHTYPES = ['Analog', 'Digital', 'Fixed Analog', 'Fixed Digital']
# Value lists of the channel options, from the CPS language file sections
# (in brackets); the stored field is the index.
LIST_EXTRAS = {
'rx_squelch_mode': ('RX squelch mode', [ # [RxSquelchMode]
'Carrier/CTC', 'Optional Signaling', 'CTC & Optional Signaling',
'CTC | Optional Signaling']),
'signaling': ('Signaling type', [ # [ChannelSignalingType]
'None', 'DTMF', 'Two Tone', 'Five Tone', 'BDC1200']),
'ptt_id': ('PTT ID', ['Off', 'BOT', 'EOT', 'Both']), # [ChannelPttId]
# [ChannelAprsReport]
'aprs_report': ('APRS report (DMR)', ['Off', 'Digital']),
# one of the 8 report channels of the APRS settings
'aprs_channel': ('APRS report channel', [str(i) for i in range(1, 9)]),
# which two-tone (1-8) or BDC1200 (1-4) entry, per signaling type
# (CPS 0x414470)
'rx_signal': ('RX signaling system', ['None'] + [
str(i) for i in range(1, 9)]),
'tx_signal': ('TX signaling system', ['None'] + [
str(i) for i in range(1, 9)]),
}
# [ChAnaTxAdmit] for analog channels, [ChDigTxAdmit] for digital ones
TX_ADMIT = {False: ['Allow TX', 'Channel Idle', 'Match CTC', 'Non Match CTC'],
True: ['Always', 'Channel Idle', 'Color Code Idle']}
BOOL_EXTRAS = [
('vox', 'VOX'),
('compander', 'Compander'),
('ptt_id_display', 'PTT ID display'),
('lone_work', 'Lone worker'),
('auto_scan', 'Auto scan'),
('aprs_rx', 'APRS receive'),
('aprs_ptt_analog', 'Analog APRS PTT mode'),
('aprs_ptt_digital', 'Digital APRS PTT mode'),
('emerg_indicator', 'Emergency indicator (DMR)'),
('emerg_ack', 'Emergency ACK (DMR)'),
('private_confirm', 'Private call confirm (DMR)'),
('short_data_confirm', 'Short data confirm (DMR)'),
('tdma_direct', 'TDMA direct mode (DMR)'),
]
# CTCSS: tenths of a Hz in BCD (88.5 Hz = 0x0885). DCS: 0x8000 plus the
# code in BCD, 0x4000 if inverted (D754I = 0xC754). No tone: 0xFFFF.
TONE_MODEL = kenwood_tone.KenwoodToneModel(
dcs_base=0x8000, pol_mask=0x4000, tone_init=0xFFFF, tone_flag=0x0000,
dcs_enc_base=16, tone_enc_base=16)
POWER_LEVELS = [chirp_common.PowerLevel('Low', watts=1),
chirp_common.PowerLevel('Middle', watts=2.5),
chirp_common.PowerLevel('High', watts=5)]
STEPS = [2.5, 5.0, 6.25, 10.0, 12.5, 25.0, 50.0, 100.0]
# --- Serial link ------------------------------------------------------------
def _garbled(message):
"""A RadioError for a visibly corrupted reply, with cable advice."""
return errors.RadioError(message + '. ' + CABLE_ADVICE)
def _bad_reply(message, got, expected):
"""A RadioError for a reply header that isn't `expected`; with cable
advice if only bit 7 differs, the bad cables' signature."""
if len(got) == len(expected) and bytes(b & 0x7F for b in got) == expected:
return _garbled(message)
return errors.RadioError(message)
class _Link:
def __init__(self, pipe):
self.pipe = pipe
def send(self, data):
time.sleep(0.01)
self.pipe.reset_input_buffer()
self.pipe.write(data)
def recv(self, n, timeout=0.5):
self.pipe.timeout = timeout
return self.pipe.read(n)
def xfer(self, data, n, timeout=0.5):
self.send(data)
resp = self.recv(n, timeout)
if len(resp) != n:
raise errors.RadioError('No reply from radio to %r' % data[:7])
return resp
def query_v(self, cmd):
"""A V query: the reply is 'V', the index, a length, the data."""
self.send(cmd)
hdr = self.recv(3)
body = self.recv(hdr[2]) if len(hdr) == 3 else b''
if len(hdr) != 3 or hdr[0] != 0x56 or len(body) != hdr[2]:
raise _bad_reply('Bad reply to radio info query %s' % cmd.hex(),
hdr[:1], b'V')
return body
def read_block(self, addr, length, timeout=5.0):
cmd = b'R' + struct.pack('> 24 or start >= end:
raise errors.RadioError('Unexpected codeplug range from radio')
firmware = info[1].decode('ascii', 'replace')
if firmware not in TESTED_FIRMWARE:
LOG.warning('DM-32UV firmware %s has not been tested with this '
'driver (tested: %s)', firmware,
', '.join(TESTED_FIRMWARE))
return firmware, start, end
def _enter_program(link):
if link.xfer(b'G\x00\x00\x00\x00\x01', 0x106)[0] != ord('S'):
raise errors.RadioError('Unexpected reply to G')
link.send(b'\xFF\xFF\xFF\xFF\x0C')
if link.xfer(b'PROGRAM', 1) != b'\x06':
raise errors.RadioError('Radio refused programming mode')
link.xfer(b'\x02', 8)
if link.xfer(b'\x06', 1) != b'\x06':
raise errors.RadioError('Radio did not acknowledge')
def _scan(link, start, end, radio, status):
"""Tag of every page: ({tag: [addresses]}, [free page addresses])."""
pages = list(range(start, end + 1 - PAGE + 1, PAGE))
where = collections.defaultdict(list)
for i, addr in enumerate(pages):
tag = link.read_block(addr + PAGE - 1, 1, timeout=0.5)[0]
if 0x80 <= tag < 0xFF:
# No page tag has bit 7 set (they go up to 0x7c); a bad cable
# sets it, which would make a page look missing.
raise _garbled('Implausible page tag %02x at %06x' % (
tag, addr))
where[tag].append(addr)
status.cur = i
radio.status_fn(status)
return where, where.pop(0xFF, [])
def do_download(radio):
link = _Link(radio.pipe)
firmware, start, end = _identify(link)
LOG.info('DM-32UV firmware %s, codeplug %06x-%06x', firmware, start, end)
_enter_program(link)
status = chirp_common.Status()
status.msg = 'Scanning flash pages'
status.max = (end + 1 - start) // PAGE + len(IMAGE_TAGS)
where, _free = _scan(link, start, end, radio, status)
status.msg = 'Cloning from radio'
base = status.cur + 1
image = bytearray(b'\xFF' * (len(IMAGE_TAGS) * PAGE))
for i, tag in enumerate(IMAGE_TAGS):
if len(where.get(tag, [])) > 1:
LOG.warning('Radio has %d pages with tag %02x; using the first. '
'Upload will refuse until the radio has tidied up.',
len(where[tag]), tag)
if where.get(tag):
data = link.read_block(where[tag][0], PAGE)
image[i * PAGE:(i + 1) * PAGE] = data
status.cur = base + i
radio.status_fn(status)
radio._metadata['dm32uv_firmware'] = firmware
return memmap.MemoryMapBytes(bytes(image))
def _write_page(link, addr, data, start, end):
"""Write one whole page with W, then read it back to check it.
The protocol has no checksums and the firmware doesn't verify writes,
so anything other than an ACK and an exact read-back stops the upload.
"""
if (len(data) != PAGE or addr % PAGE or addr < start or
addr + PAGE - 1 > end or data[-1] in NEVER_WRITE):
raise errors.RadioError('Refusing unsafe write at %06x' % addr)
link.send(b'W' + struct.pack(' 1]
if dupes:
raise errors.RadioError(
'The radio has more than one page with tag %s; not writing' %
', '.join('%02x' % t for t in dupes))
status.msg = 'Cloning to radio'
base = status.cur + 1
written = 0
for i, tag in enumerate(UPLOAD_TAGS):
slot = IMAGE_TAGS.index(tag) * PAGE
want = image[slot:slot + PAGE - 1] + bytes([tag])
# A slot is in use if it holds data, or if it came from the radio
# (tag byte set) and has since been emptied, e.g. all its channels
# deleted: then the radio's page must be emptied too.
if (image[slot:slot + PAGE - 1] != b'\xFF' * (PAGE - 1) or
image[slot + PAGE - 1] == tag and where.get(tag)):
if where.get(tag):
addr = where[tag][0]
current = link.read_block(addr, PAGE)
if tag == ZONE_TAG0:
want = _follow_display(want, current, radio_zone, zones)
elif tag == 0x04:
want = bytearray(want)
want[0x80] = (want[0x80] & ~WORK_STATE_MASK & 0xFF |
current[0x80] & WORK_STATE_MASK)
want = bytes(want)
if current == want:
addr = None
elif free:
addr = free.pop(0)
else:
raise errors.RadioError('No free page left on the radio')
if addr is not None:
_write_page(link, addr, want, start, end)
written += 1
status.cur = base + i
radio.status_fn(status)
return written
# --- Tones ------------------------------------------------------------------
def _shown(name):
"""A name as CHIRP can show it: characters outside its charset as '?'."""
return ''.join(c if c in chirp_common.CHARSET_ASCII else '?'
for c in name)
def _edited(value, old):
"""The name to store for a setting that showed `old`: `old` itself,
byte for byte, unless the user changed it."""
value = str(value).strip()
return old if value == _shown(old).strip() else value
def _name_bytes(name, length):
return name.encode('latin-1', 'replace')[:length].ljust(length, b'\x00')
def _cached(method):
"""Cache a lookup on the image until the driver next changes it (see
DM32UV._forget). Callers must not modify the result."""
@functools.wraps(method)
def wrapper(self, *args):
cache = self.__dict__.setdefault('_lookups', {})
key = (method.__name__,) + args
if key not in cache:
cache[key] = method(self, *args)
return cache[key]
return wrapper
class DM32UVZone(chirp_common.NamedBank):
"""A zone on the radio; its index is the zone number minus 1.
One zone after the last existing one is offered as "New zone"; adding
a channel to it creates it (see DM32UVZoneModel)."""
def _zone(self):
return self._model._radio._zone(self.index + 1)
def get_name(self):
name = str(self._zone().name).rstrip('\x00\xFF ')
if self.index + 1 > self._model._radio._zone_count() and (
not name or not name.isprintable()):
return 'New zone'
return name
def set_name(self, name):
self._zone().name = str(name)[:16].ljust(16, '\x00')
class DM32UVZoneModel(chirp_common.MTOBankModel):
"""The radio's zones. A channel can be in several zones; the order of
channels in a zone is kept, new ones are added at the end.
Zones are numbered 1..count without gaps, so the only zone that can be
created is count + 1: it is offered as a spare, and becomes a real zone
when a channel is added to it. A last zone left empty is removed."""
def __init__(self, radio):
super().__init__(radio, 'Zones')
def get_num_mappings(self):
return min(self._radio._zone_count() + 1, ZONE_COUNT)
def get_mappings(self):
zones = []
for i in range(self.get_num_mappings()):
zone = DM32UVZone(self, '%i' % (i + 1), 'Zone %i' % (i + 1))
zone.index = i
zones.append(zone)
return zones
def add_memory_to_mapping(self, memory, bank):
z = bank.index + 1
if z > self._radio._zone_count():
self._radio._create_zone(z)
members = self._radio._zone_members(z)
if memory.number in members:
return
if len(members) >= ZONE_MEMBERS:
raise errors.RadioError('Zone %s is full (%i channels)' % (
bank.get_name(), ZONE_MEMBERS))
self._radio._set_zone_members(bank.index + 1,
members + [memory.number])
def remove_memory_from_mapping(self, memory, bank):
members = self._radio._zone_members(bank.index + 1)
if memory.number not in members:
raise Exception('Memory %i is not in zone %s' % (
memory.number, bank.get_name()))
self._radio._set_zone_members(
bank.index + 1, [m for m in members if m != memory.number])
self._radio._drop_empty_last_zone()
def get_mapping_memories(self, bank):
return [self._radio.get_memory(n)
for n in self._radio._zone_members(bank.index + 1)]
def get_memory_mappings(self, memory):
zones = []
for z in self._radio._zone_index().get(memory.number, []):
zone = DM32UVZone(self, '%i' % z, 'Zone %i' % z)
zone.index = z - 1
zones.append(zone)
return zones
@directory.register
class DM32UV(chirp_common.CloneModeRadio):
"""Baofeng DM-32UV"""
VENDOR = 'Baofeng'
MODEL = 'DM-32UV'
BAUD_RATE = 115200
_memsize = len(IMAGE_TAGS) * PAGE
@classmethod
def get_prompts(cls):
rp = chirp_common.RadioPrompts()
rp.experimental = (
'This driver is experimental. Upload writes channels, zones, '
'DMR lists, scan lists and radio settings. Passwords, encryption '
'keys and the factory calibration are left as they are on the '
'radio. A radio with a password set is refused.')
rp.pre_download = (
'Switch the radio on and connect the programming cable.\n\n'
'The radio returns to normal by itself a few seconds after '
'the download.')
rp.pre_upload = (
'Before the first upload, download from the radio and save the '
'image as a backup.\n\n'
'Upload writes only the pages that differ from the radio and '
'checks each one by reading it back. It takes about a minute '
'plus a few seconds per changed page.')
return rp
@classmethod
def detect_from_serial(cls, pipe):
"""Check that the connected radio is a DM-32UV."""
_check_model(_search(_Link(pipe)))
return cls
def get_features(self):
rf = chirp_common.RadioFeatures()
rf.memory_bounds = (1, CH_COUNT)
rf.has_bank = True
rf.has_bank_names = True
rf.has_ctone = True
rf.has_cross = True
rf.has_rx_dtcs = True
rf.has_dtcs_polarity = True
rf.has_settings = True
rf.can_odd_split = True
rf.valid_modes = ['FM', 'NFM', 'DMR']
rf.valid_tmodes = ['', 'Tone', 'TSQL', 'DTCS', 'Cross']
rf.valid_cross_modes = ['Tone->Tone', 'DTCS->', '->DTCS', 'Tone->DTCS',
'DTCS->Tone', '->Tone', 'DTCS->DTCS']
rf.valid_duplexes = ['', '-', '+', 'split', 'off']
rf.valid_power_levels = POWER_LEVELS
rf.valid_bands = [(136000000, 174000000), (400000000, 480000000)]
rf.valid_tuning_steps = STEPS
# Frequencies are stored to 10 Hz, independent of the step.
rf.has_nostep_tuning = True
rf.valid_skips = []
rf.valid_name_length = 16
rf.valid_characters = chirp_common.CHARSET_ASCII
return rf
def sync_in(self):
try:
self._mmap = do_download(self)
except errors.RadioError:
raise
except Exception as e:
LOG.exception('Download failed')
raise errors.RadioError('Failed to download from radio: %s' % e)
self.process_mmap()
def sync_out(self):
try:
do_upload(self)
except errors.RadioError:
raise
except Exception as e:
LOG.exception('Upload failed')
raise errors.RadioError('Failed to upload to radio: %s' % e)
def process_mmap(self):
self._memobj = bitwise.parse(MEM_FORMAT, self._mmap)
self._forget()
def _forget(self):
"""Drop cached lookups; called whenever the image changes."""
self._lookups = {}
def _chan(self, number):
page, index = channel_offset(number)
return getattr(self._memobj, 'page%d' % page)[index]
def get_bank_model(self):
return DM32UVZoneModel(self)
def _zone(self, z):
page, index = zone_offset(z)
return getattr(self._memobj, 'zpage%d' % page)[index]
def _zone_count(self):
count = int(self._memobj.zone_hdr.count)
return 0 if count > ZONE_COUNT else count
def _zone_members(self, z):
if z > self._zone_count():
return [] # the spare zone, or erased flash
zone = self._zone(z)
count = min(int(zone.count), ZONE_MEMBERS)
return [int(m) for m in zone.members[0:count]
if 1 <= int(m) <= CH_COUNT]
@_cached
def _zone_index(self):
"""{channel: [zones it is in]}"""
index = collections.defaultdict(list)
for z in range(1, self._zone_count() + 1):
for m in self._zone_members(z):
if z not in index[m]:
index[m].append(z)
return dict(index)
def _set_zone_members(self, z, members):
self._forget()
zone = self._zone(z)
for i in range(ZONE_MEMBERS):
zone.members[i] = members[i] if i < len(members) else 0
zone.count = len(members)
def _create_zone(self, z):
"""Make zone z (which must be count + 1) an empty, named zone."""
if z != self._zone_count() + 1 or z > ZONE_COUNT:
raise errors.RadioError('Zones must be created in order')
self._forget()
zone = self._zone(z)
name = str(zone.name).rstrip('\x00\xFF ')
if not name or not name.isprintable():
zone.name = ('Zone %i' % z).ljust(16, '\x00')
self._set_zone_members(z, [])
self._memobj.zone_hdr.count = z
def _zone_list(self):
"""[(name, [members])] for zones 1..count."""
return [(str(self._zone(z).name).split('\x00')[0].split('\xFF')[0],
self._zone_members(z))
for z in range(1, self._zone_count() + 1)]
def _set_zone_list(self, zones, remap):
"""Rewrite zones 1..len(zones); remap = {old zone: new zone} for the
radio's current-zone pointers (a zone not in it was deleted)."""
self._forget()
old_count = self._zone_count()
for z, (name, members) in enumerate(zones, 1):
zone = self._zone(z)
if str(zone.name).split('\x00')[0].split('\xFF')[0] != name:
zone.name.set_raw(_name_bytes(name, 16))
self._set_zone_members(z, members)
for z in range(len(zones) + 1, old_count + 1):
self._zone(z).name.set_raw(b'\xFF' * 16)
self._set_zone_members(z, [])
hdr = self._memobj.zone_hdr
hdr.count = len(zones)
for zone_f, pos_f in (('a_zone', 'a_pos'), ('b_zone', 'b_pos')):
new = remap.get(int(getattr(hdr, zone_f)))
if not new:
new = 1
setattr(hdr, pos_f, 1)
setattr(hdr, zone_f, new)
members = len(zones[new - 1][1]) if zones else 0
if not 1 <= int(getattr(hdr, pos_f)) <= max(members, 1):
setattr(hdr, pos_f, 1)
def _drop_empty_last_zone(self):
count = self._zone_count()
if count > 1 and not self._zone_members(count):
self._forget()
self._zone(count).name.set_raw(b'\xFF' * 16)
self._memobj.zone_hdr.count = count - 1
def _count(self):
count = int(self._memobj.ch_count)
return 0 if count > CH_COUNT else count
# --- raw page access (DMR lists live outside the bitwise layout) ---------
def _page(self, tag):
base = IMAGE_TAGS.index(tag) * PAGE
return base, self._mmap.get(base, PAGE)
def _put(self, tag, offset, data):
base = IMAGE_TAGS.index(tag) * PAGE
assert offset + len(data) <= PAGE - 1
self._forget()
self._mmap.set(base + offset, bytes(data))
self._mmap.set(base + PAGE - 1, bytes([tag])) # the page now exists
@staticmethod
def _text(raw):
text = raw.split(b'\xFF')[0].split(b'\x00')[0]
return text.decode('latin-1')
# --- radio IDs -----------------------------------------------------------
@_cached
def _radio_ids(self):
"""[(dmr_id, name)] for radio IDs 1..count."""
_base, page = self._page(RADIOID_TAG)
count = page[0] if page[0] <= RADIOID_MAX else 0
return [(int.from_bytes(page[16 * n:16 * n + 3], 'little'),
self._text(page[16 * n + 3:16 * n + 3 + RADIOID_NAME]))
for n in range(1, count + 1)]
def _set_radio_ids(self, entries):
self._put(RADIOID_TAG, 0, bytes([len(entries)]))
for n in range(1, RADIOID_MAX + 1):
if n <= len(entries):
dmr_id, name = entries[n - 1]
rec = (dmr_id.to_bytes(3, 'little') +
_name_bytes(name, RADIOID_NAME) + b'\x00')
else:
rec = b'\x00' * 16
self._put(RADIOID_TAG, 16 * n, rec)
# --- contacts ------------------------------------------------------------
def _contact_loc(self, k):
return (CONTACT_REC_TAG0 + (k - 1) // CONTACT_PER_PAGE,
((k - 1) % CONTACT_PER_PAGE) * CONTACT_REC)
@_cached
def _contacts(self):
"""{slot: (name, dmr_id, call_type index)} for used contact slots."""
_base, index = self._page(CONTACT_INDEX_TAG)
pages = {}
contacts = {}
for k in range(1, CONTACT_MAX + 1):
if index[0x10 + (k - 1) // 8] >> ((k - 1) % 8) & 1:
continue # free slot
tag, off = self._contact_loc(k)
if tag not in pages:
pages[tag] = self._page(tag)[1]
rec = pages[tag][off:off + CONTACT_REC]
ctype = rec[0x16] - 3 if 3 <= rec[0x16] <= 5 else 0
contacts[k] = (self._text(rec[2:2 + CONTACT_NAME]),
int.from_bytes(rec[0x13:0x16], 'little'), ctype)
return contacts
def _set_contacts(self, contacts):
"""Write {slot: (name, id, type)} and rebuild the index page the way
the CPS does (0x474c00): counts, free bitmap, name- and ID-sorted
lists of (slot | call type code << 12)."""
_base, index = self._page(CONTACT_INDEX_TAG)
index = bytearray(index if index[0x10:0x74] != b'\xFF' * 100 or
index[0:2] != b'\xFF\xFF' else b'\x00' * PAGE)
for k, (name, dmr_id, ctype) in contacts.items():
tag, off = self._contact_loc(k)
old = self._page(tag)[1][off:off + CONTACT_REC]
keep = old if old != b'\xFF' * CONTACT_REC else b'\x00' * 24
name_b = _name_bytes(name, CONTACT_NAME)
if self._text(keep[2:2 + CONTACT_NAME]) == name:
name_b = keep[2:2 + CONTACT_NAME] # keep the radio's padding
rec = (keep[0:2] + name_b + b'\x00' +
dmr_id.to_bytes(3, 'little') + bytes([ctype + 3]) +
keep[0x17:0x18])
self._put(tag, off, rec)
bitmap = bytearray(b'\xFF' * 100)
for k in contacts:
bitmap[(k - 1) // 8] &= ~(1 << ((k - 1) % 8)) & 0xFF
index[0:2] = len(contacts).to_bytes(2, 'little')
groups = sum(1 for c in contacts.values() if c[2] == 1)
index[2:4] = groups.to_bytes(2, 'little')
index[4] = sum(1 for c in contacts.values() if c[2] == 2)
index[0x10:0x74] = bitmap
index[0x100:PAGE - 1] = b'\xFF' * (PAGE - 1 - 0x100)
def entry(k):
code = (contacts[k][2] + 3) << 4
return bytes([k & 0xFF, (k >> 8) & 0xF | code])
for pos, k in enumerate(sorted(contacts, key=lambda k: (
contacts[k][0].encode(), k))):
index[0x100 + 2 * pos:0x102 + 2 * pos] = entry(k)
for pos, k in enumerate(sorted(contacts, key=lambda k: (
contacts[k][1], k))):
index[0x740 + 2 * pos:0x742 + 2 * pos] = entry(k)
self._put(CONTACT_INDEX_TAG, 0, index[:PAGE - 1])
def _txc_loc(self, n):
if n < 0x800:
return TXC_TAGS[0], 2 * (n - 1)
if n in (CH_COUNT + 1, CH_COUNT + 2): # VFO A, B
return TXC_TAGS[1], 0xFFA + 2 * (n - CH_COUNT - 1)
return TXC_TAGS[1], 2 * (n & 0x7FF)
def _tx_contact(self, n):
tag, off = self._txc_loc(n)
hi, lo = self._page(tag)[1][off:off + 2]
value = (hi >> 4) << 8 | lo
return 0 if value > CONTACT_MAX else value
def _set_tx_contact(self, n, contact, digital):
tag, off = self._txc_loc(n)
hi = self._page(tag)[1][off]
hi = 0 if hi == 0xFF else hi & 0x0E
self._put(tag, off, bytes([(contact >> 8) << 4 | hi | int(digital),
contact & 0xFF]))
# --- RX group lists ------------------------------------------------------
@_cached
def _rx_groups(self):
"""{n: (name, [member IDs])} for used groups."""
_base, page = self._page(RXG_TAG)
groups = {}
for n in range(1, RXG_MAX + 1):
if page[:4] == b'\xFF' * 4 or not page[(n - 1) // 8] >> (
(n - 1) % 8) & 1:
continue
base = RXG_REC * n - 0x5C
members = [int.from_bytes(page[base + RXG_NAME + 3 * j:][:3],
'little') for j in range(RXG_MEMBERS)]
groups[n] = (self._text(page[base:base + RXG_NAME]),
[m for m in members if 0 < m <= ALL_CALL_ID])
return groups
def _set_rx_groups(self, groups):
_base, page = self._page(RXG_TAG)
head = bytearray(page[:0x10] if page[:4] != b'\xFF' * 4
else b'\x00' * 0x10)
head[0:4] = b'\x00' * 4
for n in groups:
head[(n - 1) // 8] |= 1 << ((n - 1) % 8)
self._put(RXG_TAG, 0, head)
for n in range(1, RXG_MAX + 1):
base = RXG_REC * n - 0x5C
name, members = groups.get(n, ('', []))
data = _name_bytes(name, RXG_NAME)
for j in range(RXG_MEMBERS):
m = members[j] if j < len(members) else 0
data += m.to_bytes(3, 'little')
self._put(RXG_TAG, base, data)
# --- scan lists ----------------------------------------------------------
@_cached
def _scan_lists(self):
"""[(name, [channels], options bytes +0x0c..+0x17)] for 1..count."""
_base, page = self._page(SCAN_TAG)
count = page[0] if page[0] <= SCAN_MAX else 0
lists = []
for n in range(1, count + 1):
rec = page[SCAN_REC * n - 0x38:][:SCAN_REC]
k = min(rec[0x0B], SCAN_MEMBERS)
members = [int.from_bytes(rec[0x18 + 2 * i:0x1A + 2 * i],
'little') for i in range(k)]
lists.append((self._text(rec[:SCAN_NAME]),
[m for m in members if 1 <= m <= CH_COUNT],
rec[0x0C:0x18]))
return lists
def _set_scan_lists(self, lists):
default = lists[0][2] if lists else SCAN_DEFAULT_OPTS
old_count = len(self._scan_lists())
self._put(SCAN_TAG, 0, bytes([len(lists)]))
for n in range(1, SCAN_MAX + 1):
base = SCAN_REC * n - 0x38
if n > len(lists):
if n <= old_count: # a list that was removed
self._put(SCAN_TAG, base, b'\x00' * SCAN_REC)
continue
name, members, opts = lists[n - 1]
opts = bytearray(opts if len(opts) == 12 else default)
members = members[:SCAN_MEMBERS]
designed = int.from_bytes(opts[3:5], 'little')
if members and designed not in members:
opts[3:5] = members[0].to_bytes(2, 'little')
rec = (_name_bytes(name, SCAN_NAME) +
bytes([len(members)]) + bytes(opts) +
b''.join(m.to_bytes(2, 'little') for m in members) +
b'\x00' * (2 * (SCAN_MEMBERS - len(members))))
self._put(SCAN_TAG, base, rec[:SCAN_REC])
def _remove_from_scan_lists(self, number):
lists = self._scan_lists()
new = []
for name, members, opts in lists:
members = [m for m in members if m != number]
if not members and int.from_bytes(opts[3:5], 'little') == number:
# the designed channel; the factory lists use channel 1
opts = opts[:3] + SCAN_DEFAULT_OPTS[3:5] + opts[5:]
new.append((name, members, opts))
if new != lists:
self._set_scan_lists(new)
# --- Settings tab: DMR lists ---------------------------------------------
def get_settings(self):
dmr = RadioSettingGroup('dmr', 'DMR lists')
ids = RadioSettingGroup('radio_ids', 'Radio IDs')
entries = self._radio_ids()
for n in range(1, min(len(entries) + 8, RADIOID_MAX) + 1):
dmr_id, name = entries[n - 1] if n <= len(entries) else (0, '')
ids.append(RadioSetting(
'rid_%d_id' % n, 'Radio ID %d: DMR ID (0 = unused)' % n,
RadioSettingValueInteger(0, DMR_ID_MAX,
min(dmr_id, DMR_ID_MAX))))
ids.append(RadioSetting(
'rid_%d_name' % n, 'Radio ID %d: name' % n,
RadioSettingValueString(0, RADIOID_NAME, _shown(name),
autopad=False)))
contacts = RadioSettingGroup('contacts', 'Contacts')
used = self._contacts()
free = [k for k in range(1, CONTACT_MAX + 1) if k not in used]
for k in sorted(used) + free[:10]:
name, dmr_id, ctype = used.get(k, ('', 1, 1))
contacts.append(RadioSetting(
'con_%d_name' % k, 'Contact %d: name (empty = unused)' % k,
RadioSettingValueString(0, CONTACT_NAME, _shown(name),
autopad=False)))
contacts.append(RadioSetting(
'con_%d_id' % k, 'Contact %d: DMR ID' % k,
RadioSettingValueInteger(1, ALL_CALL_ID, max(dmr_id, 1))))
contacts.append(RadioSetting(
'con_%d_type' % k, 'Contact %d: call type' % k,
RadioSettingValueList(CALL_TYPES, current_index=ctype)))
groups = RadioSettingGroup('rx_groups', 'RX group lists')
by_id = {c[1]: _shown(c[0]) for c in used.values()}
# CHIRP keeps showing this tree after later edits, so remember the
# names shown here: a member name stays valid after a rename.
self._shown_contact_ids = {_shown(c[0]): c[1] for c in used.values()}
existing = self._rx_groups()
spare = [n for n in range(1, RXG_MAX + 1) if n not in existing][:3]
for n in sorted(existing) + spare:
name, members = existing.get(n, ('', []))
groups.append(RadioSetting(
'rxg_%d_name' % n, 'RX group %d: name (empty = unused)' % n,
RadioSettingValueString(0, RXG_NAME, _shown(name),
autopad=False)))
groups.append(RadioSetting(
'rxg_%d_members' % n,
'RX group %d: contacts (names or IDs, comma separated)' % n,
RadioSettingValueString(
0, 600, ', '.join(by_id.get(m, str(m)) for m in members),
autopad=False, charset=chirp_common.CHARSET_ASCII)))
dmr.append(ids)
dmr.append(contacts)
dmr.append(groups)
scan = RadioSettingGroup('scan', 'Scan lists')
lists = self._scan_lists()
for n in range(1, min(len(lists) + 1, SCAN_MAX) + 1):
name, members, opts = lists[n - 1] if n <= len(lists) else (
'', [], bytes(12))
scan.append(RadioSetting(
'scan_%d_name' % n, 'Scan list %d: name (empty = unused)' % n,
RadioSettingValueString(0, SCAN_NAME, _shown(name),
autopad=False)))
scan.append(RadioSetting(
'scan_%d_members' % n,
'Scan list %d: channels (numbers, comma separated)' % n,
RadioSettingValueString(0, 100, ', '.join(map(str, members)),
autopad=False)))
scan.append(RadioSetting(
'scan_%d_ctc' % n, 'Scan list %d: CTC scan mode' % n,
RadioSettingValueList(SCAN_CTC_MODES, current_index=min(
opts[0] & 0xF, len(SCAN_CTC_MODES) - 1))))
scan.append(RadioSetting(
'scan_%d_tx' % n, 'Scan list %d: scan TX mode' % n,
RadioSettingValueList(SCAN_TX_MODES, current_index=min(
opts[0] >> 4, len(SCAN_TX_MODES) - 1))))
zones = RadioSettingGroup('zones', 'Zones')
zlist = self._zone_list()
zones.append(RadioSetting(
'zone_order', 'Zone order (zone numbers, comma separated; '
'reopen the image before using the Banks tab again)',
RadioSettingValueString(0, ZONE_ORDER_MAX, ', '.join(
str(z) for z in range(1, len(zlist) + 1)), autopad=False)))
for z, (name, members) in enumerate(zlist, 1):
zones.append(RadioSetting(
'zone_%d_name' % z, 'Zone %d: name' % z,
RadioSettingValueString(0, 16, _shown(name),
autopad=False)))
zones.append(RadioSetting(
'zone_%d_members' % z,
'Zone %d: channels in order (empty = delete zone)' % z,
RadioSettingValueString(0, 400, ', '.join(map(str, members)),
autopad=False)))
groups_out = [dmr, scan, zones]
if self._has_page(DTMF_TAG):
groups_out.append(self._dtmf_contacts_group())
if self._has_page(SIGNAL_TAG):
groups_out.append(self._tt_encode_group())
groups_out.append(self._ft_special_group())
radio = self._radio_settings()
if len(radio):
groups_out.insert(0, radio)
return RadioSettings(*groups_out)
def _tt_encode(self):
"""[(name, single tone, tone 1, tone 2)] of two-tone encode
entries 1..count (tones in 0.1 Hz)."""
count = int(self._memobj.tt_encode_count)
count = count if 1 <= count <= TT_ENCODE else 1 # as the CPS
out = []
for e in list(self._memobj.tt_encode)[:count]:
raw = e.name.get_raw()
name = raw.decode('utf-16-le', 'replace').split('\x00')[0]
if raw[:2] == b'\xFF\xFF':
name = ''
out.append((name, bool(e.single_tone), int(e.tone1),
int(e.tone2)))
return out
def _tt_encode_group(self):
group = RadioSettingGroup('tt_encode', 'Two-tone encode')
entries = self._tt_encode()
for n in range(1, min(len(entries) + 4, TT_ENCODE) + 1):
name, single, t1, t2 = entries[n - 1] if n <= len(entries) \
else ('', False, 0, 0)
group.append(RadioSetting(
'tte_%d_name' % n, 'Encode %d: name (empty = unused)' % n,
RadioSettingValueString(0, 16, _shown(name), autopad=False)))
group.append(RadioSetting(
'tte_%d_single' % n, 'Encode %d: send' % n,
RadioSettingValueList(['Dual Tone', 'Single Tone'],
current_index=int(single))))
for k, tone in ((1, t1), (2, t2)):
group.append(RadioSetting(
'tte_%d_tone%d' % (n, k), 'Encode %d: tone %d (Hz)' % (
n, k),
RadioSettingValueString(
0, 7, '%.1f' % (tone / 10)
if TT_HZ[0] <= tone <= TT_HZ[1] else '',
autopad=False, charset='0123456789.')))
return group
def _set_tt_encode(self, values):
old = self._tt_encode()
new = list(old)
for n in range(1, TT_ENCODE + 1):
if 'tte_%d_name' % n not in values:
continue
o = old[n - 1] if n <= len(old) else ('', False, 0, 0)
name = _edited(values['tte_%d_name' % n], o[0])
tones = []
for k in (1, 2):
text = str(values['tte_%d_tone%d' % (n, k)]).strip()
shown = '%.1f' % (o[1 + k] / 10) \
if TT_HZ[0] <= o[1 + k] <= TT_HZ[1] else ''
tones.append(o[1 + k] if text == shown else _parse_hz(
text, TT_HZ, 'Two-tone encode %d, tone %d' % (n, k)))
single = str(values['tte_%d_single' % n]) == 'Single Tone'
while len(new) < n:
new.append(('', False, 0, 0))
new[n - 1] = (name, single, tones[0], tones[1])
while len(new) > 1 and not new[-1][0]:
new.pop()
if new == old:
return
self._forget()
for n, (name, single, t1, t2) in enumerate(new, 1):
e = self._memobj.tt_encode[n - 1]
if n > len(old) or old[n - 1][0] != name:
e.name.set_raw(
name.encode('utf-16-le')[:32].ljust(32, b'\x00'))
if n > len(old) and e.get_raw()[0x20:] == b'\xFF' * 8:
e.set_raw(e.get_raw()[:0x20] + b'\xFE\xFF' + b'\xFF' * 6)
e.single_tone = int(single)
e.tone1, e.tone2 = t1, t2
for n in range(len(new) + 1, len(old) + 1):
self._memobj.tt_encode[n - 1].set_raw(b'\x00' * 0x20 +
b'\xFF' * 8)
self._memobj.tt_encode_count = len(new)
def _ft_special(self):
"""[{field: shown value}] of the 32 five-tone special calls."""
out = []
for e in self._memobj.ft_special:
types, stored = FT_SPECIAL_TYPES
t = int(e.type)
out.append({
'type': types[stored.index(t)] if t in stored else 'Off',
'code': _dtmf_text(e.code.get_raw(), CODE_CHARS['hex']),
'delimiter': FT_DELIMITERS[int(e.delimiter)]
if int(e.delimiter) < len(FT_DELIMITERS) else
FT_DELIMITERS[0],
'standard': FT_STANDARDS[int(e.standard)]
if int(e.standard) < len(FT_STANDARDS) else FT_STANDARDS[0],
'tone_long': FT_TONE_LONG[0][int(e.tone_long) - 3]
if 3 <= int(e.tone_long) < 3 + len(FT_TONE_LONG[0])
else FT_TONE_LONG[0][0],
'data': _dtmf_text(e.data.get_raw(), CODE_CHARS['hex']),
'name': self._text(e.name.get_raw()),
})
return out
def _ft_special_group(self):
group = RadioSettingGroup('ft_special', 'Five-tone special calls')
hexchars = CODE_CHARS['hex'] + CODE_CHARS['hex'].lower()
calls = self._ft_special()
used = [n for n, c in enumerate(calls, 1) if c['type'] != 'Off']
spare = [n for n in range(1, FT_SPECIAL + 1) if n not in used][:3]
for n in used + spare:
c = calls[n - 1]
label = 'Special call %d: ' % n
for key, text, value in (
('type', 'call type (Off = unused)', _pick(
FT_SPECIAL_TYPES[0], c['type'])),
('name', 'name', RadioSettingValueString(
0, 16, _shown(c['name']), autopad=False)),
('code', 'other side code', RadioSettingValueString(
0, 5, c['code'], autopad=False, charset=hexchars)),
('delimiter', 'middle delimiter', _pick(
FT_DELIMITERS, c['delimiter'])),
('standard', 'decode standard', _pick(
FT_STANDARDS, c['standard'])),
('tone_long', 'tone long', _pick(
FT_TONE_LONG[0], c['tone_long'])),
('data', 'data transmission', RadioSettingValueString(
0, 16, c['data'], autopad=False, charset=hexchars))):
group.append(RadioSetting('fts_%d_%s' % (n, key),
label + text, value))
return group
def _set_ft_special(self, values):
calls = self._ft_special()
for n in range(1, FT_SPECIAL + 1):
if 'fts_%d_type' % n not in values:
continue
c, e = calls[n - 1], self._memobj.ft_special[n - 1]
new = {k: str(values['fts_%d_%s' % (n, k)]).strip()
for k in c}
if new['name'] == _shown(c['name']).strip():
new['name'] = c['name']
for k in ('code', 'data'):
new[k] = new[k].upper()
if new == c:
continue
self._forget()
if new['type'] != c['type']:
types, stored = FT_SPECIAL_TYPES
e.type = stored[types.index(new['type'])]
if new['code'] != c['code']:
e.code.set_raw(_dtmf_bytes(new['code'], 5, CODE_CHARS['hex']))
if new['data'] != c['data']:
e.data.set_raw(_dtmf_bytes(new['data'], 16,
CODE_CHARS['hex']))
if new['name'] != c['name']:
e.name.set_raw((new['name'].encode('latin-1', 'replace')[:16]
+ b'\x00')[:16].ljust(16, b'\xFF'))
if new['delimiter'] != c['delimiter']:
e.delimiter = FT_DELIMITERS.index(new['delimiter'])
if new['standard'] != c['standard']:
e.standard = FT_STANDARDS.index(new['standard'])
if new['tone_long'] != c['tone_long']:
e.tone_long = FT_TONE_LONG[0].index(new['tone_long']) + 3
def _has_page(self, tag):
return self._page(tag)[1][:PAGE - 1] != b'\xFF' * (PAGE - 1)
def _dtmf_contacts(self):
"""[(name, number)] of DTMF (analog) contacts 1..count."""
count = int(self._memobj.dtmf_contact_count)
count = count if count <= DTMF_CONTACTS else 0
return [(self._text(c.name.get_raw()),
_dtmf_text(c.number.get_raw(), '0123456789'))
for c in list(self._memobj.dtmf_contacts)[:count]]
def _dtmf_contacts_group(self):
group = RadioSettingGroup('dtmf_contacts', 'DTMF contacts')
contacts = self._dtmf_contacts()
for n in range(1, min(len(contacts) + 4, DTMF_CONTACTS) + 1):
name, number = contacts[n - 1] if n <= len(contacts) else ('', '')
group.append(RadioSetting(
'dtc_%d_name' % n, 'DTMF contact %d: name' % n,
RadioSettingValueString(0, 16, _shown(name), autopad=False)))
group.append(RadioSetting(
'dtc_%d_number' % n, 'DTMF contact %d: number (up to 5 '
'digits)' % n,
RadioSettingValueString(0, 5, number, autopad=False,
charset='0123456789')))
return group
def _set_dtmf_contacts(self, values):
old = self._dtmf_contacts()
new = list(old)
for n in range(1, DTMF_CONTACTS + 1):
if 'dtc_%d_name' % n not in values:
continue
old_name = old[n - 1][0] if n <= len(old) else ''
name = _edited(values['dtc_%d_name' % n], old_name)
number = str(values['dtc_%d_number' % n]).strip()
while len(new) < n:
new.append(('', ''))
new[n - 1] = (name, number)
while new and not new[-1][0]:
new.pop()
for n, (name, number) in enumerate(new, 1):
if not name:
raise errors.InvalidValueError(
'DTMF contact %d: a name is needed (only the last '
'contacts can be removed)' % n)
if new == old:
return
self._forget()
for n, (name, number) in enumerate(new, 1):
c = self._memobj.dtmf_contacts[n - 1]
if n > len(old) or old[n - 1][0] != name:
c.name.set_raw(_name_bytes(name, 16))
if n > len(old) or old[n - 1][1] != number:
c.number.set_raw(_dtmf_bytes(number, 5, '0123456789'))
if n > len(old) and c.unknown.get_raw() == b'\xFF' * 11:
c.unknown.set_raw(b'\x00' * 11)
for n in range(len(new) + 1, len(old) + 1):
self._memobj.dtmf_contacts[n - 1].set_raw(b'\xFF' * 0x20)
self._memobj.dtmf_contact_count = len(new)
def _radio_settings(self):
top = RadioSettingGroup('radio', 'Radio settings')
for title, entries in RADIO_SETTINGS:
group = RadioSettingGroup('radio_%s' % title.lower().replace(
' ', '_'), title)
for sname, field, label, kind, extra in entries:
if not self._has_page(STRUCT_TAGS.get(sname, 0x04)):
continue
obj = getattr(self._memobj, sname)
name = 'set_%s_%s' % (sname, field)
if kind == 'bool':
value = RadioSettingValueBoolean(bool(getattr(obj,
field)))
elif kind == 'text':
text = str(getattr(obj, field))
text = text.split('\x00')[0].split('\xFF')[0]
text = ''.join(c for c in text
if c in chirp_common.CHARSET_ASCII)
value = RadioSettingValueString(0, extra, text,
autopad=False)
elif kind == 'coord':
value = RadioSettingValueString(
0, 12, _parse_coord(getattr(obj, field).get_raw()),
autopad=False)
elif kind == 'int':
lo, hi = extra
number = int(getattr(obj, field))
value = RadioSettingValueInteger(
lo, hi, number if lo <= number <= hi else lo)
elif kind == 'channel':
name = 'set_%s_%s_%d' % (sname, field, extra + 1)
value = self._channel_choice(
int(getattr(obj, field)[extra]))
elif kind == 'hz':
tenths = int(getattr(obj, field))
value = RadioSettingValueString(
0, 7, '%.1f' % (tenths / 10)
if extra[0] <= tenths <= extra[1] else '',
autopad=False, charset='0123456789.')
elif kind == 'dtmf':
value = RadioSettingValueString(
0, extra, _dtmf_text(getattr(obj, field).get_raw()),
autopad=False, charset=DTMF_CHARS + 'abcd')
elif kind in ('digits', 'hex'):
chars = CODE_CHARS[kind]
value = RadioSettingValueString(
0, extra, _dtmf_text(getattr(obj, field).get_raw(),
chars),
autopad=False, charset=chars + chars.lower())
else:
options, offset = extra
index = _list_index(int(getattr(obj, field)), offset)
value = RadioSettingValueList(options, current_index=(
index if 0 <= index < len(options) else 0))
group.append(RadioSetting(name, label, value))
if len(group):
top.append(group)
return top
def _channel_choice(self, value):
"""Report channel choice: the current channel, or a digital one."""
options = ['Current Channel'] + self._digital_channels()
current = 'Current Channel' if not value else '%d: %s' % (
value, self._channel_names().get(value, '(empty)'))
if current not in options:
options.append(current)
return RadioSettingValueList(options,
current_index=options.index(current))
@_cached
def _channel_names(self):
"""{number: name} of the channels in use."""
names = {}
for n in range(1, self._count() + 1):
_mem = self._chan(n)
if _mem.rxfreq.get_raw() not in (b'\xFF' * 4, b'\x00' * 4):
names[n] = _shown(str(_mem.name).rstrip('\x00\xFF '))
return names
@_cached
def _digital_channels(self):
names = self._channel_names()
return ['%d: %s' % (n, names[n]) for n in names
if self._chan(n).chtype in (1, 3)]
@staticmethod
def _set_coord(field, label, limit, text):
"""Store a coordinate typed on the Settings tab, if it changed."""
if text == _parse_coord(field.get_raw()):
return
if not text:
field.set_raw(b'\x00' * 9)
return
try:
value = float(text)
except ValueError:
value = -1
if not 0 <= value <= limit:
raise errors.InvalidValueError(
'%s must be a number from 0 to %d' % (label, limit))
field.set_raw(_format_coord(value, limit).encode())
def set_settings(self, settings):
values = {}
def walk(group):
for element in group:
if isinstance(element, RadioSetting):
values[element.get_name()] = element.value
else:
walk(element)
walk(settings)
for title, entries in RADIO_SETTINGS:
for sname, field, label, kind, extra in entries:
name = 'set_%s_%s' % (sname, field)
if kind == 'channel':
name = 'set_%s_%s_%d' % (sname, field, extra + 1)
if name not in values:
continue
obj = getattr(self._memobj, sname)
if kind == 'coord':
self._set_coord(getattr(obj, field), label, extra,
str(values[name]).strip())
elif kind == 'int':
lo, hi = extra
number = int(getattr(obj, field))
new = int(values[name])
if new != (number if lo <= number <= hi else lo):
setattr(obj, field, new)
elif kind == 'channel':
choice = str(values[name])
new = int(choice.split(':')[0]) if ':' in choice else 0
if new != int(getattr(obj, field)[extra]):
getattr(obj, field)[extra] = new
elif kind == 'bool':
setattr(obj, field, int(bool(values[name])))
elif kind == 'text':
text = str(values[name]).rstrip()
old = str(getattr(obj, field)).split('\x00')[0]
if text != old.split('\xFF')[0]:
setattr(obj, field, text[:extra].ljust(extra, '\x00'))
elif kind == 'hz':
tenths = int(getattr(obj, field))
text = str(values[name]).strip()
shown = '%.1f' % (tenths / 10) \
if extra[0] <= tenths <= extra[1] else ''
if text != shown:
setattr(obj, field, _parse_hz(text, extra, label))
elif kind in CODE_CHARS:
chars = CODE_CHARS[kind]
raw = getattr(obj, field)
text = str(values[name]).strip().upper()
if text != _dtmf_text(raw.get_raw(), chars):
if kind == 'digits':
# the CPS pads with leading zeros (0x47be70)
raw.set_raw(bytes(chars.index(c) for c in
text.rjust(extra, '0')))
else:
raw.set_raw(_dtmf_bytes(text, extra, chars))
else:
options, offset = extra
new = options.index(str(values[name]))
shown = _list_index(int(getattr(obj, field)), offset)
if not 0 <= shown < len(options):
shown = 0 # displayed as the first option
if new != shown: # don't rewrite what wasn't changed
setattr(obj, field, _list_stored(new, offset))
self._forget()
if self._has_page(DTMF_TAG):
self._set_dtmf_contacts(values)
if self._has_page(SIGNAL_TAG):
self._set_tt_encode(values)
self._set_ft_special(values)
# Radio IDs: keep the ones with an ID, in order; renumber channels.
old = self._radio_ids()
kept, remap = [], {}
for n in range(1, RADIOID_MAX + 1):
if 'rid_%d_id' % n not in values:
if n <= len(old):
kept.append(old[n - 1])
remap[n] = len(kept)
continue
dmr_id = int(values['rid_%d_id' % n])
old_id, old_name = old[n - 1] if n <= len(old) else (0, '')
if dmr_id == min(old_id, DMR_ID_MAX):
dmr_id = old_id # shown clamped, not changed
if dmr_id:
kept.append((dmr_id, _edited(values['rid_%d_name' % n],
old_name)))
remap[n] = len(kept)
if [e for e in kept] != old or any(k != v for k, v in remap.items()):
self._set_radio_ids(kept)
for number in range(1, self._count() + 1):
_mem = self._chan(number)
if int(_mem.radio_id):
_mem.radio_id = remap.get(int(_mem.radio_id), 0)
# Contacts: an empty name frees the slot.
old_contacts = self._contacts()
contacts = dict(old_contacts)
# RX group members are shown by name; accept the names shown when
# the settings were read and those from before this edit too (a
# renamed contact keeps its ID).
by_name = dict(getattr(self, '_shown_contact_ids', {}))
by_name.update({_shown(c[0]): c[1] for c in contacts.values()})
for k in range(1, CONTACT_MAX + 1):
if 'con_%d_name' % k not in values:
continue
old_name, old_id, _t = contacts.get(k, ('', 1, 0))
name = _edited(values['con_%d_name' % k], old_name)
if name:
ctype = CALL_TYPES.index(str(values['con_%d_type' % k]))
dmr_id = int(values['con_%d_id' % k])
if dmr_id == max(old_id, 1):
dmr_id = old_id # shown clamped, not changed
contacts[k] = (name, dmr_id, ctype)
else:
contacts.pop(k, None)
if contacts != old_contacts:
self._set_contacts(contacts)
for number in range(1, self._count() + 1):
if self._tx_contact(number) and \
self._tx_contact(number) not in contacts:
self._set_tx_contact(number, 0,
self._chan(number).chtype in (1, 3))
# RX groups: members by contact name or number.
by_name.update({_shown(c[0]): c[1] for c in contacts.values()})
old_groups = self._rx_groups()
groups = dict(old_groups)
for n in range(1, RXG_MAX + 1):
if 'rxg_%d_name' % n not in values:
continue
name = _edited(values['rxg_%d_name' % n],
groups.get(n, ('', []))[0])
if not name:
groups.pop(n, None)
continue
members = []
for item in str(values['rxg_%d_members' % n]).split(','):
item = item.strip()
if not item:
continue
if item in by_name:
members.append(by_name[item])
elif item.isdigit() and 0 < int(item) <= ALL_CALL_ID:
members.append(int(item))
else:
raise errors.InvalidValueError(
'RX group %s: unknown contact %r' % (name, item))
if len(members) > RXG_MEMBERS:
raise errors.InvalidValueError(
'RX group %s: at most %d contacts' % (name, RXG_MEMBERS))
groups[n] = (name, members)
if groups != old_groups:
self._set_rx_groups(groups)
# Zones: names, member order, deletion, zone order.
if 'zone_order' in values:
old = self._zone_list()
edited = {}
for z, (name, members) in enumerate(old, 1):
name = _edited(values.get('zone_%d_name' % z, _shown(name)),
name)
text = values.get('zone_%d_members' % z)
if text is not None:
members = []
for item in str(text).split(','):
item = item.strip()
if not item:
continue
if not item.isdigit() or not \
1 <= int(item) <= self._count():
raise errors.InvalidValueError(
'Zone %s: %r is not a channel in use' % (
name, item))
members.append(int(item))
if len(members) > ZONE_MEMBERS:
raise errors.InvalidValueError(
'Zone %s: at most %d channels' % (
name, ZONE_MEMBERS))
if members:
edited[z] = (name, members)
order = []
for item in str(values['zone_order']).split(','):
item = item.strip()
if not item:
continue
if not item.isdigit() or int(item) not in range(
1, len(old) + 1) or int(item) in order:
raise errors.InvalidValueError(
'Zone order: %r is not a zone number' % item)
order.append(int(item))
order = [z for z in order if z in edited] + [
z for z in edited if z not in order]
if not order:
raise errors.InvalidValueError('At least one zone must remain')
new = [edited[z] for z in order]
if new != old:
self._set_zone_list(new, {z: i for i, z in enumerate(
order, 1)})
# Scan lists: an empty name removes the list; lists stay numbered
# 1..count, so channels pointing at a later list are renumbered.
old = self._scan_lists()
new, remap = [], {}
for n in range(1, SCAN_MAX + 1):
if 'scan_%d_name' % n not in values:
if n <= len(old):
new.append(old[n - 1])
remap[n] = len(new)
continue
name = _edited(values['scan_%d_name' % n],
old[n - 1][0] if n <= len(old) else '')
if not name:
continue
members = []
for item in str(values['scan_%d_members' % n]).split(','):
item = item.strip()
if not item:
continue
if not item.isdigit() or not 1 <= int(item) <= CH_COUNT:
raise errors.InvalidValueError(
'Scan list %s: %r is not a channel number' % (
name, item))
members.append(int(item))
if len(members) > SCAN_MEMBERS:
raise errors.InvalidValueError(
'Scan list %s: at most %d channels' % (name, SCAN_MEMBERS))
opts = bytearray(old[n - 1][2] if n <= len(old) else (
old[0][2] if old else SCAN_DEFAULT_OPTS))
tx_mode = SCAN_TX_MODES.index(str(values['scan_%d_tx' % n]))
ctc_mode = SCAN_CTC_MODES.index(str(values['scan_%d_ctc' % n]))
opts[0] = tx_mode << 4 | ctc_mode
new.append((name, members, bytes(opts)))
remap[n] = len(new)
if new != old:
self._set_scan_lists(new)
for number in range(1, self._count() + 1):
_mem = self._chan(number)
if int(_mem.scanlist):
_mem.scanlist = remap.get(int(_mem.scanlist), 0)
def get_raw_memory(self, number):
return repr(self._chan(number))
def get_memory(self, number):
_mem = self._chan(number)
mem = chirp_common.Memory()
mem.number = number
rx = _mem.rxfreq.get_raw()
if number > self._count() or rx in (b'\xFF' * 4, b'\x00' * 4):
mem.empty = True
return mem
mem.freq = int(_mem.rxfreq) * 10
if _mem.txfreq.get_raw() == b'\xFF' * 4:
mem.duplex = 'off'
else:
chirp_common.split_to_offset(mem, mem.freq,
int(_mem.txfreq) * 10)
mem.name = str(_mem.name).rstrip('\x00\xFF ')
digital = _mem.chtype in (1, 3)
mem.mode = 'DMR' if digital else ('FM' if _mem.wide else 'NFM')
mem.power = POWER_LEVELS[min(int(_mem.power), 2)]
if _mem.step < len(STEPS):
mem.tuning_step = STEPS[_mem.step]
if not digital:
TONE_MODEL.get_tone(_mem, mem)
mem.extra = self._get_extra(_mem, number)
return mem
@_cached
def _dmr_names(self, kind):
"""{index: name} of the entries in one of the DMR_LISTS."""
tag, first, size, length, count = DMR_LISTS[kind]
_base, page = self._page(tag)
names = {}
for n in range(1, count + 1):
name = self._text(page[first + (n - 1) * size:][:length])
if name:
names[n] = name
return names
@_cached
def _choice_names(self, kind):
"""({index: shown name}, name of 0) for _dmr_choice."""
if kind == 'radio_id':
names = {n: '%s (%d)' % (name, dmr_id) for n, (dmr_id, name)
in enumerate(self._radio_ids(), 1)}
none = 'Default'
elif kind == 'tx_contact':
names = {k: c[0] for k, c in self._contacts().items()}
none = 'None'
elif kind == 'rxgroup':
names = {n: g[0] for n, g in self._rx_groups().items()}
none = 'None'
elif kind == 'scanlist':
names = {n: sl[0] for n, sl in enumerate(self._scan_lists(), 1)}
none = 'None'
else:
names, none = self._dmr_names(kind), 'None'
return {n: _shown(name) for n, name in names.items()}, none
def _dmr_choice(self, kind, value):
"""A RadioSettingValueList for a DMR list index (0 = none)."""
names, none = self._choice_names(kind)
options = [none] + ['%d: %s' % kv for kv in sorted(names.items())]
current = none if not value else '%d: %s' % (
value, names.get(value, '(unnamed)'))
if current not in options:
options.append(current)
return RadioSettingValueList(options,
current_index=options.index(current))
def _get_extra(self, _mem, number):
extra = RadioSettingGroup('extra', 'Extra')
extra.append(RadioSetting(
'chtype', 'Channel type',
RadioSettingValueList(CHTYPES, current_index=min(
int(_mem.chtype), len(CHTYPES) - 1))))
extra.append(RadioSetting(
'colorcode', 'Color code (DMR)',
RadioSettingValueInteger(0, 15, int(_mem.colorcode))))
extra.append(RadioSetting(
'timeslot', 'Time slot (DMR)',
RadioSettingValueList(['1', '2'],
current_index=int(_mem.timeslot))))
extra.append(RadioSetting(
'tx_contact', 'TX contact (DMR)',
self._dmr_choice('tx_contact', self._tx_contact(number))))
extra.append(RadioSetting(
'radio_id', 'Radio ID (DMR)',
self._dmr_choice('radio_id', int(_mem.radio_id))))
extra.append(RadioSetting(
'rxgroup', 'RX group list (DMR)',
self._dmr_choice('rxgroup', int(_mem.rxgroup))))
extra.append(RadioSetting(
'encrypt', 'Encryption (DMR)',
RadioSettingValueBoolean(bool(_mem.encrypt))))
extra.append(RadioSetting(
'privacy', 'Encryption key (DMR)',
self._dmr_choice('privacy', int(_mem.privacy))))
extra.append(RadioSetting(
'scanlist', 'Scan list',
self._dmr_choice('scanlist', int(_mem.scanlist))))
extra.append(RadioSetting(
'squelch', 'Squelch level',
RadioSettingValueInteger(0, 9, min(int(_mem.squelch), 9))))
extra.append(RadioSetting(
'forbid_tx', 'Forbid TX',
RadioSettingValueBoolean(bool(_mem.forbid_tx))))
extra.append(RadioSetting(
'forbid_talkaround', 'Forbid talkaround',
RadioSettingValueBoolean(bool(_mem.forbid_talkaround))))
admit = TX_ADMIT[_mem.chtype in (1, 3)]
extra.append(RadioSetting(
'tx_admit', 'TX admit',
RadioSettingValueList(admit, current_index=min(
int(_mem.tx_admit), len(admit) - 1))))
for name, (label, options) in LIST_EXTRAS.items():
extra.append(RadioSetting(
name, label, RadioSettingValueList(options, current_index=min(
int(getattr(_mem, name)), len(options) - 1))))
extra.append(RadioSetting(
'emerg_system', 'Emergency system (DMR)',
self._dmr_choice('emergency', int(_mem.emerg_system))))
for name, label in BOOL_EXTRAS:
extra.append(RadioSetting(
name, label,
RadioSettingValueBoolean(bool(getattr(_mem, name)))))
return extra
def set_memory(self, mem):
_mem = self._chan(mem.number)
if mem.empty:
_mem.set_raw(b'\xFF' * CH_SIZE)
self._update_tx_contact(mem.number, 0, False)
# Zones must not point at a channel that no longer exists.
for z in range(1, self._zone_count() + 1):
members = self._zone_members(z)
if mem.number in members:
self._set_zone_members(
z, [m for m in members if m != mem.number])
self._drop_empty_last_zone()
self._remove_from_scan_lists(mem.number)
if mem.number == self._count():
count = mem.number - 1
while count and self.get_memory(count).empty:
count -= 1
self._memobj.ch_count = count
return
if _mem.get_raw() in (b'\xFF' * CH_SIZE, b'\x00' * CH_SIZE):
# Defaults as on the channels the radio came with.
_mem.set_raw(b'\xFF' * 16 + b'\x00' * 8 + bytes.fromhex(
'0000000030000000' '00ffffffff000000' '0000000000000000'))
if mem.number > self._count():
# Channels skipped over become part of 1..count: make sure they
# are empty, not left-over template data (factory records past
# the count hold e.g. 400.000 MHz and would show up).
for gap in range(self._count() + 1, mem.number):
self._chan(gap).set_raw(b'\xFF' * CH_SIZE)
self._memobj.ch_count = mem.number
_mem.name = mem.name.ljust(16, '\x00')[:16]
_mem.rxfreq = mem.freq // 10
if mem.duplex == 'off':
_mem.txfreq.fill_raw(b'\xFF')
elif mem.duplex == 'split':
_mem.txfreq = mem.offset // 10
elif mem.duplex == '+':
_mem.txfreq = (mem.freq + mem.offset) // 10
elif mem.duplex == '-':
_mem.txfreq = (mem.freq - mem.offset) // 10
else:
_mem.txfreq = mem.freq // 10
digital = mem.mode == 'DMR'
if digital and _mem.chtype not in (1, 3):
_mem.chtype = 1
elif not digital and _mem.chtype not in (0, 2):
_mem.chtype = 0
_mem.wide = mem.mode == 'FM'
_mem.power = POWER_LEVELS.index(mem.power) if mem.power else 2
if mem.tuning_step in STEPS:
_mem.step = STEPS.index(mem.tuning_step)
TONE_MODEL.set_tone(mem, _mem)
tx_contact = self._tx_contact(mem.number)
# Values shown clamped (e.g. squelch 12 as 9) stay as they are
# unless the user changed them.
shown = {s.get_name(): str(s.value)
for s in self._get_extra(_mem, mem.number)}
for setting in mem.extra:
name = setting.get_name()
if shown.get(name) == str(setting.value):
continue
if name == 'chtype':
# Keep the analog/digital choice consistent with the mode.
value = CHTYPES.index(str(setting.value))
if (value in (1, 3)) == digital:
_mem.chtype = value
elif name == 'timeslot':
_mem.timeslot = int(str(setting.value)) - 1
elif name == 'tx_admit':
value = str(setting.value)
options = TX_ADMIT[_mem.chtype in (1, 3)]
if value not in options: # mode changed: other list
options = TX_ADMIT[_mem.chtype not in (1, 3)]
_mem.tx_admit = options.index(value)
elif name in LIST_EXTRAS:
setattr(_mem, name,
LIST_EXTRAS[name][1].index(str(setting.value)))
elif name in ('tx_contact', 'radio_id', 'rxgroup', 'privacy',
'emerg_system', 'scanlist'):
choice = str(setting.value)
value = int(choice.split(':')[0]) if ':' in choice else 0
if name == 'tx_contact':
tx_contact = value
else:
setattr(_mem, name, value)
else:
setattr(_mem, name, int(setting.value))
self._update_tx_contact(mem.number, tx_contact, _mem.chtype in (1, 3))
def _update_tx_contact(self, number, contact, digital):
"""Write the TX contact table only if something changes, so a page
the radio doesn't have isn't created for nothing."""
tag, off = self._txc_loc(number)
hi, lo = self._page(tag)[1][off:off + 2]
if (hi, lo) == (0xFF, 0xFF) and not contact:
return
if self._tx_contact(number) != contact or (hi & 1) != int(digital):
self._set_tx_contact(number, contact, digital)
chirp-20261030/chirp/drivers/baofeng_uv17.py 0000644 0001750 0001750 00000036124 15125715007 017463 0 ustar hibby hibby # Copyright 2023:
# * Sander van der Wel,
#
# 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 .
import logging
from chirp import chirp_common, directory
from chirp.settings import RadioSettingGroup, RadioSetting, \
RadioSettingValueBoolean, RadioSettingValueList, \
RadioSettings, RadioSettingValueString
import struct
from chirp.drivers import baofeng_common, baofeng_uv17Pro
from chirp import errors
LOG = logging.getLogger(__name__)
LIST_DTMFST = ["Off", "DT-ST", "ANI-ST", "DT+ANI"]
LIST_SAVE = ["Off", "1:1", "1:2", "1:3", "1:4"]
LIST_SCANMODE = ["Time", "Carrier", "Search"]
# Radios seem to have different memory sizes
def _get_memory_size(radio):
response = baofeng_uv17Pro._sendmagic(radio, radio._magic_memsize[0][0],
radio._magic_memsize[0][1])
mem_size = struct.unpack("> 4 & 0xf) * 10 + (blocknr & 0xf)
memory_map += [blocknr]
baofeng_uv17Pro._sendmagic(radio, b"\x06", 1)
return memory_map
def _download(radio):
"""Get the memory map"""
if not radio._DETECTED_BY:
# The GA510v2 (at least) is detected, and thus has already done ident
baofeng_uv17Pro._do_ident(radio)
data = b""
memory_map = _get_memory_map(radio)
status = chirp_common.Status()
status.cur = 0
status.max = radio.MEM_TOTAL // radio.BLOCK_SIZE
status.msg = "Cloning from radio..."
radio.status_fn(status)
for block_number in radio.BLOCK_O_READ:
if block_number not in memory_map:
# Memory block not found.
LOG.error('Block %i (0x%x) not in memory map: %s',
block_number, block_number, memory_map)
raise errors.RadioError('Radio memory is corrupted. ' +
'Fix this by uploading a backup image ' +
'to the radio.')
block_index = memory_map.index(block_number) + 1
start_addr = block_index * 0x1000
for addr in range(start_addr, start_addr + 0x1000,
radio.BLOCK_SIZE):
frame = radio._make_read_frame(addr, radio.BLOCK_SIZE)
radio.pipe.log('Reading addr %04x' % addr)
baofeng_common._rawsend(radio, frame)
d = baofeng_common._rawrecv(radio, radio.BLOCK_SIZE + 5)
data += d[5:]
status.cur = len(data) // radio.BLOCK_SIZE
status.msg = "Cloning from radio..."
radio.status_fn(status)
baofeng_uv17Pro._sendmagic(radio, b"\x06", 1)
return data
def _upload(radio):
"""Upload procedure"""
baofeng_uv17Pro._do_ident(radio)
memory_map = _get_memory_map(radio)
status = chirp_common.Status()
status.cur = 0
status.max = radio.WRITE_MEM_TOTAL // radio.BLOCK_SIZE
status.msg = "Cloning to radio..."
radio.status_fn(status)
for block_number in radio.BLOCK_ORDER:
# Choose a block number if memory map is corrupt
# This can happen when the upload process was interrupted
if block_number not in memory_map:
memory_map[memory_map.index(165)] = block_number
block_index = memory_map.index(block_number) + 1
start_addr = block_index * 0x1000
data_start_addr = radio.BLOCK_ORDER.index(block_number) * 0x1000
for addr in range(start_addr, start_addr + 0x1000,
radio.BLOCK_SIZE):
# sending the data
data_addr = data_start_addr + addr - start_addr
data = radio.get_mmap()[data_addr:data_addr + radio.BLOCK_SIZE]
frame = radio._make_frame(b"W", addr, radio.BLOCK_SIZE, data)
radio.pipe.log('Sending addr %04x' % addr)
baofeng_common._rawsend(radio, frame)
# receiving the response
ack = baofeng_common._rawrecv(radio, 1)
if ack != b"\x06":
msg = "Bad ack writing block 0x%04x" % addr
raise errors.RadioError(msg)
# UI Update
status.cur = (data_addr) // radio.BLOCK_SIZE
status.msg = "Cloning to radio..."
radio.status_fn(status)
@directory.register
class UV17(baofeng_uv17Pro.UV17Pro):
"""Baofeng UV-17"""
VENDOR = "Baofeng"
MODEL = "UV-17"
download_function = _download
upload_function = _upload
MODES = ["FM", "NFM"]
BLOCK_ORDER = [16, 17, 18, 19, 24, 25, 26, 4, 6]
# Add extra read blocks (e.g. calibration block 2) to the end here:
BLOCK_O_READ = list(BLOCK_ORDER)
MEM_TOTAL = 0x9000
WRITE_MEM_TOTAL = 0x9000
BLOCK_SIZE = 0x40
BAUD_RATE = 57600
_idents = [b"PSEARCH"]
_magics = [(b"PASSSTA", 3),
(b"SYSINFO", 1),
(b"\x56\x00\x00\x0A\x0D", 13),
(b"\x06", 1),
(b"\x56\x00\x10\x0A\x0D", 13),
(b"\x06", 1),
(b"\x56\x00\x20\x0A\x0D", 13),
(b"\x06", 1)]
_magic_memsize = [(b"\x56\x00\x00\x00\x0A", 11)]
_radio_memsize = 0xffff
_magics2 = [(b"\x06", 1),
(b"\xFF\xFF\xFF\xFF\x0C\x55\x56\x31\x35\x39\x39\x39", 1),
(b"\02", 8),
(b"\x06", 1)]
_fingerprint = b"\x06" + b"UV15999"
_scode_offset = 1
_mem_positions = ()
_tri_band = False
POWER_LEVELS = [chirp_common.PowerLevel("Low", watts=1.00),
chirp_common.PowerLevel("High", watts=5.00)]
LENGTH_NAME = 11
VALID_BANDS = [baofeng_uv17Pro.UV17Pro._vhf_range,
baofeng_uv17Pro.UV17Pro._uhf_range]
SCODE_LIST = ["%s" % x for x in range(1, 16)]
SQUELCH_LIST = ["Off"] + list("123456789")
LIST_POWERON_DISPLAY_TYPE = ["Full", "Message", "Voltage"]
LIST_TIMEOUT = ["Off"] + ["%s sec" % x for x in range(15, 615, 15)]
LIST_VOICE = ["Chinese", "English"]
LIST_BACKLIGHT_TIMER = ["Always On"] + ["%s sec" % x for x in range(1, 11)]
LIST_MODE = ["Name", "Frequency"]
CHANNELS = 999
CHANNEL_DEF = """
struct channel {
lbcd rxfreq[4];
lbcd txfreq[4];
u8 unused1;
ul16 rxtone;
ul16 txtone;
u8 unknown1:1,
bcl:1,
pttid:2,
unknown2:1,
wide:1,
lowpower:1,
unknown:1;
u8 scode:4,
unknown3:3,
scan:1;
u8 unknown4;
};
"""
MEM_DEFS = """
struct channelname {
char name[11];
};
struct settings {
u8 powerondistype;
u8 unknown0[15];
char boottext1[10];
u8 unknown1[6];
char boottext2[10];
u8 unknown2[22];
u8 tot;
u8 squelch;
u8 vox;
u8 powersave: 4,
unknown3:2,
voice: 1,
voicesw: 1;
u8 backlight;
u8 beep:1,
autolock:1,
unknown4:1,
tail:1,
scanmode:2,
dtmfst:2;
u8 unknown5:1,
dualstandby:1,
roger:1,
unknown6:3,
fmenable:1,
unknown7:1;
u8 unknown8[9];
u8 unknown9:6,
chbdistype:1,
chadistype:1;
};
struct ani {
u8 unknown[5];
u8 code[5];
};
struct pttid {
u8 code[5];
};
"""
MEM_LAYOUT = """
#seekto 0x0030;
struct {
struct channel mem[252];
} mem1;
#seek 0x10;
struct {
struct channel mem[255];
} mem2;
#seek 0x10;
struct {
struct channel mem[255];
} mem3;
#seek 0x10;
struct {
struct channel mem[237];
} mem4;
#seekto 0x7000;
struct settings settings;
struct vfo {
lbcd rxfreq[4];
lbcd txfreq[4];
u8 unused1;
ul16 rxtone;
ul16 txtone;
u8 unknown1:1,
bcl:1,
pttid:2,
unknown2:1,
wide:1,
lowpower:1,
unknown:1;
u8 scode:4,
unknown3:3,
scan:1;
u8 unknown4;
};
#seekto 0x0010;
struct {
struct vfo a;
struct vfo b;
} vfo;
#seekto 0x4000;
struct channelname names1[372];
#seek 0x4;
struct channelname names2[372];
#seek 0x4;
struct channelname names3[255];
#seekto 0x8000;
struct pttid pttid[15];
struct ani ani;
"""
MEM_FORMAT = CHANNEL_DEF + MEM_DEFS + MEM_LAYOUT
def _make_frame(self, cmd, addr, length, data=""):
"""Pack the info in the header format"""
frame = struct.pack("B", length)
# add the data if set
if len(data) != 0:
frame += data
# return the data
return frame
def get_settings(self):
"""Translate the bit in the mem_struct into settings in the UI"""
_mem = self._memobj
basic = RadioSettingGroup("basic", "Basic Settings")
dtmfe = RadioSettingGroup("dtmfe", "DTMF Encode Settings")
top = RadioSettings(basic, dtmfe)
self.get_settings_common_basic(basic, _mem)
def _filter(name):
filtered = ""
for char in str(name):
if char in chirp_common.CHARSET_ASCII:
filtered += char
else:
filtered += " "
return filtered
rs = RadioSetting("settings.boottext1", "Power-On Message 1",
RadioSettingValueString(
0, 10, _filter(self._memobj.settings.boottext1)))
basic.append(rs)
rs = RadioSetting("settings.boottext2", "Power-On Message 2",
RadioSettingValueString(
0, 10, _filter(self._memobj.settings.boottext2)))
basic.append(rs)
if _mem.settings.powersave > 0x04:
val = 0x00
else:
val = _mem.settings.powersave
rs = RadioSetting("settings.powersave", "Battery Saver",
RadioSettingValueList(
LIST_SAVE, current_index=val))
basic.append(rs)
rs = RadioSetting("settings.scanmode", "Scan Mode",
RadioSettingValueList(
LIST_SCANMODE,
current_index=_mem.settings.scanmode))
basic.append(rs)
rs = RadioSetting(
"settings.dtmfst", "DTMF Sidetone",
RadioSettingValueList(
LIST_DTMFST, current_index=_mem.settings.dtmfst))
basic.append(rs)
rs = RadioSetting("settings.fmenable", "Enable FM radio",
RadioSettingValueBoolean(_mem.settings.fmenable))
basic.append(rs)
self.get_settings_common_dtmf(dtmfe, _mem)
return top
def decode_tone(self, val):
pol = "N"
mode = ""
if val in [0, 0xFFFF]:
xval = 0
elif (val & 0x8000) > 0:
mode = "DTCS"
xval = (val & 0x0f) + (val >> 4 & 0xf)\
* 10 + (val >> 8 & 0xf) * 100
if (val & 0xC000) == 0xC000:
pol = "R"
else:
mode = "Tone"
xval = int((val & 0x0f) + (val >> 4 & 0xf) * 10 +
(val >> 8 & 0xf) * 100 + (val >> 12 & 0xf)
* 1000) / 10.0
return mode, xval, pol
def _get_raw_memory(self, number):
# The flash memory contains page_numbers
# This is probably to do wear leveling on the memory
# These numbers are needed, but make the channel memory
# not continuous.
number = number - 1
if number >= 762:
_mem = self._memobj.mem4.mem[number - 762]
return _mem
if number >= 507:
_mem = self._memobj.mem3.mem[number - 507]
return _mem
if number >= 252:
_mem = self._memobj.mem2.mem[number - 252]
return _mem
_mem = self._memobj.mem1.mem[number]
return _mem
def get_raw_memory(self, number):
return repr(self._get_raw_memory(number))
def get_channel_name(self, number):
number = number - 1
if number >= 744:
_name = self._memobj.names3[number - 744]
return _name
if number >= 372:
_name = self._memobj.names2[number - 372]
return _name
_name = self._memobj.names1[number]
return _name
def get_memory(self, number):
_mem = self._get_raw_memory(number)
_nam = self.get_channel_name(number)
mem = chirp_common.Memory()
mem.number = number
self.get_memory_common(_mem, _nam.name, mem)
return mem
def encode_tone(self, memtone, mode, tone, pol):
if mode == "Tone":
xtone = '%04i' % (tone * 10)
memtone.set_value((int(xtone[0]) << 12) + (int(xtone[1]) << 8) +
(int(xtone[2]) << 4) + int(xtone[3]))
elif mode == "TSQL":
xtone = '%04i' % (tone * 10)
memtone.set_value((int(tone[0]) << 12) + (int(xtone[1]) << 8) +
(int(xtone[2]) << 4) + int(xtone[3]))
elif mode == "DTCS":
xtone = str(int(tone)).rjust(4, '0')
memtone.set_value((0x8000 + (int(xtone[0]) << 12) +
(int(xtone[1]) << 8) + (int(xtone[2]) << 4) +
int(xtone[3])))
else:
memtone.set_value(0)
if mode == "DTCS" and pol == "R":
memtone.set_value(memtone + 0x4000)
def set_memory(self, mem):
_mem = self._get_raw_memory(mem.number)
_nam = self.get_channel_name(mem.number)
if mem.empty:
_mem.set_raw(b"\xff" * 16)
return
_mem.set_raw(b"\x00" * 16)
_namelength = self.get_features().valid_name_length
_nam.name = mem.name[:_namelength].ljust(11, '\x00')
self.set_memory_common(mem, _mem)
@directory.register
class UV13Pro(UV17):
VENDOR = "Baofeng"
MODEL = "UV-13Pro"
_radio_memsize = 0x31fff
chirp-20261030/chirp/drivers/baofeng_uv17Pro.py 0000644 0001750 0001750 00000303164 15270732464 020154 0 ustar hibby hibby # Copyright 2023:
# * Sander van der Wel,
#
# 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 .
import logging
from chirp.drivers import baofeng_common as bfc
from chirp import chirp_common, directory, memmap, bandplan_na
from chirp import bitwise
from chirp import platform
from chirp.settings import RadioSetting, \
RadioSettingValueBoolean, RadioSettingValueList, \
RadioSettingValueString, \
RadioSettings, RadioSettingGroup
import struct
from chirp import errors
LOG = logging.getLogger(__name__)
# Baofeng UV-17L magic string
MSTRING_UV17L = b"PROGRAMBFNORMALU"
MSTRING_UV17PROGPS = b"PROGRAMCOLORPROU"
# Baofeng GM-5RH magic string
MSTRING_GM5RH = b"PROGRAMBFGMRS05U"
# BTECH BF-F8HP Pro magic string
MSTRING_BFF8HPPRO = b"PROGRAMBF5RTECHU"
# Baofeng BF-K6 magic string
MSTRING_BFK6 = b"PROGRAMBF-K6PROU"
DTMF_CHARS = list("0123456789ABCD*#")
NUMERIC_CHARS = list("0123456789")
LIST_AB = ["A", "B"]
LIST_BANDWIDTH = ["Wide", "Narrow"]
LIST_HANGUPTIME = ["%s s" % x for x in [3, 4, 5, 6, 7, 8, 9, 10]]
LIST_SIDE_TONE = ["Off", "KB Side Tone", "ANI Side Tone", "KB + ANI Side Tone"]
LIST_PTTID = ["Off", "BOT", "EOT", "Both"]
LIST_TIMEOUT_ALARM = ["Off"] + ["%s sec" % x for x in range(1, 11)]
LIST_PILOT_TONE = ["1000 Hz", "1450 Hz", "1750 Hz", "2100 Hz"]
LIST_VOICE = ["Off", "English", "Chinese"]
LIST_WORKMODE = ["Frequency", "Channel"]
LIST_SCANMODE = ["Time", "Carrier", "Search"]
LIST_ALARMMODE = ["Local", "Send Tone", "Send Code"]
LIST_QT_SAVEMODE = ["Both", "RX", "TX"]
LIST_RPT_TAIL_CLEAR = ["%s ms" % x for x in range(0, 1100, 100)]
LIST_VOX_DELAY_TIME = ["%s ms" % x for x in range(500, 2100, 100)]
LIST_VOX_LEVEL = ["Off"] + ["%s" % x for x in range(1, 10, 1)]
LIST_VOX_LEVEL_ALT = ["%s" % x for x in range(1, 10, 1)]
LIST_GPS_MODE = ["GPS", "Beidou", "GPS + Beidou"]
LIST_GPS_TIMEZONE = ["%s" % x for x in range(-12, 13, 1)]
LIST_SHIFTS = ["Off", "+", "-"]
LIST_OFF_AB = ["Off"] + LIST_AB
LIST_POWERON_TONE = ["Off", "Tone", "Voice"]
LIST_LCD_CONTRAST = ["%s" % x for x in range(1, 6, 1)]
LIST_MODES = ["Factory mode", "FCC amateur", "IC amateur", "CE amateur",
"Indonesian amateur", "Chinese amateur"]
CHARSET_GB2312 = chirp_common.CHARSET_ASCII
for x in range(0xB0, 0xD7):
for y in range(0xA1, 0xFF):
CHARSET_GB2312 += bytes([x, y]).decode('gb2312')
def model_match(cls, data):
"""Match the opened image to the correct version"""
return data[cls.MEM_TOTAL:] == bytes(cls.MODEL, 'utf-8')
def _crypt(symbol_index, buffer):
# Some weird encryption is used. From the table below, we only use "CO 7".
tblEncrySymbol = [b"BHT ", b"CO 7", b"A ES", b" EIY", b"M PQ",
b"XN Y", b"RVB ", b" HQP", b"W RC", b"MS N",
b" SAT", b"K DH", b"ZO R", b"C SL", b"6RB ",
b" JCG", b"PN V", b"J PK", b"EK L", b"I LZ"]
tbl_encrypt_symbols = tblEncrySymbol[symbol_index]
dec_buffer = b""
index1 = 0
for index2 in range(len(buffer)):
bool_encrypt_char = ((tbl_encrypt_symbols[index1] != 32) and
(buffer[index2] != 0) and
(buffer[index2] != 255) and
(buffer[index2] != tbl_encrypt_symbols[index1])
and (buffer[index2] !=
(tbl_encrypt_symbols[index1] ^ 255)))
if (bool_encrypt_char):
dec_byte = buffer[index2] ^ tbl_encrypt_symbols[index1]
dec_buffer += struct.pack('>B', dec_byte)
else:
dec_buffer += struct.pack('>B', buffer[index2])
index1 = (index1 + 1) % 4
return dec_buffer
def _sendmagic(radio, magic, response_len):
bfc._rawsend(radio, magic)
return bfc._rawrecv(radio, response_len)
def _do_ident(radio):
saw_data = False
for ident in radio._idents:
# Flush input buffer
bfc._clean_buffer(radio)
# Ident radio
LOG.debug('Sending ident magic: %r', ident)
try:
ack = _sendmagic(radio, ident, len(radio._fingerprint))
if ack:
saw_data = True
if not ack.startswith(radio._fingerprint):
LOG.debug('Ack %r did not match fingerprint: %r', ack,
radio._fingerprint)
continue
except errors.RadioError:
# This is the timeout or invalid response length case
LOG.debug('Radio did not respond to ident magic: %r', ident)
continue
for magic, resplen in radio._magics:
_sendmagic(radio, magic, resplen)
return True
if saw_data:
raise errors.RadioError("Unexpected response from radio")
raise errors.RadioNoResponse()
def _download(radio):
"""Get the memory map"""
# Put radio in program mode and identify it
_do_ident(radio)
data = b""
# UI progress
status = chirp_common.Status()
status.cur = 0
status.max = radio.MEM_TOTAL // radio.BLOCK_SIZE
status.msg = "Cloning from radio..."
radio.status_fn(status)
for i in range(len(radio.MEM_SIZES)):
MEM_SIZE = radio.MEM_SIZES[i]
MEM_START = radio.MEM_STARTS[i]
for addr in range(MEM_START, MEM_START + MEM_SIZE,
radio.BLOCK_SIZE):
frame = radio._make_read_frame(addr, radio.BLOCK_SIZE)
radio.pipe.log('Sending request for 0x%04x' % addr)
bfc._rawsend(radio, frame)
d = bfc._rawrecv(radio, radio.BLOCK_SIZE + 4)
if radio._uses_encr:
d = _crypt(radio._encrsym, d[4:])
else:
d = d[4:]
# Aggregate the data
data += d
# UI Update
status.cur = len(data) // radio.BLOCK_SIZE
radio.status_fn(status)
return data
def _upload(radio):
# Put radio in program mode and identify it
_do_ident(radio)
data = b""
# UI progress
status = chirp_common.Status()
status.cur = 0
status.max = radio.MEM_TOTAL // radio.BLOCK_SIZE
status.msg = "Cloning to radio..."
radio.status_fn(status)
data_addr = 0x00
radio_mem = radio.get_mmap()
for i in range(len(radio.MEM_SIZES)):
MEM_SIZE = radio.MEM_SIZES[i]
MEM_START = radio.MEM_STARTS[i]
for addr in range(MEM_START, MEM_START + MEM_SIZE,
radio.BLOCK_SIZE):
data = radio_mem[data_addr:data_addr + radio.BLOCK_SIZE]
if radio._uses_encr:
data = _crypt(radio._encrsym, data)
data_addr += radio.BLOCK_SIZE
frame = radio._make_frame(b"W", addr, radio.BLOCK_SIZE, data)
radio.pipe.log('Sending address %04x' % addr)
bfc._rawsend(radio, frame)
ack = bfc._rawrecv(radio, 1)
if ack != b"\x06":
msg = "Bad ack writing block 0x%04x" % addr
raise errors.RadioError(msg)
# UI Update
status.cur = data_addr // radio.BLOCK_SIZE
status.msg = "Cloning to radio..."
radio.status_fn(status)
return data
@directory.register
class UV17Pro(bfc.BaofengCommonHT):
"""Baofeng UV-17Pro"""
VENDOR = "Baofeng"
MODEL = "UV-17Pro"
MEM_STARTS = [0x0000, 0x9000, 0xA000, 0xD000]
MEM_SIZES = [0x8040, 0x0040, 0x02C0, 0x0040]
MEM_TOTAL = 0x8380
BLOCK_SIZE = 0x40
BAUD_RATE = 115200
download_function = _download
upload_function = _upload
_gmrs = False
_bw_shift = False
_has_support_for_banknames = False
_has_workmode_support = True
_has_savemode = True
_tri_band = True
_fileid = []
_idents = [MSTRING_UV17L]
_fingerprint = b"\x06"
_magics = [(b"\x46", 16),
(b"\x4d", 15),
(b"\x53\x45\x4E\x44\x21\x05\x0D\x01\x01\x01\x04\x11\x08\x05" +
b"\x0D\x0D\x01\x11\x0F\x09\x12\x09\x10\x04\x00", 1)]
_fw_ver_start = 0x1EF0
_recv_block_size = 0x40
_mem_size = MEM_TOTAL
_ack_block = True
_has_when_to_send_aniid = True
_vfoscan = False
_has_gps = False
_has_voxsw = False
_has_pilot_tone = False
_has_send_id_delay = False
_has_skey1_short = False
_has_skey1_long = False
_has_skey2_short = False
_has_skey2_long = False
_has_skey_disable = False
_has_zone_linking = False
_has_bt = False
_has_scramble = False
_scode_offset = 0
_encrsym = 1
_has_voice = True
_uses_encr = True
_mem_params = {
'mems': 1000,
'ani': 0x8080,
'pttid': 0x80A0,
'names': 0x8280,
}
MODES = ["NFM", "FM"]
VALID_CHARS = chirp_common.CHARSET_ALPHANUMERIC + \
"!@#$%^&*()+-=[]:\";'<>?,./"
LENGTH_NAME = 12
SKIP_VALUES = ["", "S"]
DTCS_CODES = tuple(sorted(chirp_common.DTCS_CODES + (645,)))
RXTX_CODES = ('Off', )
LIST_PW_SAVEMODE = ["Off", "On"]
for code in chirp_common.TONES:
RXTX_CODES = (RXTX_CODES + (str(code), ))
for code in DTCS_CODES:
RXTX_CODES = (RXTX_CODES + ('D' + str(code) + 'N', ))
for code in DTCS_CODES:
RXTX_CODES = (RXTX_CODES + ('D' + str(code) + 'I', ))
POWER_LEVELS = [chirp_common.PowerLevel("High", watts=5.00),
chirp_common.PowerLevel("Low", watts=1.00)]
_airband = (108000000, 135999999)
_vhf_range = (136000000, 174000000)
_vhf2_range = (200000000, 260000000)
_uhf_range = (400000000, 520000000)
_uhf2_range = (350000000, 390000000)
AIRBANDS = [_airband]
STEPS = [2.5, 5.0, 6.25, 10.0, 12.5, 20.0, 25.0, 50.0]
LIST_STEPS = ["2.5", "5.0", "6.25", "10.0", "12.5", "20.0", "25.0", "50.0"]
VALID_BANDS = [_vhf_range, _vhf2_range,
_uhf_range]
PTTID_LIST = LIST_PTTID
RXDTMF_LIST = ["CTCSS/DTS", "CTCSS/DCS + DTMF"]
SCODE_LIST = ["%s" % x for x in range(1, 21)]
SQUELCH_LIST = ["Off"] + list("12345")
SCRAMBLE_LIST = ["Off"] + ["SCR%s" % x for x in list("123")]
LIST_POWERON_DISPLAY_TYPE = ["LOGO", "BATT voltage"]
LIST_TIMEOUT = ["Off"] + ["%s sec" % x for x in range(15, 195, 15)]
LIST_VOICE = ["English", "Chinese"]
LIST_BACKLIGHT_TIMER = ["Always On"] + ["%s sec"
% x for x in range(5, 25, 5)]
LIST_MODE = ["Name", "Frequency", "Channel Number"]
LIST_BEEP = ["Off", "Beep", "Voice", "Both"]
LIST_MENU_QUIT_TIME = ["%s sec" % x for x in range(5, 55, 5)] + ["60 sec"]
LIST_ID_DELAY = ["%s ms" % x for x in range(100, 3100, 100)]
LIST_SEPARATE_CODE = ["A", "B", "C", "D", "*", "#"]
LIST_GROUP_CALL_CODE = ["Off"] + LIST_SEPARATE_CODE
LIST_SKEY2_SHORT = ["FM", "Scan", "Search", "Vox"]
LIST_DTMFSPEED = ["%s ms" % x for x in [50, 100, 200, 300, 500]]
LIST_DUAL_WATCH = ["Off", "On"]
CHANNELS = 1000
_mem_format = """
struct memory_obj {
lbcd rxfreq[4];
lbcd txfreq[4];
ul16 rxtone;
ul16 txtone;
u8 scode;
u8 pttid;
u8 unknown7:2,
scramble:2,
unknown8:2,
lowpower:2;
u8 unknown1:1,
wide:1,
sqmode:2,
bcl:1,
scan:1,
unknown2:1,
fhss:1;
u8 unknown3;
u8 unknown4;
u8 unknown5;
u8 unknown6;
char name[12];
};
"""
_common_defns = """
struct vfo_entry {
u8 freq[8];
ul16 rxtone;
ul16 txtone;
u8 unknown0;
u8 bcl;
u8 sftd:3,
scode:5;
u8 unknown1;
u8 lowpower;
u8 unknown2:1,
wide:1,
unknown3:5,
fhss:1;
u8 unknown4;
u8 step;
u8 offset[6];
u8 unknown5[2];
u8 sqmode;
u8 unknown6[3];
};
struct ani_obj {
u8 code[5];
u8 unknown[1];
u8 unused1:6,
aniid:2;
u8 dtmfon;
u8 dtmfoff;
u8 separatecode;
u8 groupcallcode;
};
"""
_settings_format = """
struct settings_obj {
u8 squelch;
u8 savemode;
u8 vox;
u8 backlight;
u8 dualstandby;
u8 tot;
u8 beep;
u8 voicesw;
u8 voice;
u8 sidetone;
u8 scanmode;
u8 pttid;
u8 pttdly;
u8 chadistype;
u8 chbdistype;
u8 bcl;
u8 autolock;
u8 alarmmode;
u8 alarmtone;
u8 unknown1;
u8 tailclear;
u8 rpttailclear;
u8 rpttaildet;
u8 roger;
u8 a_or_b_selected; // a=0, b=1
u8 fmenable;
u8 chbworkmode:4,
chaworkmode:4;
u8 keylock;
u8 powerondistype;
u8 tone;
u8 unknown4[2];
u8 voxdlytime;
u8 menuquittime;
u8 bluetooth;
u8 unknown5;
u8 dispani;
u8 unknown11[3];
u8 totalarm;
u8 unknown6[2];
u8 ctsdcsscantype;
ul16 vfoscanmin;
ul16 vfoscanmax;
u8 gpsw;
u8 gpsmode;
u8 key1short;
u8 unknown7;
u8 key2short;
u8 unknown8[2];
u8 rstmenu;
u8 singlewatch;
u8 hangup;
u8 voxsw;
u8 gpstimezone;
u8 unknown10;
u8 inputdtmf;
u8 gpsunits;
u8 pontime;
};
"""
MEM_FORMAT = """
struct memory_obj memory[%(mems)i];
#seekto 0x8000;
struct {
struct vfo_entry a;
struct vfo_entry b;
} vfo;
struct settings_obj settings;
#seekto 0x%(ani)X;
struct ani_obj ani;
#seekto 0x%(pttid)04X;
struct {
u8 code[5];
char name[10];
u8 unused;
} pttid[20];
struct {
u8 unknown32[32];
u8 code[16];
} upcode;
struct {
u8 code[16];
} downcode;
"""
_end_fmt = """
#seekto 0x%(names)04X;
struct {
char name[16];
} bank_name[10];
"""
def _make_read_frame(self, addr, length):
"""Pack the info in the header format"""
frame = self._make_frame(b"\x52", addr, length)
# Return the data
return frame
def _make_frame(self, cmd, addr, length, data=""):
"""Pack the info in the header format"""
frame = cmd + struct.pack(">i", addr)[2:] + struct.pack("B", length)
# add the data if set
if len(data) != 0:
frame += data
# return the data
return frame
@classmethod
def get_prompts(cls):
rp = chirp_common.RadioPrompts()
rp.experimental = \
('This driver is a beta version.\n'
'\n'
'Please save an unedited copy of your first successful\n'
'download to a CHIRP Radio Images(*.img) file.'
)
rp.pre_download = _(
"Follow these instructions to download your info:\n"
"1 - Turn off your radio\n"
"2 - Connect your interface cable\n"
"3 - Turn on your radio\n"
"4 - Do the download of your radio data\n")
rp.pre_upload = _(
"Follow this instructions to upload your info:\n"
"1 - Turn off your radio\n"
"2 - Connect your interface cable\n"
"3 - Turn on your radio\n"
"4 - Do the upload of your radio data\n")
return rp
def process_mmap(self):
"""Process the mem map into the mem object"""
# make lines shorter for style check.
fmt = (self._mem_format + self._common_defns +
self._settings_format + self.MEM_FORMAT +
self._end_fmt) % self._mem_params
self._memobj = bitwise.parse(fmt, self._mmap)
# DTMF settings
def apply_code(self, setting, obj, length):
code = []
for j in range(0, length):
try:
code.append(DTMF_CHARS.index(str(setting.value)[j]))
except IndexError:
code.append(0xFF)
obj.code = code
def _filterCodeName(self, name):
fname = b""
for char in name:
if ord(str(char)) in [0, 255]:
break
fname += int(char).to_bytes(1, 'big')
try:
return fname.decode('gb2312').strip()
except UnicodeDecodeError:
LOG.warning('Unable to decode code name %r' % name)
return ''
def apply_codename(self, setting, obj):
codename = (str(setting.value).encode('gb2312')[:10].ljust(10,
b"\xFF"))
obj.name = codename
def get_settings_common_dtmf(self, dtmfe, _mem):
for i in range(0, len(self.SCODE_LIST)):
# Signal Code
_codeobj = self._memobj.pttid[i].code
_code = "".join([
DTMF_CHARS[x] for x in _codeobj if int(x) < 0x1F])
val = RadioSettingValueString(0, 5, _code, False)
val.set_charset(DTMF_CHARS)
pttid = RadioSetting("pttid/%i.code" % i,
"Signal Code %i" % (i + 1), val)
doc = _("Programs DTMF code %i. A channel or VFO can select "
"this code as its PTT-ID and send it when PTT is "
"pressed, released, or both.") % (i + 1)
if self.MODEL in ("BF-F8HP-PRO", "UV-5R Mini", "UV-5G Mini"):
doc += " " + _("Use at most 3 digits.")
pttid.set_doc(doc)
pttid.set_apply_callback(self.apply_code, self._memobj.pttid[i], 5)
dtmfe.append(pttid)
# Signal Code Name
_nameobj = self._memobj.pttid[i]
if 'name' not in _nameobj:
# UV17, et al do not have pttid.name
continue
rs = RadioSetting(
"pttid/%i.name" % i,
"Signal Code %i Name" % (i + 1),
RadioSettingValueString(
0, 10, self._filterCodeName(_nameobj.name),
False, CHARSET_GB2312))
rs.set_doc(_("Sets the call name for DTMF code %i, up to 10 "
"characters long.") % (i + 1))
rs.set_apply_callback(self.apply_codename, _nameobj)
dtmfe.append(rs)
_codeobj = self._memobj.ani.code
_code = "".join([DTMF_CHARS[x] for x in _codeobj if int(x) < 0x1F])
val = RadioSettingValueString(0, 5, _code, False)
val.set_charset(DTMF_CHARS)
rs = RadioSetting("ani.code", "ANI Code", val)
doc = _("Programs the radio's numeric Automatic Number "
"Identification code. ANI is a DTMF PTT-ID that can "
"identify this radio to a dispatcher or other compatible "
"equipment.")
if self.MODEL == "BF-F8HP-PRO":
doc += " " + _("Use at most 3 digits.")
rs.set_doc(doc)
rs.set_apply_callback(self.apply_code, self._memobj.ani, 5)
dtmfe.append(rs)
def get_settings_pro_dtmf(self, dtmfe, _mem):
if _mem.ani.dtmfon > 0xC3:
val = 0x03
else:
val = _mem.ani.dtmfon
rs = RadioSetting("ani.dtmfon", "DTMF Speed (on)",
RadioSettingValueList(self.LIST_DTMFSPEED,
current_index=val))
rs.set_doc(_("Sets how long each DTMF tone is transmitted. Increase "
"this if the receiving system does not reliably decode "
"the digits."))
dtmfe.append(rs)
if _mem.ani.dtmfoff > 0xC3:
val = 0x03
else:
val = _mem.ani.dtmfoff
rs = RadioSetting("ani.dtmfoff", "DTMF Speed (off)",
RadioSettingValueList(self.LIST_DTMFSPEED,
current_index=val))
rs.set_doc(_("Sets the silent gap between transmitted DTMF tones. "
"Increase this if the receiving system cannot separate "
"adjacent digits."))
dtmfe.append(rs)
if self._has_when_to_send_aniid:
rs = RadioSetting(
"ani.aniid", "When to send ANI ID",
RadioSettingValueList(
LIST_PTTID, current_index=_mem.ani.aniid))
rs.set_doc(_("Controls when the ANI code is sent: BOT when PTT is "
"pressed, EOT when PTT is released, Both at both "
"times, or Off."))
dtmfe.append(rs)
if _mem.settings.hangup >= len(LIST_HANGUPTIME):
val = 0
else:
val = _mem.settings.hangup
rs = RadioSetting("settings.hangup", "Hang-up time",
RadioSettingValueList(LIST_HANGUPTIME,
current_index=val))
rs.set_doc(_("Sets how long the DTMF decoder remains active after a "
"valid code before returning to its normal state."))
dtmfe.append(rs)
def get_settings_common_basic(self, basic, _mem):
if _mem.settings.squelch >= len(self.SQUELCH_LIST):
val = 0x00
else:
val = _mem.settings.squelch
rs = RadioSetting("settings.squelch", "Squelch",
RadioSettingValueList(
self.SQUELCH_LIST, current_index=val))
rs.set_doc(_("Sets how strong a received signal must be before the "
"speaker unmutes. 0 leaves the squelch open so you hear "
"noise all the time, higher values reject more weak "
"signals and noise. Raise this value if the speaker "
"keeps unmuting on interference, lower it if distant "
"stations are being cut off.") + " " +
_("Off is level 0."))
basic.append(rs)
if _mem.settings.tot >= len(self.LIST_TIMEOUT):
val = 0x03
else:
val = _mem.settings.tot
rs = RadioSetting("settings.tot", "Timeout Timer",
RadioSettingValueList(
self.LIST_TIMEOUT, current_index=val))
rs.set_doc(_("Limits how long a single transmission may last. When "
"the time runs out the radio stops transmitting until "
"PTT is released, which prevents overheating and stops "
"a stuck PTT from blocking the channel for everyone "
"else.") + " " +
_("Off places no limit on the length of a transmission."))
basic.append(rs)
if _mem.settings.dualstandby >= len(self.LIST_DUAL_WATCH):
val = 0x01
else:
val = _mem.settings.dualstandby
rs = RadioSetting("settings.dualstandby", "Dual Watch",
RadioSettingValueList(
self.LIST_DUAL_WATCH, current_index=val))
rs.set_doc(_("Periodically checks both the A and B displayed "
"frequencies. When either receives a signal, the radio "
"stays on it until that signal disappears."))
basic.append(rs)
if _mem.settings.powerondistype >= len(self.LIST_POWERON_DISPLAY_TYPE):
val = 0x00
else:
rs = RadioSetting("settings.powerondistype",
"Power On Display Type",
RadioSettingValueList(
self.LIST_POWERON_DISPLAY_TYPE,
current_index=_mem.settings.powerondistype))
rs.set_doc(_("Chooses the startup display, such as a logo or the "
"battery voltage."))
basic.append(rs)
if self._has_voice:
if _mem.settings.voice >= len(self.LIST_VOICE):
val = 0x01
else:
val = _mem.settings.voice
rs = RadioSetting("settings.voice", "Voice Prompt",
RadioSettingValueList(
self.LIST_VOICE, current_index=val))
rs.set_doc(_("Selects the language of the menus and the spoken "
"voice prompts."))
basic.append(rs)
rs = RadioSetting("settings.voicesw", "Enable Voice",
RadioSettingValueBoolean(_mem.settings.voicesw))
rs.set_doc(_("Announces the channel number and other operations out "
"loud as you use the radio. It can be intrusive in quiet "
"surroundings."))
basic.append(rs)
if _mem.settings.backlight >= len(self.LIST_BACKLIGHT_TIMER):
val = 0x00
else:
val = _mem.settings.backlight
rs = RadioSetting("settings.backlight", "Backlight Timer",
RadioSettingValueList(
self.LIST_BACKLIGHT_TIMER,
current_index=val))
rs.set_doc(_("Sets how many seconds the display backlight remains on "
"after a key press or radio activity.") + " " +
_("Always On keeps it lit all the time."))
basic.append(rs)
rs = RadioSetting("settings.autolock", "Key Auto Lock",
RadioSettingValueBoolean(_mem.settings.autolock))
rs.set_doc(_("Automatically locks the keypad to prevent accidental "
"changes.") + " " +
_("When it is off, the keypad can still be locked by "
"hand."))
basic.append(rs)
if _mem.settings.beep >= len(self.LIST_BEEP):
val = 0x01
else:
val = _mem.settings.beep
rs = RadioSetting("settings.beep", "Beep",
RadioSettingValueList(
self.LIST_BEEP,
current_index=_mem.settings.beep))
rs.set_doc(_("Sounds a short confirmation tone as you operate the "
"radio. Turn it off when the radio has to be used "
"discreetly."))
basic.append(rs)
rs = RadioSetting("settings.roger", "Roger",
RadioSettingValueBoolean(_mem.settings.roger))
rs.set_doc(_("Sends a short tone at the end of each transmission so "
"other stations can hear that you released PTT."))
basic.append(rs)
rs = RadioSetting("settings.chadistype", "Channel A display type",
RadioSettingValueList(
self.LIST_MODE,
current_index=_mem.settings.chadistype))
rs.set_doc(_("In channel mode, chooses whether display line A shows "
"the memory channel number, its programmed name, or its "
"frequency."))
basic.append(rs)
rs = RadioSetting("settings.chbdistype", "Channel B display type",
RadioSettingValueList(
self.LIST_MODE,
current_index=_mem.settings.chbdistype))
rs.set_doc(_("In channel mode, chooses whether display line B shows "
"the memory channel number, its programmed name, or its "
"frequency."))
basic.append(rs)
def get_settings_pro_basic(self, basic, _mem):
if _mem.settings.savemode > len(self.LIST_PW_SAVEMODE):
val = 0x01 # assume values out of range are some form of "On"
else:
val = _mem.settings.savemode
rs = RadioSetting("settings.savemode", "Save Mode",
RadioSettingValueList(self.LIST_PW_SAVEMODE,
current_index=val))
rs.set_doc(_("Cuts standby power consumption by switching the "
"receiver off and on in a repeating cycle once no signal "
"has been received for a few seconds. It considerably "
"extends battery life, at the cost of occasionally "
"clipping the first moment of an incoming "
"transmission."))
basic.append(rs)
rs = RadioSetting("settings.totalarm", "Timeout Timer Alarm",
RadioSettingValueList(
LIST_TIMEOUT_ALARM,
current_index=_mem.settings.totalarm))
rs.set_doc(_("Warns you the selected number of seconds before the "
"time-out timer ends a transmission. Off turns the "
"warning off."))
basic.append(rs)
if self._has_pilot_tone:
rs = RadioSetting("settings.tone", "Pilot Tone",
RadioSettingValueList(
LIST_PILOT_TONE,
current_index=_mem.settings.tone))
rs.set_doc(_("Selects the tone-burst frequency sent for repeater "
"access."))
basic.append(rs)
rs = RadioSetting("settings.sidetone", "Side Tone",
RadioSettingValueList(
LIST_SIDE_TONE,
current_index=_mem.settings.sidetone))
rs.set_doc(_("Chooses which transmitted DTMF tones are also heard "
"through the radio speaker. Off mutes all; KB Side Tone "
"sounds manually keyed tones; ANI Side Tone sounds "
"automatic ANI/PTT-ID tones; KB + ANI Side Tone sounds "
"both."))
basic.append(rs)
rs = RadioSetting("settings.tailclear", "Tail Clear",
RadioSettingValueBoolean(_mem.settings.tailclear))
rs.set_doc(_("Transmits a special end-of-transmission signal intended "
"to reduce the squelch burst heard by another compatible "
"handheld."))
basic.append(rs)
rs = RadioSetting("settings.scanmode", "Scan Mode",
RadioSettingValueList(
LIST_SCANMODE,
current_index=_mem.settings.scanmode))
rs.set_doc(_("Controls what scanning does after finding a signal. "
"Time resumes after a preset time, Carrier resumes after "
"the signal disappears, and Search stops scanning on the "
"detected frequency or channel."))
basic.append(rs)
rs = RadioSetting("settings.alarmmode", "Alarm Mode",
RadioSettingValueList(
LIST_ALARMMODE,
current_index=_mem.settings.alarmmode))
rs.set_doc(_("Chooses what the emergency alarm does. Local sounds "
"the alarm on this radio only, Send Tone transmits a "
"repeating alarm tone, and Send Code transmits the "
"digits 119 followed by the ANI code."))
basic.append(rs)
rs = RadioSetting("settings.alarmtone", "Sound Alarm",
RadioSettingValueBoolean(_mem.settings.alarmtone))
rs.set_doc(_("Enables or disables the alarm sound.") + " " +
_("Off keeps the alarm silent on this radio's own "
"speaker."))
basic.append(rs)
rs = RadioSetting("settings.keylock", "Key Lock",
RadioSettingValueBoolean(_mem.settings.keylock))
rs.set_doc(_("Sets the current keypad-lock state.") + " " +
_("Hold the * key on the radio to lock or unlock the "
"keypad."))
basic.append(rs)
rs = RadioSetting("settings.menuquittime", "Menu Quit Timer",
RadioSettingValueList(
self.LIST_MENU_QUIT_TIME,
current_index=_mem.settings.menuquittime))
rs.set_doc(_("Sets how long the menu stays open without a key press "
"before the radio leaves it."))
basic.append(rs)
if self._has_send_id_delay:
rs = RadioSetting("settings.pttdly", "Send ID Delay",
RadioSettingValueList(
self.LIST_ID_DELAY,
current_index=_mem.settings.pttdly))
rs.set_doc(_("Delays transmission of the automatic ANI/PTT-ID "
"code after PTT is pressed, allowing the receiving "
"path time to open before the code is sent."))
basic.append(rs)
rs = RadioSetting("settings.ctsdcsscantype", "QT Save Mode",
RadioSettingValueList(
LIST_QT_SAVEMODE,
current_index=_mem.settings.ctsdcsscantype))
rs.set_doc(_("Chooses where a CTCSS or DCS code found by scanning "
"in Frequency (VFO) mode is saved: Both saves it as "
"the receive and transmit code, RX only as the receive "
"code, and TX only as the transmit code."))
basic.append(rs)
def getKey1shortIndex(value):
key_to_index = {0x07: 0,
0x1C: 1,
0x1D: 2,
0x2D: 3,
0x0A: 4,
0x0C: 5,
0x34: 6,
0x05: 7,
0x03: 8,
0x35: 9}
return key_to_index.get(int(value), 0)
KEY_NAME_TO_CODE = {
'FM': 0x07,
'Scan': 0x1C,
'Search': 0x1D,
'Vox': 0x2D,
'TX Power': 0x0A,
'NOAA': 0x0C,
'Zone Select': 0x34,
'Monitor': 0x05,
'Alarm': 0x03,
'Scan Edit': 0x35,
}
KEY_CODE_TO_INDEX = {
code: idx for idx, code in enumerate(KEY_NAME_TO_CODE.values())
}
def make_apply_key(attr_name):
def apply(setting, obj):
val = str(setting.value)
setattr(obj, attr_name,
KEY_NAME_TO_CODE.get(val, 0x07))
return apply
apply_key1short = make_apply_key("key1short")
apply_key1long = make_apply_key("key1long")
apply_key2short = make_apply_key("key2short")
apply_key2long = make_apply_key("key2long")
def get_key_index(value):
return KEY_CODE_TO_INDEX.get(int(value), 0)
short_doc = _("Chooses what a short press of side key %i does.")
long_doc = _("Chooses what a long press of side key %i does.")
KEY_SETTINGS = [
("_has_skey1_short", "settings.key1short", "Skey1 Short",
"key1short", apply_key1short, short_doc % 1),
("_has_skey1_long", "settings.key1long", "Skey1 Long",
"key1long", apply_key1long, long_doc % 1),
("_has_skey2_short", "settings.key2short", "Skey2 Short",
"key2short", apply_key2short, short_doc % 2),
("_has_skey2_long", "settings.key2long", "Skey2 Long",
"key2long", apply_key2long, long_doc % 2),
]
for flag, path, label, attr, apply_cb, doc in KEY_SETTINGS:
if getattr(self, flag):
rs = RadioSetting(
path,
label,
RadioSettingValueList(
self.LIST_SKEY2_SHORT,
current_index=get_key_index(
getattr(_mem.settings, attr))
)
)
rs.set_doc(doc)
rs.set_apply_callback(apply_cb, _mem.settings)
basic.append(rs)
if self._has_skey_disable:
rs = RadioSetting("settings.skdisable", "Side Key Disable",
RadioSettingValueList(
self.LIST_SKEY_DISABLE,
current_index=_mem.settings.skdisable))
basic.append(rs)
rs = RadioSetting("settings.chaworkmode", "Channel A work mode",
RadioSettingValueList(
LIST_WORKMODE,
current_index=_mem.settings.chaworkmode))
rs.set_doc(_("Selects Frequency (VFO) mode for direct tuning or "
"Channel (MR) mode for programmed memories on display "
"A."))
basic.append(rs)
rs = RadioSetting("settings.chbworkmode", "Channel B work mode",
RadioSettingValueList(
LIST_WORKMODE,
current_index=_mem.settings.chbworkmode))
rs.set_doc(_("Selects Frequency (VFO) mode for direct tuning or "
"Channel (MR) mode for programmed memories on display "
"B."))
basic.append(rs)
rs = RadioSetting("settings.rpttailclear", "Rpt Tail Clear",
RadioSettingValueList(
LIST_RPT_TAIL_CLEAR,
current_index=_mem.settings.rpttailclear))
rs.set_doc(_("Selects the squelch-tail elimination setting used for "
"communication through a compatible repeater. Use 0 ms "
"when the repeater does not support this feature."))
basic.append(rs)
rs = RadioSetting("settings.rpttaildet", "Rpt Tail Delay",
RadioSettingValueList(
LIST_RPT_TAIL_CLEAR,
current_index=_mem.settings.rpttaildet))
rs.set_doc(_("Sets the delay before repeater squelch-tail "
"elimination is applied. Match this timing to the "
"repeater; use 0 ms when the feature is not required."))
basic.append(rs)
rs = RadioSetting("settings.rstmenu", "Enable Menu Rst",
RadioSettingValueBoolean(_mem.settings.rstmenu))
rs.set_doc(_("Controls whether the radio's front-panel RESET menu is "
"available. This does not reset the radio by itself."))
basic.append(rs)
if self._has_voxsw:
rs = RadioSetting("settings.voxsw", "Vox Switch",
RadioSettingValueBoolean(_mem.settings.voxsw))
rs.set_doc(_("Voice operated transmit. The radio starts "
"transmitting when you speak towards the microphone, "
"so hands-free conversation is possible without "
"pressing PTT."))
basic.append(rs)
rs = RadioSetting("settings.vox", "Vox Level",
RadioSettingValueList(
LIST_VOX_LEVEL_ALT,
current_index=_mem.settings.vox))
rs.set_doc(_("Sets the VOX sensitivity. Too sensitive a setting "
"keys the transmitter on the noise around the radio, "
"too insensitive a setting fails to pick up your "
"voice."))
basic.append(rs)
else:
rs = RadioSetting("settings.vox", "Vox Level",
RadioSettingValueList(
LIST_VOX_LEVEL,
current_index=_mem.settings.vox))
basic.append(rs)
rs = RadioSetting("settings.voxdlytime", "Vox Delay Time",
RadioSettingValueList(
LIST_VOX_DELAY_TIME,
current_index=_mem.settings.voxdlytime))
rs.set_doc(_("How long the transmitter stays keyed after you stop "
"speaking. A longer delay avoids dropping the "
"transmission during natural pauses in speech."))
basic.append(rs)
if self._has_gps:
rs = RadioSetting("settings.gpsw", "GPS On",
RadioSettingValueBoolean(_mem.settings.gpsw))
basic.append(rs)
rs = RadioSetting("settings.gpsmode", "GPS Mode",
RadioSettingValueList(
LIST_GPS_MODE,
current_index=_mem.settings.gpsmode))
basic.append(rs)
rs = RadioSetting("settings.gpstimezone", "GPS Timezone",
RadioSettingValueList(
LIST_GPS_TIMEZONE,
current_index=_mem.settings.gpstimezone))
basic.append(rs)
if self._has_bt:
rs = RadioSetting("settings.bluetooth", "Bluetooth",
RadioSettingValueBoolean(
_mem.settings.bluetooth))
rs.set_doc(_("Turns on Bluetooth so that the radio can be "
"programmed wirelessly from a phone app."))
basic.append(rs)
rs = RadioSetting("settings.fmenable", "Disable FM radio",
RadioSettingValueBoolean(_mem.settings.fmenable))
rs.set_doc(_("Blocks use of the broadcast FM radio receiver. When "
"it is off, the FM radio can be used as normal."))
basic.append(rs)
if self._has_zone_linking:
for i in range(1, 11):
rs = RadioSetting(f"settings.zn{i}linkd",
f"Scan Link Zone {i}",
RadioSettingValueBoolean(getattr(
_mem.settings, f"zn{i}linkd")))
basic.append(rs)
rs = RadioSetting("settings.lnkzones", "Scan Zone Linking",
RadioSettingValueBoolean(_mem.settings.lnkzones))
basic.append(rs)
def get_settings_common_workmode(self, workmode, _mem):
vfoA = RadioSettingGroup("vfoA", "VFO A")
vfoB = RadioSettingGroup("vfoB", "VFO B")
def my_validate(value):
value = chirp_common.parse_freq(value)
for band in self.VALID_BANDS:
if value >= band[0] and value < band[1]:
return chirp_common.format_freq(value)
msg = ("{0} is not in a valid band.".format(value))
LOG.debug(msg)
return chirp_common.format_freq(band[0]) # Default to valid value
def apply_freq(setting, obj):
value = chirp_common.parse_freq(str(setting.value)) / 10
for i in range(7, -1, -1):
obj.freq[i] = value % 10
value /= 10
freqA = RadioSettingValueString(0, 10,
bfc.bcd_decode_freq(_mem.vfo.a.freq))
freqA.set_validate_callback(my_validate)
rs = RadioSetting("vfo.a.freq", "Frequency", freqA)
rs.set_apply_callback(apply_freq, _mem.vfo.a)
rs.set_doc(_("Sets the receive frequency shown on display line A in "
"Frequency (VFO) mode."))
vfoA.append(rs)
freqB = RadioSettingValueString(0, 10,
bfc.bcd_decode_freq(_mem.vfo.b.freq))
freqB.set_validate_callback(my_validate)
rs = RadioSetting("vfo.b.freq", "Frequency", freqB)
rs.set_apply_callback(apply_freq, _mem.vfo.b)
rs.set_doc(_("Sets the receive frequency shown on display line B in "
"Frequency (VFO) mode."))
vfoB.append(rs)
if _mem.vfo.a.sftd >= len(LIST_SHIFTS):
val = 0
else:
val = _mem.vfo.a.sftd
rs = RadioSetting("vfo.a.sftd", "Shift",
RadioSettingValueList(
LIST_SHIFTS, current_index=val))
rs.set_doc(_("Sets VFO A's repeater shift direction. + transmits "
"above the receive frequency, - transmits below it, "
"and Off uses the same frequency for transmit and "
"receive."))
vfoA.append(rs)
if _mem.vfo.b.sftd >= len(LIST_SHIFTS):
val = 0
else:
val = _mem.vfo.b.sftd
rs = RadioSetting("vfo.b.sftd", "Shift",
RadioSettingValueList(
LIST_SHIFTS, current_index=val))
rs.set_doc(_("Sets VFO B's repeater shift direction. + transmits "
"above the receive frequency, - transmits below it, "
"and Off uses the same frequency for transmit and "
"receive."))
vfoB.append(rs)
def convert_bytes_to_offset(bytes):
real_offset = 0
for byte in bytes:
real_offset = (real_offset * 10) + byte
return chirp_common.format_freq(real_offset * 1000)
def apply_offset(setting, obj):
value = chirp_common.parse_freq(str(setting.value)) / 1000
for i in range(5, -1, -1):
obj.offset[i] = value % 10
value /= 10
offA = RadioSettingValueString(
0, 10, convert_bytes_to_offset(_mem.vfo.a.offset))
rs = RadioSetting("vfo.a.offset",
"Offset (0.0-999.999)", offA)
rs.set_apply_callback(apply_offset, _mem.vfo.a)
rs.set_doc(_("Sets the difference in MHz between VFO A's receive "
"and transmit frequencies. The Shift setting "
"determines whether this offset is added or "
"subtracted."))
vfoA.append(rs)
offB = RadioSettingValueString(
0, 10, convert_bytes_to_offset(_mem.vfo.b.offset))
rs = RadioSetting("vfo.b.offset",
"Offset (0.0-999.999)", offB)
rs.set_apply_callback(apply_offset, _mem.vfo.b)
rs.set_doc(_("Sets the difference in MHz between VFO B's receive "
"and transmit frequencies. The Shift setting "
"determines whether this offset is added or "
"subtracted."))
vfoB.append(rs)
POWER_LEVELS = [str(x) for x in self.POWER_LEVELS]
if _mem.vfo.a.lowpower >= len(POWER_LEVELS):
val = 0
else:
val = _mem.vfo.a.lowpower
rs = RadioSetting("vfo.a.lowpower", "Power",
RadioSettingValueList(POWER_LEVELS,
current_index=val))
rs.set_doc(_("Selects VFO A's transmitter power. Use the lowest "
"power that provides reliable communication."))
vfoA.append(rs)
if _mem.vfo.b.lowpower >= len(POWER_LEVELS):
val = 0
else:
val = _mem.vfo.b.lowpower
rs = RadioSetting("vfo.b.lowpower", "Power",
RadioSettingValueList(POWER_LEVELS,
current_index=val))
rs.set_doc(_("Selects VFO B's transmitter power. Use the lowest "
"power that provides reliable communication."))
vfoB.append(rs)
if _mem.vfo.a.wide >= len(LIST_BANDWIDTH):
val = 0
else:
val = _mem.vfo.a.wide
rs = RadioSetting("vfo.a.wide", "Bandwidth",
RadioSettingValueList(LIST_BANDWIDTH,
current_index=val))
rs.set_doc(_("Selects wide or narrow FM for VFO A. Use Narrow where "
"required by the channel plan to reduce "
"interference to adjacent channels."))
vfoA.append(rs)
if _mem.vfo.b.wide >= len(LIST_BANDWIDTH):
val = 0
else:
val = _mem.vfo.b.wide
rs = RadioSetting("vfo.b.wide", "Bandwidth",
RadioSettingValueList(LIST_BANDWIDTH,
current_index=val))
rs.set_doc(_("Selects wide or narrow FM for VFO B. Use Narrow where "
"required by the channel plan to reduce "
"interference to adjacent channels."))
vfoB.append(rs)
if _mem.vfo.a.scode >= len(self.SCODE_LIST):
val = 0
else:
val = _mem.vfo.a.scode
rs = RadioSetting("vfo.a.scode", "Signal Code",
RadioSettingValueList(self.SCODE_LIST,
current_index=val))
rs.set_doc(_("Selects which of the %i programmed DTMF PTT-ID codes "
"VFO A uses. Edit the code itself under DTMF "
"Encode Settings.") % len(self.SCODE_LIST))
vfoA.append(rs)
if _mem.vfo.b.scode >= len(self.SCODE_LIST):
val = 0
else:
val = _mem.vfo.b.scode
rs = RadioSetting("vfo.b.scode", "Signal Code",
RadioSettingValueList(self.SCODE_LIST,
current_index=val))
rs.set_doc(_("Selects which of the %i programmed DTMF PTT-ID codes "
"VFO B uses. Edit the code itself under DTMF "
"Encode Settings.") % len(self.SCODE_LIST))
vfoB.append(rs)
if _mem.vfo.a.step >= len(self.STEPS):
val = 0
else:
val = _mem.vfo.a.step
rs = RadioSetting("vfo.a.step", "Tuning Step",
RadioSettingValueList(self.LIST_STEPS,
current_index=val))
rs.set_doc(_("Sets how far VFO A moves for each tuning step and "
"while scanning in Frequency mode. Choose the "
"spacing used by the frequencies you intend to "
"tune."))
vfoA.append(rs)
if _mem.vfo.b.step >= len(self.STEPS):
val = 0
else:
val = _mem.vfo.b.step
rs = RadioSetting("vfo.b.step", "Tuning Step",
RadioSettingValueList(self.LIST_STEPS,
current_index=val))
rs.set_doc(_("Sets how far VFO B moves for each tuning step and "
"while scanning in Frequency mode. Choose the "
"spacing used by the frequencies you intend to "
"tune."))
vfoB.append(rs)
workmode.append(vfoA)
workmode.append(vfoB)
def get_settings_common_bank(self, bank, _mem):
def _filterName(name):
fname = b""
for char in name:
if ord(str(char)) == 255:
break
fname += int(char).to_bytes(1, 'big')
return fname.decode('gb2312').strip()
def apply_bankname(setting, obj):
name = str(setting.value).encode('gb2312')[:16].ljust(16, b"\xff")
obj.name = name
if self._has_support_for_banknames:
for i in range(0, 10):
_nameobj = self._memobj.bank_name[i]
rs = RadioSetting("bank_name/%i.name" % i,
"Bank name %i" % (i + 1),
RadioSettingValueString(
0, 16, _filterName(_nameobj.name), False,
CHARSET_GB2312))
if self.MODEL == "BF-F8HP-PRO":
rs.set_doc("12 characters maximum (only the first 6 show "
"up on the main display)")
rs.set_apply_callback(apply_bankname, _nameobj)
bank.append(rs)
def get_settings(self):
"""Translate the bit in the mem_struct into settings in the UI"""
_mem = self._memobj
supported = []
basic = RadioSettingGroup("basic", "Basic Settings")
self.get_settings_common_basic(basic, _mem)
self.get_settings_pro_basic(basic, _mem)
supported.append(basic) # add basic menu
if self._has_workmode_support:
workmode = RadioSettingGroup("workmode", "Work Mode Settings")
self.get_settings_common_workmode(workmode, _mem)
supported.append(workmode) # add workmode menu if supported
dtmfe = RadioSettingGroup("dtmfe", "DTMF Encode Settings")
self.get_settings_common_dtmf(dtmfe, _mem)
self.get_settings_pro_dtmf(dtmfe, _mem)
supported.append(dtmfe) # add dtmfe menu
if self._has_support_for_banknames:
bank = RadioSettingGroup("bank", "Bank names")
self.get_settings_common_bank(bank, _mem)
supported.append(bank) # add bank menu if supported
top = RadioSettings(*tuple(supported))
return top
def set_settings(self, settings):
_settings = self._memobj.settings
for element in settings:
if not isinstance(element, RadioSetting):
self.set_settings(element)
continue
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 == "pwronpwd":
numstr = self._num2bbcd(element.value)
setattr(self._memobj.settings, setting, numstr)
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
def sync_in(self):
"""Download from radio"""
try:
data = self.download_function()
except errors.RadioError:
# Pass through any real errors we raise
raise
except Exception:
# If anything unexpected happens, make sure we raise
# a RadioError and log the problem
LOG.exception('Unexpected error during download')
raise errors.RadioError('Unexpected error communicating '
'with the radio')
self._mmap = memmap.MemoryMapBytes(data)
self.process_mmap()
def sync_out(self):
"""Upload to radio"""
try:
self.upload_function()
except errors.RadioError:
raise
except Exception:
# If anything unexpected happens, make sure we raise
# a RadioError and log the problem
LOG.exception('Unexpected error during upload')
raise errors.RadioError('Unexpected error communicating '
'with the radio')
def get_features(self):
"""Get the radio's features"""
rf = chirp_common.RadioFeatures()
rf.has_settings = True
rf.has_bank = False
rf.has_tuning_step = False
rf.can_odd_split = True
rf.has_name = True
rf.has_offset = True
rf.has_mode = True
rf.has_dtcs = True
rf.has_rx_dtcs = True
rf.has_dtcs_polarity = True
rf.has_ctone = True
rf.has_cross = True
rf.valid_modes = self.MODES
rf.valid_characters = self.VALID_CHARS
rf.valid_name_length = self.LENGTH_NAME
if self._gmrs:
rf.valid_duplexes = ["", "+", "off"]
else:
rf.valid_duplexes = ["", "-", "+", "split", "off"]
rf.valid_tmodes = ['', 'Tone', 'TSQL', 'DTCS', 'Cross']
rf.valid_cross_modes = [
"Tone->Tone",
"DTCS->",
"->DTCS",
"Tone->DTCS",
"DTCS->Tone",
"->Tone",
"DTCS->DTCS"]
rf.valid_skips = self.SKIP_VALUES
rf.valid_dtcs_codes = self.DTCS_CODES
rf.memory_bounds = (1, self.CHANNELS)
rf.valid_power_levels = self.POWER_LEVELS
rf.valid_bands = self.VALID_BANDS
rf.valid_tuning_steps = self.STEPS
return rf
def validate_memory(self, mem):
msgs = []
if 'AM' in self.MODES:
in_range = chirp_common.in_range
airbands = self.AIRBANDS
if in_range(mem.freq, airbands) and mem.mode != 'AM':
msgs.append(chirp_common.ValidationWarning(
_('Frequency in this range requires AM mode')))
if not in_range(mem.freq, airbands) and mem.mode == 'AM':
msgs.append(chirp_common.ValidationWarning(
_('Frequency in this range must not be AM mode')))
return msgs + super().validate_memory(mem)
def decode_tone(self, val):
mode = ""
pol = "N"
if val in [0, 0xFFFF]:
xval = 0
elif val >= 0x0258:
mode = "Tone"
xval = int(val) / 10.0
elif val <= 0x0258:
mode = "DTCS"
if val > 0x69:
index = val - 0x6A
pol = "R"
else:
index = val - 1
xval = self.DTCS_CODES[index]
else:
LOG.warn("Bug: tone is %04x" % val)
return mode, xval, pol
def encode_tone(self, memtone, mode, tone, pol):
if mode == "Tone":
memtone.set_value(int(tone * 10))
elif mode == "TSQL":
memtone.set_value(int(tone * 10))
elif mode == "DTCS":
if pol == 'R':
memtone.set_value(self.DTCS_CODES.index(tone) + 1 + 0x69)
else:
memtone.set_value(self.DTCS_CODES.index(tone) + 1)
else:
memtone.set_value(0)
def split_txfreq(self, _mem, freq):
if self._is_txinh(_mem):
# TX freq not set
duplex = "off"
offset = 0
else:
offset = (int(_mem.txfreq) * 10) - freq
if offset != 0:
if chirp_common.is_split(self.get_features().valid_bands,
freq, int(_mem.txfreq) * 10):
duplex = "split"
offset = int(_mem.txfreq) * 10
elif offset < 0:
offset = abs(offset)
duplex = "-"
elif offset > 0:
duplex = "+"
else:
duplex = ""
offset = 0
return offset, duplex
def get_memory_common(self, _mem, name, mem):
if _mem.get_raw()[0] == 255:
mem.empty = True
return mem
mem.freq = int(_mem.rxfreq) * 10
# TX freq set
mem.offset, mem.duplex = self.split_txfreq(_mem, mem.freq)
txtone = self.decode_tone(_mem.txtone)
rxtone = self.decode_tone(_mem.rxtone)
chirp_common.split_tone_decode(mem, txtone, rxtone)
if not _mem.scan:
mem.skip = "S"
levels = self.POWER_LEVELS
try:
mem.power = levels[_mem.lowpower]
except IndexError:
mem.power = levels[0]
mem.mode = _mem.wide and self.MODES[0] or self.MODES[1]
if chirp_common.in_range(mem.freq, self.AIRBANDS):
mem.mode = "AM"
mem.extra = RadioSettingGroup("Extra", "extra")
rs = RadioSetting("bcl", "BCL",
RadioSettingValueBoolean(_mem.bcl))
rs.set_doc(_("Prevents transmitting on a channel that is already in "
"use. Pressing PTT on a busy channel sounds an alert "
"tone instead of keying the transmitter."))
mem.extra.append(rs)
rs = RadioSetting("pttid", "PTT ID",
RadioSettingValueList(self.PTTID_LIST,
current_index=_mem.pttid))
rs.set_doc(_("Controls when this channel sends the selected DTMF "
"PTT-ID: BOT when PTT is pressed, EOT when PTT is "
"released, Both at both times, or Off."))
mem.extra.append(rs)
scode = (_mem.scode - self._scode_offset) % len(self.SCODE_LIST)
rs = RadioSetting("scode", "S-CODE",
RadioSettingValueList(self.SCODE_LIST,
current_index=scode))
rs.set_doc(_("Selects which of the %i programmed DTMF PTT-ID codes "
"this channel transmits. Edit the code itself under "
"DTMF Encode Settings.") % len(self.SCODE_LIST))
mem.extra.append(rs)
if self._has_scramble:
rs = RadioSetting("scramble", "Scramble",
RadioSettingValueList(
self.SCRAMBLE_LIST,
current_index=_mem.scramble))
mem.extra.append(rs)
if self.MODEL in ["BF-F8HP-PRO"]:
rs = RadioSetting("sqmode", "RX DTMF",
RadioSettingValueList(
self.RXDTMF_LIST,
current_index=_mem.sqmode))
mem.extra.append(rs)
rs = RadioSetting("fhss", "Encryption",
RadioSettingValueBoolean(_mem.fhss))
mem.extra.append(rs)
if self.MODEL in ["K6", "UV-5R Mini"]:
rs = RadioSetting("sqmode", "RX DTMF",
RadioSettingValueBoolean(_mem.sqmode))
rs.set_doc(_("Selects whether the radio uses DTMF codes, "
"together with CTCSS or DCS, to decide when to "
"unmute the speaker on this channel. Off unmutes "
"it on the correct CTCSS or DCS alone."))
mem.extra.append(rs)
rs = RadioSetting("fhss", "FHSS",
RadioSettingValueBoolean(_mem.fhss))
rs.set_doc(_("Turns on the privacy function the manual calls "
"Encryption for this channel. Other radios can "
"only understand the conversation if they have it "
"turned on as well and use the same DCS code."))
mem.extra.append(rs)
mem.name = str(name).replace('\xFF', ' ').replace('\x00', ' ').rstrip()
def _get_raw_memory(self, number):
return self._memobj.memory[number - 1]
def get_raw_memory(self, number):
return repr(self._get_raw_memory(number))
def get_memory(self, number):
_mem = self._get_raw_memory(number)
mem = chirp_common.Memory()
mem.number = number
self.get_memory_common(_mem, _mem.name, mem)
return mem
def unsplit_txfreq(self, mem):
_mem = self._get_raw_memory(mem.number)
if mem.duplex == "off":
for i in range(0, 4):
_mem.txfreq[i].set_raw(b"\xFF")
elif mem.duplex == "split":
_mem.txfreq = mem.offset / 10
elif mem.duplex == "+":
_mem.txfreq = (mem.freq + mem.offset) / 10
elif mem.duplex == "-":
_mem.txfreq = (mem.freq - mem.offset) / 10
else:
_mem.txfreq = mem.freq / 10
def set_memory_common(self, mem, _mem):
_mem.rxfreq = mem.freq / 10
self.unsplit_txfreq(mem)
((txmode, txtone, txpol), (rxmode, rxtone, rxpol)) = \
chirp_common.split_tone_encode(mem)
self.encode_tone(_mem.txtone, txmode, txtone, txpol)
self.encode_tone(_mem.rxtone, rxmode, rxtone, rxpol)
_mem.scan = mem.skip != "S"
_mem.wide = mem.mode == self.MODES[0]
if mem.power:
_mem.lowpower = self.POWER_LEVELS.index(mem.power)
else:
_mem.lowpower = 0
# extra settings
if len(mem.extra) > 0:
# there are setting, parse
for setting in mem.extra:
if setting.get_name() == "scode":
setattr(_mem, setting.get_name(), str(int(setting.value) +
self._scode_offset))
else:
setattr(_mem, setting.get_name(), setting.value)
else:
# there are no extra settings, load defaults
_mem.bcl = 0
_mem.pttid = 0
_mem.scode = self._scode_offset
def set_memory(self, mem):
_mem = self._get_raw_memory(mem.number)
_mem.set_raw(b"\x00"*16 + b"\xff" * 16)
if mem.empty:
_mem.set_raw(b"\xff" * 32)
return
_namelength = self.get_features().valid_name_length
_mem.name = mem.name.ljust(_namelength, '\xFF')
self.set_memory_common(mem, _mem)
@directory.register
class UV21ProV2(UV17Pro):
VENDOR = "Baofeng"
MODEL = "UV-21ProV2"
_airband_rx = (108000000, 135999999)
_vhf_range = (136000000, 174000000)
_vhf2_range = (220000000, 260000000)
_uhf_rx1_range = (350000000, 390000000)
_uhf_range = (400000000, 479999999)
_uhf_rx2_range = (480000000, 520000000)
VALID_BANDS = [_airband_rx,
_vhf_range,
_vhf2_range,
_uhf_rx1_range,
_uhf_range,
_uhf_rx2_range]
MODES = UV17Pro.MODES + ['AM']
@directory.register
class UV25(UV17Pro):
VENDOR = "Baofeng"
MODEL = "UV-25"
@directory.register
class UV17ProGPS(UV17Pro):
VENDOR = "Baofeng"
MODEL = "UV-17ProGPS"
_has_support_for_banknames = True
_has_workmode_support = True
_idents = [MSTRING_UV17PROGPS]
_magics = [(b"\x46", 16),
(b"\x4d", 7),
(b"\x53\x45\x4E\x44\x21\x05\x0D\x01\x01" +
b"\x01\x04\x11\x08\x05\x0D\x0D\x01\x11\x0F\x09\x12\x09" +
b"\x10\x04\x00", 1)]
_has_when_to_send_aniid = False
_vfoscan = True
_has_gps = True
_has_voxsw = True
_has_pilot_tone = True
_has_send_id_delay = True
_has_skey2_short = True
VALID_BANDS = [UV17Pro._airband, UV17Pro._vhf_range, UV17Pro._vhf2_range,
UV17Pro._uhf_range, UV17Pro._uhf2_range]
MODES = UV17Pro.MODES + ['AM']
def get_bank_model(self):
return chirp_common.StaticBankModel(self, banks=10)
def get_features(self):
rf = super().get_features()
rf.has_bank = True
return rf
class UV17ProGPSGMRS(UV17ProGPS):
"""UV-17Pro GPS variant with GMRS frequency-based enforcement.
Shared base for GMRS radios in the UV-17Pro GPS family that use
frequency-based TX restrictions with implied modes.
"""
_gmrs = True
_low_power_index = 1
def get_features(self):
rf = super().get_features()
rf.valid_duplexes = ["", "-", "+", "split", "off"]
return rf
def _apply_gmrs_limits(self, mem):
if mem.duplex == "off":
return mem.mode, mem.power
if mem.duplex == "split":
tx_freq = mem.offset
elif mem.duplex == "+":
tx_freq = mem.freq + mem.offset
elif mem.duplex == "-":
tx_freq = mem.freq - mem.offset
else:
tx_freq = mem.freq
if tx_freq in bandplan_na.GMRS_HHONLY:
mode = "NFM"
power = self.POWER_LEVELS[self._low_power_index]
else:
mode = mem.mode
power = mem.power
return mode, power
def get_memory(self, number):
mem = super().get_memory(number)
if mem.empty:
return mem
mem.mode, mem.power = self._apply_gmrs_limits(mem)
return mem
def set_memory(self, mem):
super().set_memory(mem)
mode, power = self._apply_gmrs_limits(mem)
_mem = self._get_raw_memory(mem.number)
if power is not None:
_mem.lowpower = self.POWER_LEVELS.index(power)
if mode is not None:
_mem.wide = mode == self.MODES[0]
def validate_memory(self, mem):
msgs = super().validate_memory(mem)
if mem.empty:
return msgs
msg_tx = "TX frequency must be a GMRS frequency"
msg_low_power = (
"GMRS 467 MHz interstitial frequencies require low power"
)
msg_nfm = (
"GMRS 467 MHz interstitial frequencies require narrowband (NFM)"
)
gmrs_tx_freqs = set(bandplan_na.ALL_GMRS_FREQS)
gmrs_tx_freqs.update(
freq + 5000000 for freq in bandplan_na.GMRS_HIRPT
)
if mem.duplex == "off":
tx_freq = None
elif mem.duplex == "split":
tx_freq = mem.offset
elif mem.duplex == "+":
tx_freq = mem.freq + mem.offset
elif mem.duplex == "-":
tx_freq = mem.freq - mem.offset
else:
tx_freq = mem.freq
if tx_freq is not None and tx_freq not in gmrs_tx_freqs:
msgs.append(chirp_common.ValidationWarning(msg_tx))
if tx_freq in bandplan_na.GMRS_HHONLY:
if mem.power != self.POWER_LEVELS[self._low_power_index]:
msgs.append(chirp_common.ValidationWarning(msg_low_power))
if mem.mode != "NFM":
msgs.append(chirp_common.ValidationWarning(msg_nfm))
return msgs
@directory.register
class RadioddityGM30Plus(UV17ProGPSGMRS):
"""Radioddity GM-30 Plus"""
VENDOR = "Radioddity"
MODEL = "GM-30 Plus"
@directory.register
class RadioddityGM30Pro(UV17ProGPSGMRS):
"""Radioddity GM-30 Pro"""
VENDOR = "Radioddity"
MODEL = "GM-30 Pro"
_has_gps = False
_has_bt = True
_has_scramble = True
@directory.register
class BaofengGM21(UV17ProGPSGMRS):
"""Baofeng GM-21"""
VENDOR = "Baofeng"
MODEL = "GM-21"
_has_gps = False
_has_bt = True
@directory.register
class UV32(UV17ProGPS):
VENDOR = "Baofeng"
MODEL = "UV-32"
POWER_LEVELS = [chirp_common.PowerLevel("High", watts=10.00),
chirp_common.PowerLevel("Low", watts=2.00),
chirp_common.PowerLevel("Medium", watts=5.00)]
@directory.register
class UV21ProGPS(UV17ProGPS):
VENDOR = "Baofeng"
MODEL = "UV-21ProGPS"
VALID_BANDS = UV21ProV2.VALID_BANDS
MODES = UV21ProV2.MODES
@directory.register
class UV28Plus(UV17ProGPS):
VENDOR = "Baofeng"
MODEL = "UV-28Plus"
VALID_BANDS = UV21ProV2.VALID_BANDS
MODES = UV21ProV2.MODES
POWER_LEVELS = [chirp_common.PowerLevel("High", watts=10.00),
chirp_common.PowerLevel("Low", watts=2.00),
chirp_common.PowerLevel("Medium", watts=5.00)]
_has_bt = True
@directory.register
class BF5RM(UV17Pro):
VENDOR = "Baofeng"
MODEL = "5RM"
VALID_BANDS = [UV17Pro._airband, UV17Pro._vhf_range, UV17Pro._vhf2_range,
UV17Pro._uhf_range, UV17Pro._uhf2_range]
POWER_LEVELS = [chirp_common.PowerLevel("High", watts=8.00),
chirp_common.PowerLevel("Low", watts=1.00),
chirp_common.PowerLevel("Medium", watts=5.00)]
SCODE_LIST = ["%s" % x for x in range(1, 16)]
SQUELCH_LIST = ["Off"] + list("123456789")
LIST_PW_SAVEMODE = ["Off", "1:1", "1:2", "1:4"]
_has_workmode_support = True
MODES = UV17Pro.MODES + ['AM']
@directory.register
class MaxTalkerP15(BF5RM):
VENDOR = 'MaxTalker'
MODEL = 'P15'
@directory.register
class MaxTalkerMT5RM(MaxTalkerP15):
MODEL = 'MT-5RM'
@directory.register
class BFK5Plus(BF5RM):
VENDOR = "Baofeng"
MODEL = "K5-Plus"
@directory.register
class BaofengUV5RMPlus(BF5RM):
VENDOR = "Baofeng"
MODEL = "UV-5RM Plus"
_magics = [(b"\x46", 16),
(b"\x4d", 15),
(b"\x53\x45\x4E\x44\x13\x0F\x0D\x13\x0E\x0E\x0E\x0D\x0A" +
b"\x0E\x0E\x02\x0F\x08\x04\x11\x0C\x00\x0D\x0E\x00", 1)]
_encrsym = 13
_has_pilot_tone = True
_has_send_id_delay = True
_has_scramble = True
@directory.register
class GM5RH(UV17Pro):
VENDOR = "Baofeng"
MODEL = "GM-5RH"
VALID_BANDS = [UV17Pro._vhf_range, UV17Pro._vhf2_range, UV17Pro._uhf_range]
POWER_LEVELS = [chirp_common.PowerLevel("High", watts=5.00),
chirp_common.PowerLevel("Low", watts=0.50),
chirp_common.PowerLevel("Medium", watts=3.00)]
SCODE_LIST = ["%s" % x for x in range(1, 16)]
LIST_PW_SAVEMODE = ["Off", "1:1", "2:1", "3:1", "4:1"]
_has_workmode_support = True
_idents = [MSTRING_GM5RH]
@directory.register
class UV5GPlus(GM5RH):
VENDOR = "Radioddity"
MODEL = "UV-5G Plus"
@directory.register
class UV17RPlus(UV17Pro):
VENDOR = "Baofeng"
MODEL = "UV-17R-Plus"
VALID_BANDS = [UV17Pro._airband, UV17Pro._vhf_range, UV17Pro._vhf2_range,
UV17Pro._uhf_range, UV17Pro._uhf2_range]
MODES = UV17Pro.MODES + ['AM']
@directory.register
class F8HPPro(UV17Pro):
VENDOR = "Baofeng"
MODEL = "BF-F8HP-PRO"
# ==========
# Notice to developers:
# The BF-F8HP-PRO support in this driver is currently based upon v0.52
# firmware.
# ==========
_idents = [MSTRING_BFF8HPPRO]
_magics = [(b"\x46", 16),
(b"\x4d", 6),
(b"\x53\x45\x4E\x44\x12\x0D\x0A\x0A\x10\x03\x0D\x02\x11\x0C" +
b"\x12\x0A\x11\x06\x04\x0E\x02\x09\x0D\x00\x00", 1)]
_encrsym = 3
STEPS = [2.5, 5.0, 6.25, 8.33, 10.0, 12.5, 20.0, 25.0, 50.0, 100.0]
LIST_STEPS = ["2.5", "5.0", "6.25", "10.0", "12.5", "20.0", "25.0", "50.0",
"100.0"]
_airband = (108000000, 136999999)
_vhf_range = (137000000, 173999999)
_airband2 = (174000000, 218999999)
_vhf2_range = (219000000, 224999999)
_airband3 = (225000000, 399999999)
AIRBANDS = [_airband, _airband2, _airband3]
VALID_BANDS = [_airband, _vhf_range, _airband2, _vhf2_range, _airband3,
UV17Pro._uhf_range]
POWER_LEVELS = [chirp_common.PowerLevel("High", watts=10.00),
chirp_common.PowerLevel("Low", watts=1.00),
chirp_common.PowerLevel('Mid', watts=3.00)]
LIST_POWER_ON_TIME = ['3 Seconds', '5 Seconds', '10 Seconds']
LIST_GPS_UNITS = ['km/h', 'MPH', 'kn']
LIST_POWERON_DISPLAY_TYPE = ["LOGO", "BATT voltage", "Station ID"]
LIST_BEEP = ["Off", "On"]
LIST_MENU_QUIT_TIME = ["Off", "10 sec", "20 sec", "30 sec", "60 sec"]
LIST_BACKLIGHT_TIMER = ["Always On", "5 sec", "10 sec", "15 sec",
"20 sec", "30 sec", "60 sec"]
LIST_ID_DELAY = ["%s ms" % x for x in range(100, 3100, 100)]
LIST_SKEY2_SHORT = ["FM", "Scan", "Search", "Vox", "TX Power", "NOAA",
"Zone Select", "Monitor", "Alarm", "Scan Edit"]
MODES = UV17Pro.MODES + ['AM']
SQUELCH_LIST = ["Off"] + list("12345678")
LIST_SKEY_DISABLE = ["Off", "SK Only", "PTT Only", "SK + PTT"]
LIST_MODE = ["Name", "Frequency", "Channel Number", "Name + Frequency"]
_has_support_for_banknames = True
_vfoscan = True
_has_gps = True
_has_voxsw = True
_has_pilot_tone = True
_has_send_id_delay = True
_has_skey1_short = True
_has_skey1_long = True
_has_skey2_short = True
_has_skey2_long = True
_has_skey_disable = True
_has_voice = False
_has_when_to_send_aniid = False
_has_zone_linking = True
MEM_STARTS = [0x0000, 0x9000, 0xA000, 0xD000]
MEM_SIZES = [0x8040, 0x0080, 0x02C0, 0x00C0]
MEM_TOTAL = 0x8440
_mem_params = {
'mems': 1000,
'ani': 0x80C0,
'pttid': 0x80E0,
'names': 0x8380,
}
_settings_format = """
struct settings_obj {
u8 squelch;
u8 savemode;
u8 vox;
u8 backlight;
u8 dualstandby;
u8 tot;
u8 beep;
u8 voicesw;
u8 voice;
u8 sidetone;
u8 scanmode;
u8 pttid;
u8 pttdly;
u8 chadistype;
u8 chbdistype;
u8 bcl;
u8 autolock;
u8 alarmmode;
u8 alarmtone;
u8 unknown1;
u8 tailclear;
u8 rpttailclear;
u8 rpttaildet;
u8 roger;
u8 a_or_b_selected; // a=0, b=1
u8 fmenable;
u8 chbworkmode:4,
chaworkmode:4;
u8 keylock;
u8 powerondistype;
u8 tone;
u8 unknown4[2];
u8 voxdlytime;
u8 menuquittime;
u8 unknown5[2];
u8 dispani;
u8 unknown11[2];
u8 skdisable;
u8 totalarm;
u8 zn8linkd:1,
zn7linkd:1,
zn6linkd:1,
zn5linkd:1,
zn4linkd:1,
zn3linkd:1,
zn2linkd:1,
zn1linkd:1;
u8 unknown6:6,
zn10linkd:1,
zn9linkd:1;
u8 ctsdcsscantype;
ul16 vfoscanmin;
ul16 vfoscanmax;
u8 gpsw;
u8 gpsmode;
u8 key1short;
u8 key1long;
u8 key2short;
u8 key2long;
u8 unknown8;
u8 rstmenu;
u8 singlewatch;
u8 hangup;
u8 voxsw;
u8 gpstimezone;
u8 lnkzones;
u8 inputdtmf;
u8 gpsunits;
u8 pontime;
char stationid[8];
};
"""
def get_settings_pro_dtmf(self, dtmfe, _mem):
super().get_settings_pro_dtmf(dtmfe, _mem)
rs = RadioSetting("ani.separatecode", "Separate Code",
RadioSettingValueList(
self.LIST_SEPARATE_CODE,
current_index=_mem.ani.separatecode))
dtmfe.append(rs)
rs = RadioSetting("ani.groupcallcode", "Group Call Code",
RadioSettingValueList(
self.LIST_GROUP_CALL_CODE,
current_index=_mem.ani.groupcallcode))
dtmfe.append(rs)
_codeobj = self._memobj.upcode.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("upcode.code", "Up Code", val)
rs.set_apply_callback(self.apply_code, self._memobj.upcode, 16)
dtmfe.append(rs)
_codeobj = self._memobj.downcode.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("downcode.code", "Down Code", val)
rs.set_apply_callback(self.apply_code, self._memobj.downcode, 16)
dtmfe.append(rs)
def get_settings_pro_basic(self, basic, _mem):
super().get_settings_pro_basic(basic, _mem)
rs = RadioSetting("settings.dispani", "Display ANI",
RadioSettingValueBoolean(_mem.settings.dispani))
basic.append(rs)
rs = RadioSetting("settings.pontime", "Power on Time",
RadioSettingValueList(
self.LIST_POWER_ON_TIME,
current_index=_mem.settings.pontime))
basic.append(rs)
rs = RadioSetting("settings.gpsunits", "GPS Speed Units",
RadioSettingValueList(
self.LIST_GPS_UNITS,
current_index=_mem.settings.gpsunits))
basic.append(rs)
rs = RadioSetting("settings.inputdtmf", "Input DTMF",
RadioSettingValueBoolean(_mem.settings.inputdtmf))
basic.append(rs)
rs = RadioSetting("settings.singlewatch", "Single Watch",
RadioSettingValueBoolean(_mem.settings.singlewatch))
basic.append(rs)
def _filterStationID(name):
fname = b""
for char in name:
if ord(str(char)) in [0, 255]:
break
fname += int(char).to_bytes(1, 'big')
return fname.decode('gb2312').strip()
def apply_stationid(setting, obj):
stationid = (str(setting.value).encode('gb2312')[:8].ljust(8,
b"\xFF"))
obj.stationid = stationid
_nameobj = self._memobj.settings
rs = RadioSetting("settings.stationid",
"StationID",
RadioSettingValueString(
0, 8, _filterStationID(_nameobj.stationid),
False, CHARSET_GB2312))
rs.set_apply_callback(apply_stationid, _nameobj)
basic.append(rs)
def get_bank_model(self):
return chirp_common.StaticBankModel(self, banks=10)
def get_features(self):
rf = super().get_features()
rf.has_bank = True
return rf
@directory.register
class UV5RH(UV17Pro):
VENDOR = "Baofeng"
MODEL = "UV-5RH"
VALID_BANDS = [UV17Pro._airband, UV17Pro._vhf_range, UV17Pro._vhf2_range,
UV17Pro._uhf_range, UV17Pro._uhf2_range]
POWER_LEVELS = [chirp_common.PowerLevel("High", watts=10.00),
chirp_common.PowerLevel("Low", watts=2.00),
chirp_common.PowerLevel("Medium", watts=5.00)]
SCODE_LIST = ["%s" % x for x in range(1, 16)]
SQUELCH_LIST = ["Off"] + list("123456789")
LIST_PW_SAVEMODE = ["Off", "1:1", "1:2", "1:4"]
MODES = UV17Pro.MODES + ['AM']
_has_workmode_support = True
@directory.register
class BFK6(UV17Pro):
VENDOR = "Baofeng"
MODEL = "K6"
_has_scramble = True
_idents = [MSTRING_BFK6]
_magics = [(b"\x46", 16),
]
_uses_encr = False
LIST_DTMFSPEED = ["%s ms" % x for x in [50, 100, 200, 300, 400, 500]]
_uhf_range = (400000000, 600000000)
VALID_BANDS = [UV17Pro._airband, UV17Pro._vhf_range, (UV17Pro._vhf2_range),
_uhf_range, UV17Pro._uhf2_range]
MODES = UV17Pro.MODES + ['AM']
POWER_LEVELS = [chirp_common.PowerLevel("High", watts=5.00),
chirp_common.PowerLevel("Low", watts=1.00)]
LIST_PW_SAVEMODE = ["Off", "1:1", "1:2", "1:4"]
LIST_POWERON_DISPLAY_TYPE = ["LOGO", "Message", "BATT voltage"]
LIST_BEEP = ["Off", "On"]
LIST_ID_DELAY = ["Off", "100 ms", "200 ms"] + ["%s ms" % x for x in range(
400, 1200, 200)]
LIST_SKEYS = ["Off", "Flashlight", "TX Power", "Scan", "VOX", "SOS",
"FM Radio"]
LIST_DUAL_WATCH = ["Single Display", "Dual Display, Dual Watch",
"Dual Display, Single Watch"]
_has_voxsw = True
_has_pilot_tone = True
_has_send_id_delay = True
_has_when_to_send_aniid = False
MEM_STARTS = [0x0000, 0x9000, 0xA000, 0xC000, 0xD000]
MEM_SIZES = [0x8040, 0x0080, 0x02C0, 0x0040, 0x0040]
MEM_TOTAL = 0x8400
_mem_params = {
'mems': 999,
'ani': 0x80C0,
'pttid': 0x80E0,
}
CHANNELS = 999
_settings_format = """
struct settings_obj {
u8 squelch;
u8 savemode;
u8 vox;
u8 backlight;
u8 dualstandby;
u8 tot;
u8 beep;
u8 voicesw;
u8 voice;
u8 sidetone;
u8 scanmode;
u8 pttid;
u8 pttdly;
u8 chadistype;
u8 chbdistype;
u8 bcl;
u8 autolock;
u8 alarmmode;
u8 alarmtone;
u8 tdr_tx;
u8 tailclear;
u8 rpttailclear;
u8 rpttaildet;
u8 roger;
u8 a_or_b_selected; // a=0, b=1
u8 fmenable;
u8 chbworkmode:4,
chaworkmode:4;
u8 keylock;
u8 unknown2:4,
powerontone:2,
powerondistype:2;
u8 tone;
u8 unknown4[2];
u8 voxdlytime;
u8 menuquittime;
u8 unknown5[2];
u8 dispani;
u8 unknown11[3];
u8 totalarm;
u8 lcdcontrast;
u8 invertlcd;
u8 ctsdcsscantype;
ul16 vfoscanmin;
ul16 vfoscanmax;
u8 fminterrupt;
u8 txdisable;
u8 key1short;
u8 key1long;
u8 key2short;
u8 unknown8[2];
u8 rstmenu;
u8 singlewatch;
u8 hangup;
u8 voxsw;
u8 gpstimezone;
u8 pwronpwdsw;
u8 pwronpwd[3];
char stationid[16];
};
"""
_end_fmt = """
#seekto 0x83C0;
struct {
u8 unknown1:4,
modetype:4;
u8 tx_220mhz;
u8 unknown2;
u8 tx_520mhz;
u8 rx_am;
} modes;
"""
def _bbcd2num(self, bcdarr, strlen=6):
# doing bbcd
LOG.debug(bcdarr.get_value())
string = ''.join("%02X" % b for b in bcdarr)
LOG.debug("@_bbcd2num, received: %s" % string)
if strlen <= 6:
string = string[:strlen]
return string
def _num2bbcd(self, value):
numstr = value.get_value()
numstr = str.ljust(numstr.strip(), 6, "F")
bcdarr = list(bytearray.fromhex(numstr))
LOG.debug("@_num2bbcd, sending: %s" % bcdarr)
return bcdarr
def get_settings_pro_basic(self, basic, _mem):
super().get_settings_pro_basic(basic, _mem)
if _mem.settings.tdr_tx >= len(LIST_OFF_AB):
val = 0x00
else:
val = _mem.settings.tdr_tx
rs = RadioSetting("settings.tdr_tx", "TDR TX",
RadioSettingValueList(
LIST_OFF_AB, current_index=val))
basic.append(rs)
if _mem.settings.powerontone >= len(LIST_POWERON_TONE):
val = 0x00
else:
val = _mem.settings.powerontone
rs = RadioSetting("settings.powerontone", "Power On Tone",
RadioSettingValueList(
LIST_POWERON_TONE, current_index=val))
basic.append(rs)
if _mem.settings.lcdcontrast >= len(LIST_LCD_CONTRAST):
val = 0x03
else:
val = _mem.settings.lcdcontrast
rs = RadioSetting("settings.lcdcontrast", "LCD Contrast",
RadioSettingValueList(
LIST_LCD_CONTRAST, current_index=val))
basic.append(rs)
rs = RadioSetting("settings.invertlcd", "Invert LCD",
RadioSettingValueBoolean(_mem.settings.invertlcd))
basic.append(rs)
rs = RadioSetting("settings.key1short", "PF1",
RadioSettingValueList(
self.LIST_SKEYS,
current_index=_mem.settings.key1short))
basic.append(rs)
rs = RadioSetting("settings.key1long", "PF1 Long",
RadioSettingValueList(
self.LIST_SKEYS,
current_index=_mem.settings.key1long))
basic.append(rs)
rs = RadioSetting("settings.key2short", "PF2",
RadioSettingValueList(
self.LIST_SKEYS,
current_index=_mem.settings.key2short))
basic.append(rs)
rs = RadioSetting("settings.fminterrupt", "FM Interrupt",
RadioSettingValueBoolean(_mem.settings.fminterrupt))
basic.append(rs)
rs = RadioSetting("settings.txdisable", "Disable TX",
RadioSettingValueBoolean(_mem.settings.txdisable))
basic.append(rs)
rs = RadioSetting("settings.pwronpwdsw", "Power On Password Switch",
RadioSettingValueBoolean(_mem.settings.pwronpwdsw))
basic.append(rs)
def _filter(name):
filtered = ""
for char in str(name):
if char in NUMERIC_CHARS:
filtered += char
else:
filtered += " "
return filtered
# setup power on password entry
codesetting = getattr(_mem.settings, "pwronpwd")
codestr = self._bbcd2num(codesetting, 6)
code = RadioSettingValueString(0, 6, _filter(codestr))
code.set_charset(NUMERIC_CHARS + list(" "))
rs = RadioSetting("settings.pwronpwd", "Power On Password", code)
basic.append(rs)
def _filterStationID(name):
fname = b""
for char in name:
if ord(str(char)) in [0, 255]:
break
fname += int(char).to_bytes(1, 'big')
return fname.decode('gb2312').strip()
def apply_stationid(setting, obj):
stationid = (str(setting.value).encode('gb2312')[:16].ljust(16,
b"\xFF"))
obj.stationid = stationid
_nameobj = self._memobj.settings
rs = RadioSetting("settings.stationid",
"StationID",
RadioSettingValueString(
0, 16, _filterStationID(_nameobj.stationid),
False, CHARSET_GB2312))
rs.set_apply_callback(apply_stationid, _nameobj)
basic.append(rs)
rs = RadioSetting("modes.modetype", "Factory Modes",
RadioSettingValueList(
LIST_MODES,
current_index=_mem.modes.modetype))
basic.append(rs)
rs = RadioSetting("modes.tx_220mhz", "220M TX",
RadioSettingValueBoolean(_mem.modes.tx_220mhz))
basic.append(rs)
rs = RadioSetting("modes.tx_520mhz", "520M TX",
RadioSettingValueBoolean(_mem.modes.tx_520mhz))
basic.append(rs)
rs = RadioSetting("modes.rx_am", "AM RX",
RadioSettingValueBoolean(_mem.modes.rx_am))
basic.append(rs)
def get_settings_pro_dtmf(self, dtmfe, _mem):
super().get_settings_pro_dtmf(dtmfe, _mem)
rs = RadioSetting("ani.separatecode", "Separate Code",
RadioSettingValueList(
self.LIST_SEPARATE_CODE,
current_index=_mem.ani.separatecode))
dtmfe.append(rs)
rs = RadioSetting("ani.groupcallcode", "Group Call Code",
RadioSettingValueList(
self.LIST_GROUP_CALL_CODE,
current_index=_mem.ani.groupcallcode))
dtmfe.append(rs)
_codeobj = self._memobj.upcode.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("upcode.code", "Up Code", val)
rs.set_apply_callback(self.apply_code, self._memobj.upcode, 16)
dtmfe.append(rs)
_codeobj = self._memobj.downcode.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("downcode.code", "Down Code", val)
rs.set_apply_callback(self.apply_code, self._memobj.downcode, 16)
dtmfe.append(rs)
@directory.register
class UV5RMini(UV17Pro):
"""Baofeng UV-5R Mini"""
VENDOR = "Baofeng"
MODEL = "UV-5R Mini"
LIST_BEEP = ["Off", "On"]
LIST_DTMFSPEED = ["%s ms" % x for x in [50, 100, 200, 300, 400, 500]]
LIST_SKEY2_SHORT = ["FM Radio", "Scan", "Search", "VOX", "Flashlight",
"SOS"]
MEM_STARTS = [0x0000, 0x9000, 0xA000]
MEM_SIZES = [0x8040, 0x0040, 0x01C0]
MEM_TOTAL = 0x8240
_is_on_ble = False
_has_support_for_banknames = False
_has_bt = True # allow BT setting
_idents = [MSTRING_UV17PROGPS]
_mem_size = MEM_TOTAL
_has_voxsw = True
_has_pilot_tone = True
_has_send_id_delay = True
_has_skey1_short = True
_mem_params = {
'mems': 999,
'ani': 0x8080,
'pttid': 0x80A0,
}
CHANNELS = 999
STEPS = [2.5, 5.0, 6.25, 8.33, 10.0, 12.5, 20.0, 25.0, 50.0]
_uhf_rx1_range = (350000000, 390000000)
_uhf_range = (400000000, 480000000)
_uhf_rx2_range = (480000000, 520000000)
VALID_BANDS = [UV17Pro._airband,
UV17Pro._vhf_range,
_uhf_rx1_range,
_uhf_range,
_uhf_rx2_range]
MODES = UV17Pro.MODES + ['AM']
BLE_UP_BLOCK_SIZE = 0x80 # uploading to radio over BLE uses 0x80 blocksize
def sync_in(self):
self._is_on_ble = platform.get_platform().is_ble_serial(self.pipe)
LOG.debug('Detected BLE: %s' % self._is_on_ble)
return super().sync_in()
def sync_out(self):
self._is_on_ble = platform.get_platform().is_ble_serial(self.pipe)
LOG.debug('Detected BLE: %s' % self._is_on_ble)
return super().sync_out()
def _upload(radio):
"""Upload to UV5R Mini (over BLE or USB/Serial)"""
# Put radio in program mode and identify it and
# determine if on BLE or USB/Serial connection
_do_ident(radio)
if radio._is_on_ble: # BLE upload needs a diff blocksize
_blocksize = radio.BLE_UP_BLOCK_SIZE
else:
_blocksize = radio.BLOCK_SIZE
data = b""
# UI progress
status = chirp_common.Status()
status.cur = 0
status.max = radio.MEM_TOTAL // _blocksize
status.msg = "Cloning to radio on %s/Serial..." % \
('BLE' if radio._is_on_ble else 'USB')
radio.status_fn(status)
data_addr = 0x00
radio_mem = radio.get_mmap()
radio.pipe.log('Uploading to radio using block size of 0x%04x'
% _blocksize)
for i in range(len(radio.MEM_SIZES)):
MEM_SIZE = radio.MEM_SIZES[i]
MEM_START = radio.MEM_STARTS[i]
for addr in range(MEM_START, MEM_START + MEM_SIZE,
_blocksize):
# calc min byte count to keep from reading too many bytes
byte_count = min(_blocksize, (MEM_START + MEM_SIZE) - addr)
data = radio_mem[data_addr:data_addr + byte_count]
if byte_count < _blocksize: # does block need padding?
radio.pipe.log('Padding partial block with \\xff '
'by 0x%02x bytes' %
(_blocksize - len(data)))
# pad partial block with \xff
data += (b'\xff' * (_blocksize - len(data)))
if radio._uses_encr:
data = _crypt(radio._encrsym, data)
data_addr += byte_count # advance read pointer
frame = radio._make_frame(b"W", addr, _blocksize, data)
radio.pipe.log('Sending address %04x' % addr)
bfc._rawsend(radio, frame)
ack = bfc._rawrecv(radio, 1)
if ack != b"\x06":
msg = "Bad ack writing block %04x" % addr
raise errors.RadioError(msg)
# UI Update
status.cur = data_addr // _blocksize
radio.status_fn(status)
return data
upload_function = _upload
_end_fmt = """
// #seekto 0x8220;
struct {
u8 unknown1[32];
} modes;
"""
def get_settings_pro_dtmf(self, dtmfe, _mem):
super().get_settings_pro_dtmf(dtmfe, _mem)
rs = RadioSetting("ani.separatecode", "Separate Code",
RadioSettingValueList(
self.LIST_SEPARATE_CODE,
current_index=_mem.ani.separatecode))
rs.set_doc(_("Selects the special DTMF digit interpreted as a "
"separator by the decoder."))
dtmfe.append(rs)
rs = RadioSetting("ani.groupcallcode", "Group Call Code",
RadioSettingValueList(
self.LIST_GROUP_CALL_CODE,
current_index=_mem.ani.groupcallcode))
rs.set_doc(_("Selects the special DTMF digit interpreted as a "
"group-call wildcard by the decoder. Off assigns no "
"group-call digit."))
dtmfe.append(rs)
_codeobj = self._memobj.upcode.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("upcode.code", "Up Code", val)
rs.set_doc(_("Programs the DTMF code sent at the beginning of a "
"transmission (BOT)."))
rs.set_apply_callback(self.apply_code, self._memobj.upcode, 16)
dtmfe.append(rs)
_codeobj = self._memobj.downcode.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("downcode.code", "Down Code", val)
rs.set_doc(_("Programs the DTMF code sent at the end of a "
"transmission (EOT)."))
rs.set_apply_callback(self.apply_code, self._memobj.downcode, 16)
dtmfe.append(rs)
@directory.register
class UV5GMini(UV5RMini):
# ==========
# Notice to developers:
# The UV-5G Mini support in this driver is
# currently based upon v0.05 firmware.
#
# This driver will also work with the UV-5G Mini with v0.01 firmware.
# For the UV-5G Mini with Fw 0.01 it is not necessary to use
# the UV-5R Mini driver
# ==========
"""Baofeng UV-5G Mini"""
VENDOR = "Baofeng"
MODEL = "UV-5G Mini"
_idents = [
b'PROGRAMGMRS5RMIU', # magic for Fw v0.05
MSTRING_UV17PROGPS, # magic for Fw v0.01
]
_low_power_index = 1
def get_memory(self, number):
mem = super().get_memory(number)
# inhibit changing freq, duplex and offset for GMRS channels 1-30
if mem.number >= 1 and mem.number <= 30:
mem.immutable = ['freq', 'duplex', 'offset']
# channels 8 - 14 have to be Narrow FM and Low power
if mem.number >= 8 and mem.number <= 14:
mem.mode = 'NFM'
# force low power
mem.power = self.POWER_LEVELS[self._low_power_index]
# inhibit changing freq, bandwidth, TX power, duplex and offset
mem.immutable = ['freq', 'mode', 'power',
'duplex', 'offset']
return mem
chirp-20261030/chirp/drivers/baofeng_uv3r.py 0000644 0001750 0001750 00000052235 14670320477 017570 0 ustar hibby hibby # Copyright 2011 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 .
"""Baofeng UV3r radio management module"""
import time
import logging
from chirp.drivers.wouxun_common import do_download, do_upload
from chirp import util, chirp_common, bitwise, errors, directory
from chirp.settings import RadioSetting, RadioSettingGroup, \
RadioSettingValueBoolean, RadioSettingValueList, \
RadioSettingValueInteger, RadioSettingValueString, \
RadioSettingValueFloat, RadioSettings
LOG = logging.getLogger(__name__)
def _uv3r_prep(radio):
radio.pipe.write(b"\x05PROGRAM")
ack = radio.pipe.read(1)
if ack != b"\x06":
raise errors.RadioError("Radio did not ACK first command")
radio.pipe.write(b"\x02")
ident = radio.pipe.read(8)
if len(ident) != 8:
LOG.debug(util.hexprint(ident))
raise errors.RadioError("Radio did not send identification")
radio.pipe.write(b"\x06")
if radio.pipe.read(1) != b"\x06":
raise errors.RadioError("Radio did not ACK ident")
def uv3r_prep(radio):
"""Do the UV3R identification dance"""
for _i in range(0, 10):
try:
return _uv3r_prep(radio)
except errors.RadioError as e:
time.sleep(1)
raise e
def uv3r_download(radio):
"""Talk to a UV3R and do a download"""
try:
uv3r_prep(radio)
return do_download(radio, 0x0000, 0x0E40, 0x0010)
except errors.RadioError:
raise
except Exception as e:
raise errors.RadioError("Failed to communicate with radio: %s" % e)
def uv3r_upload(radio):
"""Talk to a UV3R and do an upload"""
try:
uv3r_prep(radio)
return do_upload(radio, 0x0000, 0x0E40, 0x0010)
except errors.RadioError:
raise
except Exception as e:
raise errors.RadioError("Failed to communicate with radio: %s" % e)
UV3R_MEM_FORMAT = """
#seekto 0x0010;
struct {
lbcd rx_freq[4];
u8 rxtone;
lbcd offset[4];
u8 txtone;
u8 ishighpower:1,
iswide:1,
dtcsinvt:1,
unknown1:1,
dtcsinvr:1,
unknown2:1,
duplex:2;
u8 unknown;
lbcd tx_freq[4];
} tx_memory[99];
#seekto 0x0780;
struct {
lbcd lower_vhf[2];
lbcd upper_vhf[2];
lbcd lower_uhf[2];
lbcd upper_uhf[2];
} limits;
struct vfosettings {
lbcd freq[4];
u8 rxtone;
u8 unknown1;
lbcd offset[3];
u8 txtone;
u8 power:1,
bandwidth:1,
unknown2:4,
duplex:2;
u8 step;
u8 unknown3[4];
};
#seekto 0x0790;
struct {
struct vfosettings uhf;
struct vfosettings vhf;
} vfo;
#seekto 0x07C2;
struct {
u8 squelch;
u8 vox;
u8 timeout;
u8 save:1,
unknown_1:1,
dw:1,
ste:1,
beep:1,
unknown_2:1,
bclo:1,
ch_flag:1;
u8 backlight:2,
relaym:1,
scanm:1,
pri:1,
unknown_3:3;
u8 unknown_4[3];
u8 pri_ch;
} settings;
#seekto 0x07E0;
u16 fm_presets[16];
#seekto 0x0810;
struct {
lbcd rx_freq[4];
u8 rxtone;
lbcd offset[4];
u8 txtone;
u8 ishighpower:1,
iswide:1,
dtcsinvt:1,
unknown1:1,
dtcsinvr:1,
unknown2:1,
duplex:2;
u8 unknown;
lbcd tx_freq[4];
} rx_memory[99];
#seekto 0x1008;
struct {
u8 unknown[8];
u8 name[6];
u8 pad[2];
} names[128];
"""
STEPS = [5.0, 6.25, 10.0, 12.5, 20.0, 25.0]
STEP_LIST = [str(x) for x in STEPS]
BACKLIGHT_LIST = ["Off", "Key", "Continuous"]
TIMEOUT_LIST = ["Off"] + ["%s sec" % x for x in range(30, 210, 30)]
SCANM_LIST = ["TO", "CO"]
PRI_CH_LIST = ["Off"] + ["%s" % x for x in range(1, 100)]
CH_FLAG_LIST = ["Freq Mode", "Channel Mode"]
POWER_LIST = ["Low", "High"]
BANDWIDTH_LIST = ["Narrow", "Wide"]
DUPLEX_LIST = ["Off", "-", "+"]
STE_LIST = ["On", "Off"]
UV3R_DUPLEX = ["", "-", "+", ""]
UV3R_POWER_LEVELS = [chirp_common.PowerLevel("High", watts=2.00),
chirp_common.PowerLevel("Low", watts=0.50)]
UV3R_DTCS_POL = ["NN", "NR", "RN", "RR"]
@directory.register
class UV3RRadio(chirp_common.CloneModeRadio):
"""Baofeng UV-3R"""
VENDOR = "Baofeng"
MODEL = "UV-3R"
def get_features(self):
rf = chirp_common.RadioFeatures()
rf.has_settings = True
rf.valid_tmodes = ["", "Tone", "TSQL", "DTCS", "Cross"]
rf.valid_modes = ["FM", "NFM"]
rf.valid_power_levels = UV3R_POWER_LEVELS
rf.valid_bands = [(136000000, 235000000), (400000000, 529000000)]
rf.valid_skips = []
rf.valid_duplexes = ["", "-", "+", "split"]
rf.valid_cross_modes = ["Tone->Tone", "Tone->DTCS", "DTCS->Tone",
"->Tone", "->DTCS"]
rf.valid_tuning_steps = STEPS
rf.has_ctone = True
rf.has_cross = True
rf.has_tuning_step = False
rf.has_bank = False
rf.has_name = False
rf.can_odd_split = True
rf.memory_bounds = (1, 99)
return rf
def sync_in(self):
self._mmap = uv3r_download(self)
self.process_mmap()
def sync_out(self):
uv3r_upload(self)
def process_mmap(self):
self._memobj = bitwise.parse(UV3R_MEM_FORMAT, self._mmap)
def get_memory(self, number):
_mem = self._memobj.rx_memory[number - 1]
mem = chirp_common.Memory()
mem.number = number
if _mem.get_raw(asbytes=False)[0] == "\xff":
mem.empty = True
return mem
mem.freq = int(_mem.rx_freq) * 10
mem.offset = int(_mem.offset) * 10
mem.duplex = UV3R_DUPLEX[_mem.duplex]
if mem.offset > 60000000:
if mem.duplex == "+":
mem.offset = mem.freq + mem.offset
elif mem.duplex == "-":
mem.offset = mem.freq - mem.offset
mem.duplex = "split"
mem.power = UV3R_POWER_LEVELS[1 - _mem.ishighpower]
if not _mem.iswide:
mem.mode = "NFM"
dtcspol = (int(_mem.dtcsinvt) << 1) + _mem.dtcsinvr
mem.dtcs_polarity = UV3R_DTCS_POL[dtcspol]
if _mem.txtone in [0, 0xFF]:
txmode = ""
elif _mem.txtone < 0x33:
mem.rtone = chirp_common.TONES[_mem.txtone - 1]
txmode = "Tone"
elif _mem.txtone >= 0x33:
tcode = chirp_common.DTCS_CODES[_mem.txtone - 0x33]
mem.dtcs = tcode
txmode = "DTCS"
else:
LOG.warn("Bug: tx_mode is %02x" % _mem.txtone)
if _mem.rxtone in [0, 0xFF]:
rxmode = ""
elif _mem.rxtone < 0x33:
mem.ctone = chirp_common.TONES[_mem.rxtone - 1]
rxmode = "Tone"
elif _mem.rxtone >= 0x33:
rcode = chirp_common.DTCS_CODES[_mem.rxtone - 0x33]
mem.dtcs = rcode
rxmode = "DTCS"
else:
LOG.warn("Bug: rx_mode is %02x" % _mem.rxtone)
if txmode == "Tone" and not rxmode:
mem.tmode = "Tone"
elif txmode == rxmode and txmode == "Tone" and mem.rtone == mem.ctone:
mem.tmode = "TSQL"
elif txmode == rxmode and txmode == "DTCS":
mem.tmode = "DTCS"
elif rxmode or txmode:
mem.tmode = "Cross"
mem.cross_mode = "%s->%s" % (txmode, rxmode)
return mem
def _set_tone(self, _mem, which, value, mode):
if mode == "Tone":
val = chirp_common.TONES.index(value) + 1
elif mode == "DTCS":
val = chirp_common.DTCS_CODES.index(value) + 0x33
elif mode == "":
val = 0
else:
raise errors.RadioError("Internal error: tmode %s" % mode)
setattr(_mem, which, val)
def _set_memory(self, mem, _mem):
if mem.empty:
_mem.set_raw("\xff" * 16)
return
_mem.rx_freq = mem.freq / 10
if mem.duplex == "split":
diff = mem.freq - mem.offset
_mem.offset = abs(diff) / 10
_mem.duplex = UV3R_DUPLEX.index(diff < 0 and "+" or "-")
for i in range(0, 4):
_mem.tx_freq[i].set_raw("\xFF")
else:
_mem.offset = mem.offset / 10
_mem.duplex = UV3R_DUPLEX.index(mem.duplex)
_mem.tx_freq = (mem.freq + mem.offset) / 10
_mem.ishighpower = mem.power == UV3R_POWER_LEVELS[0]
_mem.iswide = mem.mode == "FM"
_mem.dtcsinvt = mem.dtcs_polarity[0] == "R"
_mem.dtcsinvr = mem.dtcs_polarity[1] == "R"
rxtone = txtone = 0
rxmode = txmode = ""
if mem.tmode == "DTCS":
rxmode = txmode = "DTCS"
rxtone = txtone = mem.dtcs
elif mem.tmode and mem.tmode != "Cross":
rxtone = txtone = mem.tmode == "Tone" and mem.rtone or mem.ctone
txmode = "Tone"
rxmode = mem.tmode == "TSQL" and "Tone" or ""
elif mem.tmode == "Cross":
txmode, rxmode = mem.cross_mode.split("->", 1)
if txmode == "DTCS":
txtone = mem.dtcs
elif txmode == "Tone":
txtone = mem.rtone
if rxmode == "DTCS":
rxtone = mem.dtcs
elif rxmode == "Tone":
rxtone = mem.ctone
self._set_tone(_mem, "txtone", txtone, txmode)
self._set_tone(_mem, "rxtone", rxtone, rxmode)
def set_memory(self, mem):
_tmem = self._memobj.tx_memory[mem.number - 1]
_rmem = self._memobj.rx_memory[mem.number - 1]
self._set_memory(mem, _tmem)
self._set_memory(mem, _rmem)
def get_settings(self):
_settings = self._memobj.settings
_vfo = self._memobj.vfo
basic = RadioSettingGroup("basic", "Basic Settings")
group = RadioSettings(basic)
rs = RadioSetting("squelch", "Squelch Level",
RadioSettingValueInteger(0, 9, _settings.squelch))
basic.append(rs)
rs = RadioSetting("backlight", "LCD Back Light",
RadioSettingValueList(
BACKLIGHT_LIST,
current_index=_settings.backlight))
basic.append(rs)
rs = RadioSetting("beep", "Keypad Beep",
RadioSettingValueBoolean(_settings.beep))
basic.append(rs)
rs = RadioSetting("vox", "VOX Level (0=OFF)",
RadioSettingValueInteger(0, 9, _settings.vox))
basic.append(rs)
rs = RadioSetting("dw", "Dual Watch",
RadioSettingValueBoolean(_settings.dw))
basic.append(rs)
rs = RadioSetting("ste", "Squelch Tail Eliminate",
RadioSettingValueList(
STE_LIST, current_index=_settings.ste))
basic.append(rs)
rs = RadioSetting("save", "Battery Saver",
RadioSettingValueBoolean(_settings.save))
basic.append(rs)
rs = RadioSetting("timeout", "Time Out Timer",
RadioSettingValueList(
TIMEOUT_LIST, current_index=_settings.timeout))
basic.append(rs)
rs = RadioSetting("scanm", "Scan Mode",
RadioSettingValueList(
SCANM_LIST, current_index=_settings.scanm))
basic.append(rs)
rs = RadioSetting("relaym", "Repeater Sound Response",
RadioSettingValueBoolean(_settings.relaym))
basic.append(rs)
rs = RadioSetting("bclo", "Busy Channel Lock Out",
RadioSettingValueBoolean(_settings.bclo))
basic.append(rs)
rs = RadioSetting("pri", "Priority Channel Scanning",
RadioSettingValueBoolean(_settings.pri))
basic.append(rs)
rs = RadioSetting("pri_ch", "Priority Channel",
RadioSettingValueList(
PRI_CH_LIST, current_index=_settings.pri_ch))
basic.append(rs)
rs = RadioSetting("ch_flag", "Display Mode",
RadioSettingValueList(
CH_FLAG_LIST, current_index=_settings.ch_flag))
basic.append(rs)
_limit = int(self._memobj.limits.lower_vhf) / 10
if _limit < 115 or _limit > 239:
_limit = 144
rs = RadioSetting("limits.lower_vhf", "VHF Lower Limit (115-239 MHz)",
RadioSettingValueInteger(115, 235, _limit))
def apply_limit(setting, obj):
value = int(setting.value) * 10
obj.lower_vhf = value
rs.set_apply_callback(apply_limit, self._memobj.limits)
basic.append(rs)
_limit = int(self._memobj.limits.upper_vhf) / 10
if _limit < 115 or _limit > 239:
_limit = 146
rs = RadioSetting("limits.upper_vhf", "VHF Upper Limit (115-239 MHz)",
RadioSettingValueInteger(115, 235, _limit))
def apply_limit(setting, obj):
value = int(setting.value) * 10
obj.upper_vhf = value
rs.set_apply_callback(apply_limit, self._memobj.limits)
basic.append(rs)
_limit = int(self._memobj.limits.lower_uhf) / 10
if _limit < 200 or _limit > 529:
_limit = 420
rs = RadioSetting("limits.lower_uhf", "UHF Lower Limit (200-529 MHz)",
RadioSettingValueInteger(200, 529, _limit))
def apply_limit(setting, obj):
value = int(setting.value) * 10
obj.lower_uhf = value
rs.set_apply_callback(apply_limit, self._memobj.limits)
basic.append(rs)
_limit = int(self._memobj.limits.upper_uhf) / 10
if _limit < 200 or _limit > 529:
_limit = 450
rs = RadioSetting("limits.upper_uhf", "UHF Upper Limit (200-529 MHz)",
RadioSettingValueInteger(200, 529, _limit))
def apply_limit(setting, obj):
value = int(setting.value) * 10
obj.upper_uhf = value
rs.set_apply_callback(apply_limit, self._memobj.limits)
basic.append(rs)
vfo_preset = RadioSettingGroup("vfo_preset", "VFO Presets")
group.append(vfo_preset)
def convert_bytes_to_freq(bytes):
real_freq = 0
real_freq = bytes
return chirp_common.format_freq(real_freq * 10)
def apply_vhf_freq(setting, obj):
value = chirp_common.parse_freq(str(setting.value)) / 10
obj.vhf.freq = value
val = RadioSettingValueString(
0, 10, convert_bytes_to_freq(int(_vfo.vhf.freq)))
rs = RadioSetting("vfo.vhf.freq",
"VHF RX Frequency (115.00000-236.00000)", val)
rs.set_apply_callback(apply_vhf_freq, _vfo)
vfo_preset.append(rs)
rs = RadioSetting("vfo.vhf.duplex", "Shift Direction",
RadioSettingValueList(
DUPLEX_LIST, current_index=_vfo.vhf.duplex))
vfo_preset.append(rs)
def convert_bytes_to_offset(bytes):
real_offset = 0
real_offset = bytes
return chirp_common.format_freq(real_offset * 10000)
def apply_vhf_offset(setting, obj):
value = chirp_common.parse_freq(str(setting.value)) / 10000
obj.vhf.offset = value
val = RadioSettingValueString(
0, 10, convert_bytes_to_offset(int(_vfo.vhf.offset)))
rs = RadioSetting("vfo.vhf.offset", "Offset (0.00-37.995)", val)
rs.set_apply_callback(apply_vhf_offset, _vfo)
vfo_preset.append(rs)
rs = RadioSetting("vfo.vhf.power", "Power Level",
RadioSettingValueList(
POWER_LIST, current_index=_vfo.vhf.power))
vfo_preset.append(rs)
rs = RadioSetting("vfo.vhf.bandwidth", "Bandwidth",
RadioSettingValueList(
BANDWIDTH_LIST,
current_index=_vfo.vhf.bandwidth))
vfo_preset.append(rs)
rs = RadioSetting("vfo.vhf.step", "Step",
RadioSettingValueList(
STEP_LIST, current_index=_vfo.vhf.step))
vfo_preset.append(rs)
def apply_uhf_freq(setting, obj):
value = chirp_common.parse_freq(str(setting.value)) / 10
obj.uhf.freq = value
val = RadioSettingValueString(
0, 10, convert_bytes_to_freq(int(_vfo.uhf.freq)))
rs = RadioSetting("vfo.uhf.freq",
"UHF RX Frequency (200.00000-529.00000)", val)
rs.set_apply_callback(apply_uhf_freq, _vfo)
vfo_preset.append(rs)
rs = RadioSetting("vfo.uhf.duplex", "Shift Direction",
RadioSettingValueList(
DUPLEX_LIST, current_index=_vfo.uhf.duplex))
vfo_preset.append(rs)
def apply_uhf_offset(setting, obj):
value = chirp_common.parse_freq(str(setting.value)) / 10000
obj.uhf.offset = value
val = RadioSettingValueString(
0, 10, convert_bytes_to_offset(int(_vfo.uhf.offset)))
rs = RadioSetting("vfo.uhf.offset", "Offset (0.00-69.995)", val)
rs.set_apply_callback(apply_uhf_offset, _vfo)
vfo_preset.append(rs)
rs = RadioSetting("vfo.uhf.power", "Power Level",
RadioSettingValueList(
POWER_LIST, current_index=_vfo.uhf.power))
vfo_preset.append(rs)
rs = RadioSetting("vfo.uhf.bandwidth", "Bandwidth",
RadioSettingValueList(
BANDWIDTH_LIST,
current_index=_vfo.uhf.bandwidth))
vfo_preset.append(rs)
rs = RadioSetting("vfo.uhf.step", "Step",
RadioSettingValueList(
STEP_LIST, current_index=_vfo.uhf.step))
vfo_preset.append(rs)
fm_preset = RadioSettingGroup("fm_preset", "FM Radio Presets")
group.append(fm_preset)
for i in range(0, 16):
if self._memobj.fm_presets[i] < 0x01AF:
used = True
preset = self._memobj.fm_presets[i] / 10.0 + 65
else:
used = False
preset = 65
rs = RadioSetting("fm_presets_%1i" % i, "FM Preset %i" % (i + 1),
RadioSettingValueBoolean(used),
RadioSettingValueFloat(65, 108, preset, 0.1, 1))
fm_preset.append(rs)
return group
def set_settings(self, settings):
_settings = self._memobj.settings
for element in settings:
if not isinstance(element, RadioSetting):
if element.get_name() == "fm_preset":
self._set_fm_preset(element)
else:
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
def _set_fm_preset(self, settings):
for element in settings:
try:
index = (int(element.get_name().split("_")[-1]))
val = list(element.value)
if val[0].get_value():
value = int(val[1].get_value() * 10 - 650)
else:
value = 0x01AF
LOG.debug("Setting fm_presets[%1i] = %s" % (index, value))
setting = self._memobj.fm_presets
setting[index] = value
except Exception:
LOG.debug(element.get_name())
raise
@classmethod
def match_model(cls, filedata, filename):
return len(filedata) == 3648
def get_raw_memory(self, number):
_rmem = self._memobj.tx_memory[number - 1]
_tmem = self._memobj.rx_memory[number - 1]
return repr(_rmem) + repr(_tmem)
chirp-20261030/chirp/drivers/baofeng_wp970i.py 0000644 0001750 0001750 00000100701 14670320477 017720 0 ustar hibby hibby # Copyright 2016:
# * Jim Unroe KC9HI,
#
# 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 .
import logging
from chirp.drivers import baofeng_common as bfc
from chirp import chirp_common, directory
from chirp import bitwise
from chirp.settings import RadioSettingGroup, RadioSetting, \
RadioSettingValueBoolean, RadioSettingValueList, \
RadioSettingValueString, RadioSettingValueInteger, \
RadioSettingValueFloat, RadioSettings, \
InvalidValueError
LOG = logging.getLogger(__name__)
# #### MAGICS #########################################################
# Baofeng WP970I magic string
MSTRING_WP970I = b"\x50\xBB\xFF\x20\x14\x04\x13"
# Baofeng UV-9G magic string
MSTRING_UV9G = b"\x50\xBB\xFF\x20\x12\x05\x25"
# Baofeng UV-S9X3 magic string
MSTRING_UVS9X3 = b"\x50\xBB\xFF\x20\x12\x07\x25"
DTMF_CHARS = "0123456789 *#ABCD"
STEPS = [2.5, 5.0, 6.25, 10.0, 12.5, 20.0, 25.0, 50.0]
LIST_AB = ["A", "B"]
LIST_ALMOD = ["Site", "Tone", "Code"]
LIST_BANDWIDTH = ["Wide", "Narrow"]
LIST_COLOR = ["Off", "Blue", "Orange", "Purple"]
LIST_DTMFSPEED = ["%s ms" % x for x in range(50, 2010, 10)]
LIST_DTMFST = ["Off", "DT-ST", "ANI-ST", "DT+ANI"]
LIST_MODE = ["Channel", "Name", "Frequency"]
LIST_OFF1TO9 = ["Off"] + list("123456789")
LIST_OFF1TO10 = LIST_OFF1TO9 + ["10"]
LIST_OFFAB = ["Off"] + LIST_AB
LIST_RESUME = ["TO", "CO", "SE"]
LIST_PONMSG = ["Full", "Message"]
LIST_PTTID = ["Off", "BOT", "EOT", "Both"]
LIST_SCODE = ["%s" % x for x in range(1, 16)]
LIST_RPSTE = ["Off"] + ["%s" % x for x in range(1, 11)]
LIST_SAVE = ["Off", "1:1", "1:2", "1:3", "1:4"]
LIST_SHIFTD = ["Off", "+", "-"]
LIST_STEDELAY = ["Off"] + ["%s ms" % x for x in range(100, 1100, 100)]
LIST_STEP = [str(x) for x in STEPS]
LIST_TIMEOUT = ["%s sec" % x for x in range(15, 615, 15)]
LIST_TXPOWER = ["High", "Mid", "Low"]
LIST_VOICE = ["Off", "English", "Chinese"]
LIST_WORKMODE = ["Frequency", "Channel"]
TXP_CHOICES = ["High", "Low"]
TXP_VALUES = [0x00, 0x02]
def model_match(cls, data):
"""Match the opened/downloaded image to the correct version"""
if len(data) > 0x2008:
rid = data[0x2008:0x2010]
return rid.startswith(cls.MODEL.encode())
elif len(data) == 0x2008:
rid = data[0x1EF0:0x1EF7]
return rid in cls._fileid
else:
return False
class WP970I(bfc.BaofengCommonHT):
"""Baofeng WP970I"""
VENDOR = "Baofeng"
MODEL = "WP970I"
_tri_band = False
_fileid = []
_magic = [MSTRING_WP970I, ]
_magic_response_length = 8
_fw_ver_start = 0x1EF0
_recv_block_size = 0x40
_mem_size = 0x2000
_ack_block = True
_ranges = [(0x0000, 0x0DF0),
(0x0E00, 0x1800),
(0x1EE0, 0x1EF0),
(0x1F60, 0x1F70),
(0x1F80, 0x1F90),
(0x1FC0, 0x1FD0)]
_send_block_size = 0x10
MODES = ["NFM", "FM"]
VALID_CHARS = chirp_common.CHARSET_ALPHANUMERIC + \
"!@#$%^&*()+-=[]:\";'<>?,./"
LENGTH_NAME = 6
SKIP_VALUES = ["", "S"]
DTCS_CODES = tuple(sorted(chirp_common.DTCS_CODES + (645,)))
POWER_LEVELS = [chirp_common.PowerLevel("High", watts=5.00),
chirp_common.PowerLevel("Med", watts=3.00),
chirp_common.PowerLevel("Low", watts=1.00)]
_vhf_range = (130000000, 180000000)
_vhf2_range = (200000000, 260000000)
_uhf_range = (400000000, 521000000)
VALID_BANDS = [_vhf_range,
_uhf_range]
PTTID_LIST = LIST_PTTID
SCODE_LIST = LIST_SCODE
MEM_FORMAT = """
// #seekto 0x0000;
struct {
lbcd rxfreq[4];
lbcd txfreq[4];
ul16 rxtone;
ul16 txtone;
u8 unused1:3,
isuhf:1,
scode:4;
u8 unknown1:7,
txtoneicon:1;
u8 mailicon:3,
unknown2:3,
lowpower:2;
u8 unknown3:1,
wide:1,
unknown4:2,
bcl:1,
scan:1,
pttid:2;
} memory[128];
#seekto 0x0B00;
struct {
u8 code[5];
u8 unused[11];
} pttid[15];
#seekto 0x0CAA;
struct {
u8 code[5];
u8 unused1:6,
aniid:2;
u8 unknown[2];
u8 dtmfon;
u8 dtmfoff;
} ani;
#seekto 0x0E20;
struct {
u8 squelch;
u8 step;
u8 unknown_e22;
u8 save;
u8 vox;
u8 unknown_e25;
u8 abr;
u8 tdr;
u8 beep;
u8 timeout;
u8 unknown_e2a[4];
u8 voice;
u8 unknown_e2f;
u8 dtmfst;
u8 unknown_e31;
u8 unknown_e32:6,
screv:2;
u8 pttid;
u8 pttlt;
u8 mdfa;
u8 mdfb;
u8 bcl;
u8 autolk;
u8 sftd;
u8 unknown_e3a[3];
u8 wtled;
u8 rxled;
u8 txled;
u8 almod;
u8 band;
u8 tdrab;
u8 ste;
u8 rpste;
u8 rptrl;
u8 ponmsg;
u8 roger;
u8 rogerrx;
u8 tdrch;
u8 displayab:1,
unknown_e4a1:2,
fmradio:1,
alarm:1,
unknown_e4a2:1,
reset:1,
menu:1;
u8 unknown_e4b:6,
singleptt:1,
vfomrlock:1;
u8 workmode;
u8 keylock;
} settings;
#seekto 0x0E76;
struct {
u8 unused1:1,
mrcha:7;
u8 unused2:1,
mrchb:7;
} wmchannel;
struct vfo {
u8 unknown0[8];
u8 freq[8];
u8 offset[6];
ul16 rxtone;
ul16 txtone;
u8 unused1:7,
band:1;
u8 unknown3;
u8 unused2:2,
sftd:2,
scode:4;
u8 unknown4;
u8 unused3:1,
step:3,
unused4:4;
u8 unused5:1,
widenarr:1,
unused6:4,
txpower3:2;
};
#seekto 0x0F00;
struct {
struct vfo a;
struct vfo b;
} vfo;
#seekto 0x0F4E;
u16 fm_presets;
#seekto 0x1000;
struct {
char name[7];
u8 unknown1[9];
} names[128];
#seekto 0x1ED0;
struct {
char line1[7];
char line2[7];
} sixpoweron_msg;
#seekto 0x1EE0;
struct {
char line1[7];
char line2[7];
} poweron_msg;
#seekto 0x1EF0;
struct {
char line1[7];
char line2[7];
} firmware_msg;
struct squelch {
u8 sql0;
u8 sql1;
u8 sql2;
u8 sql3;
u8 sql4;
u8 sql5;
u8 sql6;
u8 sql7;
u8 sql8;
u8 sql9;
};
#seekto 0x1F60;
struct {
struct squelch vhf;
u8 unknown1[6];
u8 unknown2[16];
struct squelch uhf;
} squelch;
struct limit {
u8 enable;
bbcd lower[2];
bbcd upper[2];
};
#seekto 0x1FC0;
struct {
struct limit vhf;
struct limit uhf;
struct limit vhf2;
} limits;
"""
@classmethod
def get_prompts(cls):
rp = chirp_common.RadioPrompts()
rp.experimental = \
('This driver is a beta version.\n'
'\n'
'Please save an unedited copy of your first successful\n'
'download to a CHIRP Radio Images(*.img) file.'
)
rp.pre_download = _(
"Follow these instructions to download your info:\n"
"1 - Turn off your radio\n"
"2 - Connect your interface cable\n"
"3 - Turn on your radio\n"
"4 - Do the download of your radio data\n")
rp.pre_upload = _(
"Follow this instructions to upload your info:\n"
"1 - Turn off your radio\n"
"2 - Connect your interface cable\n"
"3 - Turn on your radio\n"
"4 - Do the upload of your radio data\n")
return rp
def get_features(self):
rf = bfc.BaofengCommonHT.get_features(self)
rf.valid_tuning_steps = STEPS
return rf
def process_mmap(self):
"""Process the mem map into the mem object"""
self._memobj = bitwise.parse(self.MEM_FORMAT, self._mmap)
def get_settings(self):
"""Translate the bit in the mem_struct into settings in the UI"""
_mem = self._memobj
basic = RadioSettingGroup("basic", "Basic Settings")
advanced = RadioSettingGroup("advanced", "Advanced Settings")
other = RadioSettingGroup("other", "Other Settings")
work = RadioSettingGroup("work", "Work Mode Settings")
fm_preset = RadioSettingGroup("fm_preset", "FM Preset")
dtmfe = RadioSettingGroup("dtmfe", "DTMF Encode Settings")
service = RadioSettingGroup("service", "Service Settings")
top = RadioSettings(basic, advanced, other, work, fm_preset, dtmfe,
service)
# Basic settings
if _mem.settings.squelch > 0x09:
val = 0x00
else:
val = _mem.settings.squelch
rs = RadioSetting("settings.squelch", "Squelch",
RadioSettingValueList(
LIST_OFF1TO9, current_index=val))
basic.append(rs)
if _mem.settings.save > 0x04:
val = 0x00
else:
val = _mem.settings.save
rs = RadioSetting("settings.save", "Battery Saver",
RadioSettingValueList(
LIST_SAVE, current_index=val))
basic.append(rs)
if _mem.settings.vox > 0x0A:
val = 0x00
else:
val = _mem.settings.vox
rs = RadioSetting("settings.vox", "Vox",
RadioSettingValueList(
LIST_OFF1TO10, current_index=val))
basic.append(rs)
if _mem.settings.abr > 0x0A:
val = 0x00
else:
val = _mem.settings.abr
rs = RadioSetting("settings.abr", "Backlight Timeout",
RadioSettingValueList(
LIST_OFF1TO10, current_index=val))
basic.append(rs)
rs = RadioSetting("settings.tdr", "Dual Watch",
RadioSettingValueBoolean(_mem.settings.tdr))
basic.append(rs)
rs = RadioSetting("settings.beep", "Beep",
RadioSettingValueBoolean(_mem.settings.beep))
basic.append(rs)
if _mem.settings.timeout > 0x27:
val = 0x03
else:
val = _mem.settings.timeout
rs = RadioSetting("settings.timeout", "Timeout Timer",
RadioSettingValueList(
LIST_TIMEOUT, current_index=val))
basic.append(rs)
if _mem.settings.voice > 0x02:
val = 0x01
else:
val = _mem.settings.voice
rs = RadioSetting("settings.voice", "Voice Prompt",
RadioSettingValueList(
LIST_VOICE, current_index=val))
basic.append(rs)
rs = RadioSetting(
"settings.dtmfst", "DTMF Sidetone",
RadioSettingValueList(
LIST_DTMFST, current_index=_mem.settings.dtmfst))
basic.append(rs)
if _mem.settings.screv > 0x02:
val = 0x01
else:
val = _mem.settings.screv
rs = RadioSetting("settings.screv", "Scan Resume",
RadioSettingValueList(
LIST_RESUME, current_index=val))
basic.append(rs)
rs = RadioSetting(
"settings.pttid", "When to send PTT ID",
RadioSettingValueList(
LIST_PTTID, current_index=_mem.settings.pttid))
basic.append(rs)
if _mem.settings.pttlt > 0x1E:
val = 0x05
else:
val = _mem.settings.pttlt
rs = RadioSetting("pttlt", "PTT ID Delay",
RadioSettingValueInteger(0, 50, val))
basic.append(rs)
rs = RadioSetting(
"settings.mdfa", "Display Mode (A)",
RadioSettingValueList(
LIST_MODE, current_index=_mem.settings.mdfa))
basic.append(rs)
rs = RadioSetting(
"settings.mdfb", "Display Mode (B)",
RadioSettingValueList(
LIST_MODE, current_index=_mem.settings.mdfb))
basic.append(rs)
rs = RadioSetting("settings.autolk", "Automatic Key Lock",
RadioSettingValueBoolean(_mem.settings.autolk))
basic.append(rs)
rs = RadioSetting("settings.wtled", "Standby LED Color",
RadioSettingValueList(
LIST_COLOR, current_index=_mem.settings.wtled))
basic.append(rs)
rs = RadioSetting("settings.rxled", "RX LED Color",
RadioSettingValueList(
LIST_COLOR, current_index=_mem.settings.rxled))
basic.append(rs)
rs = RadioSetting("settings.txled", "TX LED Color",
RadioSettingValueList(
LIST_COLOR, current_index=_mem.settings.txled))
basic.append(rs)
val = _mem.settings.almod
rs = RadioSetting("settings.almod", "Alarm Mode",
RadioSettingValueList(
LIST_ALMOD, current_index=val))
basic.append(rs)
if _mem.settings.tdrab > 0x02:
val = 0x00
else:
val = _mem.settings.tdrab
rs = RadioSetting("settings.tdrab", "Dual Watch TX Priority",
RadioSettingValueList(
LIST_OFFAB, current_index=val))
basic.append(rs)
rs = RadioSetting("settings.ste", "Squelch Tail Eliminate (HT to HT)",
RadioSettingValueBoolean(_mem.settings.ste))
basic.append(rs)
if _mem.settings.rpste > 0x0A:
val = 0x00
else:
val = _mem.settings.rpste
rs = RadioSetting("settings.rpste",
"Squelch Tail Eliminate (repeater)",
RadioSettingValueList(
LIST_RPSTE, current_index=val))
basic.append(rs)
if _mem.settings.rptrl > 0x0A:
val = 0x00
else:
val = _mem.settings.rptrl
rs = RadioSetting("settings.rptrl", "STE Repeater Delay",
RadioSettingValueList(
LIST_STEDELAY, current_index=val))
basic.append(rs)
rs = RadioSetting(
"settings.ponmsg", "Power-On Message",
RadioSettingValueList(
LIST_PONMSG, current_index=_mem.settings.ponmsg))
basic.append(rs)
rs = RadioSetting("settings.roger", "Roger Beep",
RadioSettingValueBoolean(_mem.settings.roger))
basic.append(rs)
# Advanced settings
rs = RadioSetting("settings.reset", "RESET Menu",
RadioSettingValueBoolean(_mem.settings.reset))
advanced.append(rs)
rs = RadioSetting("settings.menu", "All Menus",
RadioSettingValueBoolean(_mem.settings.menu))
advanced.append(rs)
rs = RadioSetting("settings.fmradio", "Broadcast FM Radio",
RadioSettingValueBoolean(_mem.settings.fmradio))
advanced.append(rs)
rs = RadioSetting("settings.alarm", "Alarm Sound",
RadioSettingValueBoolean(_mem.settings.alarm))
advanced.append(rs)
# Other settings
def _filter(name):
filtered = ""
for char in str(name):
if char in chirp_common.CHARSET_ASCII:
filtered += char
else:
filtered += " "
return filtered
_msg = _mem.firmware_msg
val = RadioSettingValueString(0, 7, _filter(_msg.line1))
val.set_mutable(False)
rs = RadioSetting("firmware_msg.line1", "Firmware Message 1", val)
other.append(rs)
val = RadioSettingValueString(0, 7, _filter(_msg.line2))
val.set_mutable(False)
rs = RadioSetting("firmware_msg.line2", "Firmware Message 2", val)
other.append(rs)
_msg = _mem.sixpoweron_msg
val = RadioSettingValueString(0, 7, _filter(_msg.line1))
val.set_mutable(False)
rs = RadioSetting("sixpoweron_msg.line1", "6+Power-On Message 1", val)
other.append(rs)
val = RadioSettingValueString(0, 7, _filter(_msg.line2))
val.set_mutable(False)
rs = RadioSetting("sixpoweron_msg.line2", "6+Power-On Message 2", val)
other.append(rs)
_msg = _mem.poweron_msg
rs = RadioSetting("poweron_msg.line1", "Power-On Message 1",
RadioSettingValueString(
0, 7, _filter(_msg.line1)))
other.append(rs)
rs = RadioSetting("poweron_msg.line2", "Power-On Message 2",
RadioSettingValueString(
0, 7, _filter(_msg.line2)))
other.append(rs)
lower = 130
upper = 179
rs = RadioSetting("limits.vhf.lower", "VHF Lower Limit (MHz)",
RadioSettingValueInteger(
lower, upper, _mem.limits.vhf.lower))
other.append(rs)
rs = RadioSetting("limits.vhf.upper", "VHF Upper Limit (MHz)",
RadioSettingValueInteger(
lower, upper, _mem.limits.vhf.upper))
other.append(rs)
if self._tri_band:
lower = 200
upper = 260
rs = RadioSetting("limits.vhf2.lower", "VHF2 Lower Limit (MHz)",
RadioSettingValueInteger(
lower, upper, _mem.limits.vhf2.lower))
other.append(rs)
rs = RadioSetting("limits.vhf2.upper", "VHF2 Upper Limit (MHz)",
RadioSettingValueInteger(
lower, upper, _mem.limits.vhf2.upper))
other.append(rs)
lower = 400
upper = 520
rs = RadioSetting("limits.uhf.lower", "UHF Lower Limit (MHz)",
RadioSettingValueInteger(
lower, upper, _mem.limits.uhf.lower))
other.append(rs)
rs = RadioSetting("limits.uhf.upper", "UHF Upper Limit (MHz)",
RadioSettingValueInteger(
lower, upper, _mem.limits.uhf.upper))
other.append(rs)
# Work mode settings
rs = RadioSetting("settings.displayab", "Display",
RadioSettingValueList(
LIST_AB, current_index=_mem.settings.displayab))
work.append(rs)
rs = RadioSetting("settings.workmode", "VFO/MR Mode",
RadioSettingValueList(
LIST_WORKMODE,
current_index=_mem.settings.workmode))
work.append(rs)
rs = RadioSetting("settings.keylock", "Keypad Lock",
RadioSettingValueBoolean(_mem.settings.keylock))
work.append(rs)
rs = RadioSetting("wmchannel.mrcha", "MR A Channel",
RadioSettingValueInteger(0, 127,
_mem.wmchannel.mrcha))
work.append(rs)
rs = RadioSetting("wmchannel.mrchb", "MR B Channel",
RadioSettingValueInteger(0, 127,
_mem.wmchannel.mrchb))
work.append(rs)
def my_validate(value):
value = chirp_common.parse_freq(value)
msg = ("Can't be less than %i.0000")
if value > 99000000 and value < 130 * 1000000:
raise InvalidValueError(msg % (130))
msg = ("Can't be between %i.9975-%i.0000")
if (179 + 1) * 1000000 <= value and value < 400 * 1000000:
raise InvalidValueError(msg % (179, 400))
msg = ("Can't be greater than %i.9975")
if value > 99000000 and value > (520 + 1) * 1000000:
raise InvalidValueError(msg % (520))
return chirp_common.format_freq(value)
def apply_freq(setting, obj):
value = chirp_common.parse_freq(str(setting.value)) / 10
for i in range(7, -1, -1):
obj.freq[i] = value % 10
value /= 10
val1a = RadioSettingValueString(0, 10,
bfc.bcd_decode_freq(_mem.vfo.a.freq))
val1a.set_validate_callback(my_validate)
rs = RadioSetting("vfo.a.freq", "VFO A Frequency", val1a)
rs.set_apply_callback(apply_freq, _mem.vfo.a)
work.append(rs)
val1b = RadioSettingValueString(0, 10,
bfc.bcd_decode_freq(_mem.vfo.b.freq))
val1b.set_validate_callback(my_validate)
rs = RadioSetting("vfo.b.freq", "VFO B Frequency", val1b)
rs.set_apply_callback(apply_freq, _mem.vfo.b)
work.append(rs)
rs = RadioSetting("vfo.a.sftd", "VFO A Shift",
RadioSettingValueList(
LIST_SHIFTD, current_index=_mem.vfo.a.sftd))
work.append(rs)
rs = RadioSetting("vfo.b.sftd", "VFO B Shift",
RadioSettingValueList(
LIST_SHIFTD, current_index=_mem.vfo.b.sftd))
work.append(rs)
def convert_bytes_to_offset(bytes):
real_offset = 0
for byte in bytes:
real_offset = (real_offset * 10) + byte
return chirp_common.format_freq(real_offset * 1000)
def apply_offset(setting, obj):
value = chirp_common.parse_freq(str(setting.value)) / 1000
for i in range(5, -1, -1):
obj.offset[i] = value % 10
value /= 10
val1a = RadioSettingValueString(
0, 10, convert_bytes_to_offset(_mem.vfo.a.offset))
rs = RadioSetting("vfo.a.offset",
"VFO A Offset", val1a)
rs.set_apply_callback(apply_offset, _mem.vfo.a)
work.append(rs)
val1b = RadioSettingValueString(
0, 10, convert_bytes_to_offset(_mem.vfo.b.offset))
rs = RadioSetting("vfo.b.offset",
"VFO B Offset", val1b)
rs.set_apply_callback(apply_offset, _mem.vfo.b)
work.append(rs)
def apply_txpower_listvalue(setting, obj):
LOG.debug("Setting value: " + str(
setting.value) + " from list")
val = str(setting.value)
index = TXP_CHOICES.index(val)
val = TXP_VALUES[index]
obj.set_value(val)
if self._tri_band:
if _mem.vfo.a.txpower3 in TXP_VALUES:
idx = TXP_VALUES.index(_mem.vfo.a.txpower3)
else:
idx = TXP_VALUES.index(0x00)
rs = RadioSettingValueList(TXP_CHOICES, current_index=idx)
rset = RadioSetting("vfo.a.txpower3", "VFO A Power", rs)
rset.set_apply_callback(apply_txpower_listvalue,
_mem.vfo.a.txpower3)
work.append(rset)
if _mem.vfo.b.txpower3 in TXP_VALUES:
idx = TXP_VALUES.index(_mem.vfo.b.txpower3)
else:
idx = TXP_VALUES.index(0x00)
rs = RadioSettingValueList(TXP_CHOICES, current_index=idx)
rset = RadioSetting("vfo.b.txpower3", "VFO B Power", rs)
rset.set_apply_callback(apply_txpower_listvalue,
_mem.vfo.b.txpower3)
work.append(rset)
else:
rs = RadioSetting(
"vfo.a.txpower3", "VFO A Power",
RadioSettingValueList(
LIST_TXPOWER, current_index=min(
_mem.vfo.a.txpower3, 0x02)))
work.append(rs)
rs = RadioSetting(
"vfo.b.txpower3", "VFO B Power",
RadioSettingValueList(
LIST_TXPOWER, current_index=min(
_mem.vfo.b.txpower3, 0x02)))
work.append(rs)
rs = RadioSetting("vfo.a.widenarr", "VFO A Bandwidth",
RadioSettingValueList(
LIST_BANDWIDTH,
current_index=_mem.vfo.a.widenarr))
work.append(rs)
rs = RadioSetting("vfo.b.widenarr", "VFO B Bandwidth",
RadioSettingValueList(
LIST_BANDWIDTH,
current_index=_mem.vfo.b.widenarr))
work.append(rs)
rs = RadioSetting("vfo.a.scode", "VFO A S-CODE",
RadioSettingValueList(
LIST_SCODE,
current_index=_mem.vfo.a.scode))
work.append(rs)
rs = RadioSetting("vfo.b.scode", "VFO B S-CODE",
RadioSettingValueList(
LIST_SCODE,
current_index=_mem.vfo.b.scode))
work.append(rs)
rs = RadioSetting("vfo.a.step", "VFO A Tuning Step",
RadioSettingValueList(
LIST_STEP, current_index=_mem.vfo.a.step))
work.append(rs)
rs = RadioSetting("vfo.b.step", "VFO B Tuning Step",
RadioSettingValueList(
LIST_STEP, current_index=_mem.vfo.b.step))
work.append(rs)
# broadcast FM settings
value = self._memobj.fm_presets
value_shifted = ((value & 0x00FF) << 8) | ((value & 0xFF00) >> 8)
if value_shifted >= 65.0 * 10 and value_shifted <= 108.0 * 10:
# storage method 3 (discovered 2022)
self._bw_shift = True
preset = value_shifted / 10.0
elif value >= 65.0 * 10 and value <= 108.0 * 10:
# storage method 2
preset = value / 10.0
elif value <= 108.0 * 10 - 650:
# original storage method (2012)
preset = value / 10.0 + 65
else:
# unknown (undiscovered method or no FM chip?)
preset = False
if preset:
rs = RadioSettingValueFloat(65, 108.0, preset, 0.1, 1)
rset = RadioSetting("fm_presets", "FM Preset(MHz)", rs)
fm_preset.append(rset)
# DTMF settings
def apply_code(setting, obj, length):
code = []
for j in range(0, length):
try:
code.append(DTMF_CHARS.index(str(setting.value)[j]))
except IndexError:
code.append(0xFF)
obj.code = code
for i in range(0, 15):
_codeobj = self._memobj.pttid[i].code
_code = "".join([DTMF_CHARS[x] for x in _codeobj if int(x) < 0x1F])
val = RadioSettingValueString(0, 5, _code, False)
val.set_charset(DTMF_CHARS)
pttid = RadioSetting("pttid/%i.code" % i,
"Signal Code %i" % (i + 1), val)
pttid.set_apply_callback(apply_code, self._memobj.pttid[i], 5)
dtmfe.append(pttid)
if _mem.ani.dtmfon > 0xC3:
val = 0x03
else:
val = _mem.ani.dtmfon
rs = RadioSetting("ani.dtmfon", "DTMF Speed (on)",
RadioSettingValueList(LIST_DTMFSPEED,
current_index=val))
dtmfe.append(rs)
if _mem.ani.dtmfoff > 0xC3:
val = 0x03
else:
val = _mem.ani.dtmfoff
rs = RadioSetting("ani.dtmfoff", "DTMF Speed (off)",
RadioSettingValueList(LIST_DTMFSPEED,
current_index=val))
dtmfe.append(rs)
_codeobj = self._memobj.ani.code
_code = "".join([DTMF_CHARS[x] for x in _codeobj if int(x) < 0x1F])
val = RadioSettingValueString(0, 5, _code, False)
val.set_charset(DTMF_CHARS)
rs = RadioSetting("ani.code", "ANI Code", val)
rs.set_apply_callback(apply_code, self._memobj.ani, 5)
dtmfe.append(rs)
rs = RadioSetting("ani.aniid", "When to send ANI ID",
RadioSettingValueList(LIST_PTTID,
current_index=_mem.ani.aniid))
dtmfe.append(rs)
# Service settings
for band in ["vhf", "uhf"]:
for index in range(0, 10):
key = "squelch.%s.sql%i" % (band, index)
if band == "vhf":
_obj = self._memobj.squelch.vhf
elif band == "uhf":
_obj = self._memobj.squelch.uhf
val = RadioSettingValueInteger(0, 123,
getattr(
_obj, "sql%i" % (index)))
if index == 0:
val.set_mutable(False)
name = "%s Squelch %i" % (band.upper(), index)
rs = RadioSetting(key, name, val)
service.append(rs)
return top
@classmethod
def match_model(cls, filedata, filename):
match_size = False
match_model = False
# testing the file data size
if len(filedata) in [0x2008, 0x2010]:
match_size = True
# testing the firmware model fingerprint
match_model = model_match(cls, filedata)
if match_size and match_model:
return True
else:
return False
class RH5XAlias(chirp_common.Alias):
VENDOR = "Rugged"
MODEL = "RH5X"
class UV82IIIAlias(chirp_common.Alias):
VENDOR = "Baofeng"
MODEL = "UV-82III"
class UV9RPROAlias(chirp_common.Alias):
VENDOR = "Baofeng"
MODEL = "UV-9R Pro"
@directory.register
class BFA58(WP970I):
"""Baofeng BF-A58"""
VENDOR = "Baofeng"
MODEL = "BF-A58"
LENGTH_NAME = 7
ALIASES = [RH5XAlias, UV9RPROAlias]
_fileid = ["BFT515 ", "BFT517 "]
@directory.register
class UV82WP(WP970I):
"""Baofeng UV82-WP"""
VENDOR = "Baofeng"
MODEL = "UV-82WP"
@directory.register
class GT3WP(WP970I):
"""Baofeng GT-3WP"""
VENDOR = "Baofeng"
MODEL = "GT-3WP"
LENGTH_NAME = 7
@directory.register
class RT6(WP970I):
"""Retevis RT6"""
VENDOR = "Retevis"
MODEL = "RT6"
@directory.register
class BFA58S(WP970I):
VENDOR = "Baofeng"
MODEL = "BF-A58S"
LENGTH_NAME = 7
POWER_LEVELS = [chirp_common.PowerLevel("High", watts=5.00),
chirp_common.PowerLevel("Low", watts=1.00)]
ALIASES = [UV82IIIAlias]
_tri_band = True
def get_features(self):
rf = WP970I.get_features(self)
rf.valid_bands = [self._vhf_range,
self._vhf2_range,
self._uhf_range]
return rf
@directory.register
class UVS9X3(BFA58S):
VENDOR = "Baofeng"
MODEL = "UV-S9X3"
ALIASES = []
_magic = [MSTRING_UVS9X3, ]
@directory.register
class BF5RXRadio(BFA58S):
VENDOR = "Baofeng"
MODEL = "5RX"
ALIASES = []
_magic = [MSTRING_UVS9X3, ]
_air_range = (108000000, 136000000)
_vhf_range = (136000000, 174000000)
_vhf2_range = (200000000, 260000000)
_uhf2_range = (350000000, 390000000)
_uhf_range = (400000000, 520000000)
def get_features(self):
rf = WP970I.get_features(self)
rf.valid_bands = [self._air_range,
self._vhf_range,
self._vhf2_range,
self._uhf2_range,
self._uhf_range]
return rf
@directory.register
class UV9R(WP970I):
"""Baofeng UV-9R"""
VENDOR = "Baofeng"
MODEL = "UV-9R"
LENGTH_NAME = 7
@directory.register
class UV9G(WP970I):
"""Baofeng UV-9G"""
VENDOR = "Baofeng"
MODEL = "UV-9G"
LENGTH_NAME = 7
POWER_LEVELS = [chirp_common.PowerLevel("High", watts=5.00),
chirp_common.PowerLevel("Med", watts=1.00),
chirp_common.PowerLevel("Low", watts=0.50)]
_magic = [MSTRING_UV9G, ]
_gmrs = False # sold as GMRS radio but supports full band TX/RX
@classmethod
def match_model(cls, filedata, filename):
# This radio has always been post-metadata, so never do
# old-school detection
return False
chirp-20261030/chirp/drivers/bf_t1.py 0000644 0001750 0001750 00000072463 15172631605 016204 0 ustar hibby hibby # Copyright 2017 Pavel Milanes, CO7WT,
#
# This driver is a community effort as I don't have the radio on my hands, so
# I was only the director of the orchestra, without the players this may never
# came true, so special thanks to the following hams for their contribution:
# - Henk van der Laan, PA3CQN
# - Setting Discovery.
# - Special channels for RELAY and EMERGENCY.
# - Harold Hankins
# - Memory limits, testing & bug hunting.
# - Dmitry Milkov
# - Testing & bug hunting.
# - Many others participants in the issue page on Chirp's site.
#
# 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 .
from chirp import chirp_common, directory, memmap
from chirp import bitwise, errors, util
from chirp.settings import RadioSetting, RadioSettingGroup, \
RadioSettingValueBoolean, RadioSettingValueList, \
RadioSettingValueInteger, RadioSettingValueFloat, \
RadioSettings
import struct
import logging
LOG = logging.getLogger(__name__)
# A note about the memory in these radios
#
# The '9100' OEM software only manipulates the lower 0x0180 bytes on read/write
# operations as we know, the file generated by the OEM software IS NOT an exact
# eeprom image, it's a crude text file with a pseudo csv format
#
# Later investigations by Harold Hankins found that the eeprom extend up to 2k
# consistent with a hardware chip K24C16 a 2k x 8 bit serial eeprom
MEM_SIZE = 0x0800 # 2048 bytes
WRITE_SIZE = 0x0180 # 384 bytes
BLOCK_SIZE = 0x10
ACK_CMD = b"\x06"
MODES = ["NFM", "FM"]
SKIP_VALUES = ["S", ""]
TONES = chirp_common.TONES
DTCS = tuple(sorted(chirp_common.DTCS_CODES + (645,)))
# Special channels
SPECIALS = {
"EMG": -2,
"RLY": -1
}
# Settings vars
TOT_LIST = ["Off"] + ["%s" % x for x in range(30, 210, 30)]
SCAN_TYPE_LIST = ["Time", "Carrier", "Search"]
LANGUAGE_LIST = ["Off", "English", "Chinese"]
TIMER_LIST = ["Off"] + ["%s h" % (x * 0.5) for x in range(1, 17)]
FM_RANGE_LIST = ["76-108", "65-76"]
RELAY_MODE_LIST = ["Off", "RX sync", "TX sync"]
BACKLIGHT_LIST = ["Off", "Key", "On"]
POWER_LIST = ["0.5 Watt", "1.0 Watt"]
# This is a general serial timeout for all serial read functions.
# Practice has show that about 0.07 sec will be enough to cover all radios.
STIMEOUT = 0.07
# this var controls the verbosity in the debug and by default it's low (False)
# make it True and you will to get a very verbose debug.log
debug = False
# #### ID strings #####################################################
# BF-T1 handheld
BFT1_magic = b"\x05PROGRAM"
BFT1_ident = b" BF9100S"
def _clean_buffer(radio):
"""Cleaning the read serial buffer, hard timeout to survive an infinite
data stream"""
dump = "1"
datacount = 0
try:
while len(dump) > 0:
dump = radio.pipe.read(100)
datacount += len(dump)
# hard limit to survive a infinite serial data stream
# 5 times bigger than a normal rx block (20 bytes)
if datacount > 101:
seriale = "Please check your serial port selection."
raise errors.RadioError(seriale)
except Exception:
raise errors.RadioError("Unknown error cleaning the serial buffer")
def _rawrecv(radio, amount=0):
"""Raw read from the radio device"""
# var to hold the data to return
data = b""
try:
if amount == 0:
data = radio.pipe.read()
else:
data = radio.pipe.read(amount)
# DEBUG
if debug is True:
LOG.debug("<== (%d) bytes:\n\n%s" %
(len(data), util.hexprint(data)))
# fail if no data is received
if len(data) == 0:
raise errors.RadioNoResponse()
except errors.RadioError:
raise
except Exception:
raise errors.RadioError("Error reading data from radio")
return data
def _send(radio, data):
"""Send data to the radio device"""
try:
radio.pipe.write(data)
# DEBUG
if debug is True:
LOG.debug("==> (%d) bytes:\n\n%s" %
(len(data), util.hexprint(data)))
except:
raise errors.RadioError("Error sending data to radio")
def _make_frame(cmd, addr, data=""):
"""Pack the info in the header format"""
frame = struct.pack(">BHB", ord(cmd), addr, BLOCK_SIZE)
# add the data if set
if len(data) != 0:
frame += data
return frame
def _recv(radio, addr):
"""Get data from the radio"""
# Get the full 20 bytes at a time
# 4 bytes header + 16 bytes of data (BLOCK_SIZE)
# get the whole block
block = _rawrecv(radio, BLOCK_SIZE + 4)
# short answer
if len(block) < (BLOCK_SIZE + 4):
raise errors.RadioError("Wrong block length (short) at 0x%04x" % addr)
# long answer
if len(block) > (BLOCK_SIZE + 4):
raise errors.RadioError("Wrong block length (long) at 0x%04x" % addr)
# header validation
c, a, l = struct.unpack(">cHB", block[0:4])
if c != b"W" or a != addr or l != BLOCK_SIZE:
LOG.debug("Invalid header for block 0x%04x:" % addr)
LOG.debug("CMD: %s ADDR: %04x SIZE: %02x" % (c, a, l))
raise errors.RadioError("Invalid header for block 0x%04x:" % addr)
# return the data, 16 bytes of payload
return block[4:]
def _start_clone_mode(radio, status):
"""Put the radio in clone mode, 3 tries"""
# cleaning the serial buffer
_clean_buffer(radio)
# prep the data to show in the UI
status.cur = 0
status.msg = "Identifying the radio..."
status.max = 3
radio.status_fn(status)
try:
for a in range(0, status.max):
# Update the UI
status.cur = a + 1
radio.status_fn(status)
# send the magic word
_send(radio, radio._magic)
# Now you get a x06 of ACK if all goes well
ack = _rawrecv(radio, 1)
if ack == ACK_CMD:
# DEBUG
LOG.info("Magic ACK received")
status.cur = status.max
radio.status_fn(status)
return True
return False
except errors.RadioError:
raise
except Exception as e:
raise errors.RadioError("Error sending Magic to radio:\n%s" % e)
def _do_ident(radio, status):
"""Put the radio in PROGRAM mode & identify it"""
# set the serial discipline (default)
radio.pipe.baudrate = 9600
radio.pipe.parity = "N"
radio.pipe.bytesize = 8
radio.pipe.stopbits = 1
radio.pipe.timeout = STIMEOUT
# open the radio into program mode
if _start_clone_mode(radio, status) is False:
raise errors.RadioError("Radio did not enter clone mode, wrong model?")
# Ok, poke it to get the ident string
_send(radio, b"\x02")
ident = _rawrecv(radio, len(radio._id))
# basic check for the ident
if len(ident) != len(radio._id):
raise errors.RadioError("Radio send a odd identification block.")
# check if ident is OK
if ident != radio._id:
LOG.debug("Incorrect model ID, got this:\n\n" + util.hexprint(ident))
raise errors.RadioError("Radio identification failed.")
# handshake
_send(radio, ACK_CMD)
ack = _rawrecv(radio, 1)
# checking handshake
if len(ack) == 1 and ack == ACK_CMD:
# DEBUG
LOG.info("ID ACK received")
else:
LOG.debug("Radio handshake failed.")
raise errors.RadioError("Radio handshake failed.")
# DEBUG
LOG.info("Positive ident, this is a %s %s" % (radio.VENDOR, radio.MODEL))
return True
def _download(radio):
"""Get the memory map"""
# UI progress
status = chirp_common.Status()
# put radio in program mode and identify it
_do_ident(radio, status)
# reset the progress bar in the UI
status.max = MEM_SIZE // BLOCK_SIZE
status.msg = "Cloning from radio..."
status.cur = 0
radio.status_fn(status)
# cleaning the serial buffer
_clean_buffer(radio)
data = b""
for addr in range(0, MEM_SIZE, BLOCK_SIZE):
# sending the read request
_send(radio, _make_frame(b"R", addr))
# read
d = _recv(radio, addr)
# aggregate the data
data += d
# UI Update
status.cur = addr // BLOCK_SIZE
status.msg = "Cloning from radio..."
radio.status_fn(status)
# close comms with the radio
_send(radio, b"\x62")
# DEBUG
LOG.info("Close comms cmd sent, radio must reboot now.")
return data
def _upload(radio):
"""Upload procedure, we only upload to the radio the Writable space"""
# UI progress
status = chirp_common.Status()
# put radio in program mode and identify it
_do_ident(radio, status)
# get the data to upload to radio
data = radio.get_mmap()
# Reset the UI progress
status.max = WRITE_SIZE // BLOCK_SIZE
status.cur = 0
status.msg = "Cloning to radio..."
radio.status_fn(status)
# cleaning the serial buffer
_clean_buffer(radio)
# the fun start here, we use WRITE_SIZE instead of the full MEM_SIZE
for addr in range(0, WRITE_SIZE, BLOCK_SIZE):
# getting the block of data to send
d = data[addr:addr + BLOCK_SIZE]
# build the frame to send
frame = _make_frame(b"W", addr, d)
# send the frame
_send(radio, frame)
# receiving the response
ack = _rawrecv(radio, 1)
# basic check
if len(ack) != 1:
raise errors.RadioError("No ACK when writing block 0x%04x" % addr)
if ack != ACK_CMD:
raise errors.RadioError("Bad ACK writing block 0x%04x:" % addr)
# UI Update
status.cur = addr // BLOCK_SIZE
status.msg = "Cloning to radio..."
radio.status_fn(status)
# close comms with the radio
_send(radio, b"\x62")
# DEBUG
LOG.info("Close comms cmd sent, radio must reboot now.")
def _model_match(cls, data):
"""Match the opened/downloaded image to the correct version"""
# a reliable fingerprint: the model name at
rid = data[0x06f8:0x0700]
if rid == BFT1_ident:
return True
return False
def _decode_ranges(low, high):
"""Unpack the data in the ranges zones in the memmap and return
a tuple with the integer corresponding to the MHz it means"""
return (int(low) * 100000, int(high) * 100000)
MEM_FORMAT = """
struct channel {
lbcd rxfreq[4]; // rx freq.
u8 rxtone; // x00 = none
// x01 - x32 = index of the analog tones
// x33 - x9b = index of Digital tones
// Digital tone polarity is handled below by
// ttondinv & ttondinv settings
lbcd txoffset[4]; // the difference against RX, direction handled by
// offplus & offminus
u8 txtone; // Idem to rxtone
u8 noskip:1, // if true is included in the scan
wide:1, // 1 = Wide, 0 = Narrow
ttondinv:1, // if true TX tone is Digital & Inverted
unA:1, //
rtondinv:1, // if true RX tone is Digital & Inverted
unB:1, //
offplus:1, // TX = RX + offset
offminus:1; // TX = RX - offset
u8 empty[5];
};
#seekto 0x0000;
struct channel emg; // channel 0 is Emergent CH
#seekto 0x0010;
struct channel channels[20]; // normal 1-20 mem channels
#seekto 0x0150; // Settings
struct {
lbcd vhfl[2]; // VHF low limit
lbcd vhfh[2]; // VHF high limit
lbcd uhfl[2]; // UHF low limit
lbcd uhfh[2]; // UHF high limit
u8 unk0[8];
u8 unk1[2]; // start of 0x0160 <=======
u8 squelch; // byte: 0-9
u8 vox; // byte: 0-9
u8 timeout; // tot, 0 off, then 30 sec increments up to 180
u8 batsave:1, // battery save 0 = off, 1 = on
fm_funct:1, // fm-radio 0=off, 1=on ( off disables fm button on set )
ste:1, // squelch tail 0 = off, 1 = on
blo:1, // busy lockout 0 = off, 1 = on
beep:1, // key beep 0 = off, 1 = on
lock:1, // keylock 0 = ff, = on
backlight:2; // backlight 00 = off, 01 = key, 10 = on
u8 scantype; // scan type 0 = timed, 1 = carrier, 2 = stop
u8 channel; // active channel 1-20, setting it works on upload
u8 fmrange; // fm range 1 = low[65-76](ASIA), 0 = high[76-108](AMERICA)
u8 alarm; // alarm (count down timer)
// d0 - d16 in half hour increments => off, 0.5 - 8.0 h
u8 voice; // voice prompt 0 = off, 1 = English, 2 = Chinese
u8 volume; // volume 1-7 as per the radio steps
// set to #FF by original software on upload
// chirp uploads actual value and works.
u16 fm_vfo; // the frequency of the fm receiver.
// resulting frequency is 65 + value * 0.1 MHz
// 0x145 is then 65 + 325*0.1 = 97.5 MHz
u8 relaym; // relay mode, d0 = off, d2 = re-tx, d1 = re-rx
// still a mystery on how it works
u8 tx_pwr; // tx pwr 0 = low (0.5W), 1 = high(1.0W)
} settings;
#seekto 0x0170; // Relay CH
struct channel rly;
"""
@directory.register
class BFT1(chirp_common.CloneModeRadio, chirp_common.ExperimentalRadio):
"""Baofeng BT-F1 radio & possibly alike radios"""
VENDOR = "Baofeng"
MODEL = "BF-T1"
_vhf_range = (130000000, 174000000)
_uhf_range = (400000000, 520000000)
_upper = 20
_magic = BFT1_magic
_id = BFT1_ident
_bw_shift = False
@classmethod
def get_prompts(cls):
rp = chirp_common.RadioPrompts()
rp.experimental = \
('This driver is experimental.\n'
'\n'
'Please keep a copy of your memories with the original software '
'if you treasure them, this driver is new and may contain'
' bugs.\n'
'\n'
'"Emergent CH" & "Relay CH" are implemented via special channels,'
'be sure to click on the button on the interface to access them.'
)
rp.pre_download = _(
"Follow these instructions to download your info:\n"
"1 - Turn off your radio\n"
"2 - Connect your interface cable\n"
"3 - Turn on your radio\n"
"4 - Do the download of your radio data\n")
rp.pre_upload = _(
"Follow these instructions to upload your info:\n"
"1 - Turn off your radio\n"
"2 - Connect your interface cable\n"
"3 - Turn on your radio\n"
"4 - Do the upload of your radio data\n")
return rp
def get_features(self):
"""Get the radio's features"""
rf = chirp_common.RadioFeatures()
rf.valid_special_chans = list(SPECIALS.keys())
rf.has_settings = True
rf.has_bank = False
rf.has_tuning_step = False
rf.can_odd_split = True
rf.has_name = False
rf.has_offset = True
rf.has_mode = True
rf.valid_modes = MODES
rf.has_dtcs = True
rf.has_rx_dtcs = True
rf.has_dtcs_polarity = True
rf.has_ctone = True
rf.has_cross = True
rf.valid_duplexes = ["", "-", "+", "split"]
rf.valid_tmodes = ['', 'Tone', 'TSQL', 'DTCS', 'Cross']
rf.valid_cross_modes = [
"Tone->Tone",
"DTCS->",
"->DTCS",
"Tone->DTCS",
"DTCS->Tone",
"->Tone",
"DTCS->DTCS"]
rf.valid_skips = SKIP_VALUES
rf.valid_dtcs_codes = DTCS
rf.memory_bounds = (1, self._upper)
rf.valid_tuning_steps = [2.5, 5., 6.25, 10., 12.5, 25.]
# normal dual bands
rf.valid_bands = [self._vhf_range, self._uhf_range]
return rf
def process_mmap(self):
"""Process the mem map into the mem object"""
# Get it
self._memobj = bitwise.parse(MEM_FORMAT, self._mmap)
# set the band limits as the memmap
settings = self._memobj.settings
self._vhf_range = _decode_ranges(settings.vhfl, settings.vhfh)
self._uhf_range = _decode_ranges(settings.uhfl, settings.uhfh)
def sync_in(self):
"""Download from radio"""
data = _download(self)
self._mmap = memmap.MemoryMapBytes(data)
self.process_mmap()
def sync_out(self):
"""Upload to radio"""
try:
_upload(self)
except errors.RadioError:
raise
except Exception as e:
raise errors.RadioError("Error: %s" % e)
def _decode_tone(self, val, inv):
"""Parse the tone data to decode from mem, it returns:
Mode (''|DTCS|Tone), Value (None|###), Polarity (None,N,R)"""
if val == 0:
return '', None, None
elif val < 51: # analog tone
return 'Tone', TONES[val - 1], None
elif val > 50: # digital tone
pol = "N"
# polarity?
if inv == 1:
pol = "R"
return 'DTCS', DTCS[val - 51], pol
def _encode_tone(self, memtone, meminv, mode, tone, pol):
"""Parse the tone data to encode from UI to mem"""
if mode == '' or mode is None:
memtone.set_value(0)
meminv.set_value(0)
elif mode == 'Tone':
# caching errors for analog tones.
try:
memtone.set_value(TONES.index(tone) + 1)
meminv.set_value(0)
except:
msg = "TCSS Tone '%d' is not supported" % tone
LOG.error(msg)
raise errors.RadioError(msg)
elif mode == 'DTCS':
# caching errors for digital tones.
try:
memtone.set_value(DTCS.index(tone) + 51)
if pol == "R":
meminv.set_value(True)
else:
meminv.set_value(False)
except:
msg = "Digital Tone '%d' is not supported" % tone
LOG.error(msg)
raise errors.RadioError(msg)
else:
msg = "Internal error: invalid mode '%s'" % mode
LOG.error(msg)
raise errors.InvalidDataError(msg)
def get_raw_memory(self, number):
return repr(self._memobj.memory[number])
def _get_special(self, number):
if isinstance(number, str):
return (getattr(self._memobj, number.lower()))
elif number < 0:
for k, v in SPECIALS.items():
if number == v:
return (getattr(self._memobj, k.lower()))
else:
return self._memobj.channels[number-1]
def get_memory(self, number):
"""Get the mem representation from the radio image"""
_mem = self._get_special(number)
# Create a high-level memory object to return to the UI
mem = chirp_common.Memory()
# Check if special or normal
if isinstance(number, str):
mem.number = SPECIALS[number]
mem.extd_number = number
else:
mem.number = number
if _mem.get_raw(asbytes=False)[0] == "\xFF":
mem.empty = True
return mem
# Freq and offset
mem.freq = int(_mem.rxfreq) * 10
# TX freq (Stored as a difference)
mem.offset = int(_mem.txoffset) * 10
mem.duplex = ""
# must work out the polarity
if mem.offset != 0:
if _mem.offminus == 1:
mem.duplex = "-"
# tx below RX
if _mem.offplus == 1:
# tx above RX
mem.duplex = "+"
# split RX/TX in different bands
if mem.offset > 71000000:
mem.duplex = "split"
# show the actual value in the offset, depending on the shift
if _mem.offminus == 1:
mem.offset = mem.freq - mem.offset
if _mem.offplus == 1:
mem.offset = mem.freq + mem.offset
# wide/narrow
mem.mode = MODES[int(_mem.wide)]
# skip
mem.skip = SKIP_VALUES[_mem.noskip]
# tone data
rxtone = txtone = None
txtone = self._decode_tone(_mem.txtone, _mem.ttondinv)
rxtone = self._decode_tone(_mem.rxtone, _mem.rtondinv)
chirp_common.split_tone_decode(mem, txtone, rxtone)
return mem
def set_memory(self, mem):
"""Set the memory data in the eeprom img from the UI"""
# get the eprom representation of this channel
_mem = self._get_special(mem.number)
# if empty memory
if mem.empty:
# the channel itself
_mem.set_raw("\xFF" * 16)
# return it
return mem
# frequency
_mem.rxfreq = mem.freq / 10
# duplex/ offset Offset is an absolute value
_mem.txoffset = mem.offset / 10
# must work out the polarity
if mem.duplex == "":
_mem.offplus = 0
_mem.offminus = 0
elif mem.duplex == "+":
_mem.offplus = 1
_mem.offminus = 0
elif mem.duplex == "-":
_mem.offplus = 0
_mem.offminus = 1
elif mem.duplex == "split":
if mem.freq > mem.offset:
_mem.offplus = 0
_mem.offminus = 1
_mem.txoffset = (mem.freq - mem.offset) / 10
else:
_mem.offplus = 1
_mem.offminus = 0
_mem.txoffset = (mem.offset - mem.freq) / 10
# wide/narrow
_mem.wide = MODES.index(mem.mode)
# skip
_mem.noskip = SKIP_VALUES.index(mem.skip)
# tone data
((txmode, txtone, txpol), (rxmode, rxtone, rxpol)) = \
chirp_common.split_tone_encode(mem)
self._encode_tone(_mem.txtone, _mem.ttondinv, txmode, txtone, txpol)
self._encode_tone(_mem.rxtone, _mem.rtondinv, rxmode, rxtone, rxpol)
return mem
def get_settings(self):
_settings = self._memobj.settings
basic = RadioSettingGroup("basic", "Basic Settings")
fm = RadioSettingGroup("fm", "FM Radio")
adv = RadioSettingGroup("adv", "Advanced Settings")
group = RadioSettings(basic, fm, adv)
# ## Basic Settings
rs = RadioSetting("tx_pwr", "TX Power",
RadioSettingValueList(
POWER_LIST, current_index=_settings.tx_pwr))
basic.append(rs)
rs = RadioSetting("channel", "Active Channel",
RadioSettingValueInteger(1, 20, _settings.channel))
basic.append(rs)
rs = RadioSetting("squelch", "Squelch Level",
RadioSettingValueInteger(0, 9, _settings.squelch))
basic.append(rs)
rs = RadioSetting("vox", "VOX Level",
RadioSettingValueInteger(0, 9, _settings.vox))
basic.append(rs)
# volume validation, as the OEM software set 0xFF on write
_volume = _settings.volume
if _volume > 7:
_volume = 7
rs = RadioSetting("volume", "Volume Level",
RadioSettingValueInteger(0, 7, _volume))
basic.append(rs)
rs = RadioSetting("scantype", "Scan Type",
RadioSettingValueList(SCAN_TYPE_LIST, current_index=_settings.scantype))
basic.append(rs)
rs = RadioSetting("timeout", "Time Out Timer (seconds)",
RadioSettingValueList(
TOT_LIST, current_index=_settings.timeout))
basic.append(rs)
rs = RadioSetting("voice", "Voice Prompt",
RadioSettingValueList(
LANGUAGE_LIST, current_index=_settings.voice))
basic.append(rs)
rs = RadioSetting("alarm", "Alarm Time",
RadioSettingValueList(
TIMER_LIST, current_index=_settings.alarm))
basic.append(rs)
rs = RadioSetting("backlight", "Backlight",
RadioSettingValueList(
BACKLIGHT_LIST,
current_index=_settings.backlight))
basic.append(rs)
rs = RadioSetting("blo", "Busy Lockout",
RadioSettingValueBoolean(_settings.blo))
basic.append(rs)
rs = RadioSetting("ste", "Squelch Tail Eliminate",
RadioSettingValueBoolean(_settings.ste))
basic.append(rs)
rs = RadioSetting("batsave", "Battery Save",
RadioSettingValueBoolean(_settings.batsave))
basic.append(rs)
rs = RadioSetting("lock", "Key Lock",
RadioSettingValueBoolean(_settings.lock))
basic.append(rs)
rs = RadioSetting("beep", "Key Beep",
RadioSettingValueBoolean(_settings.beep))
basic.append(rs)
# ## FM Settings
rs = RadioSetting("fm_funct", "FM Function",
RadioSettingValueBoolean(_settings.fm_funct))
fm.append(rs)
rs = RadioSetting("fmrange", "FM Range",
RadioSettingValueList(
FM_RANGE_LIST, current_index=_settings.fmrange))
fm.append(rs)
# callbacks for the FM VFO
def apply_fm_freq(setting, obj):
value = int(setting.value.get_value() * 10) - 650
LOG.debug("Setting fm_vfo = %s" % (value))
if self._bw_shift:
value = ((value & 0x00FF) << 8) | ((value & 0xFF00) >> 8)
setattr(obj, setting.get_name(), value)
# broadcast FM setting
value = _settings.fm_vfo
value_shifted = ((value & 0x00FF) << 8) | ((value & 0xFF00) >> 8)
if value_shifted <= 108.0 * 10 - 650:
# storage method 3 (discovered 2022)
self._bw_shift = True
_fm_vfo = value_shifted / 10.0 + 65
elif value <= 108.0 * 10 - 650:
# original storage method (2012)
_fm_vfo = value / 10.0 + 65
else:
# unknown (undiscovered method or no FM chip?)
_fm_vfo = False
if _fm_vfo:
rs = RadioSetting("fm_vfo", "FM Station",
RadioSettingValueFloat(65, 108, _fm_vfo))
rs.set_apply_callback(apply_fm_freq, _settings)
fm.append(rs)
# ## Advanced
def apply_limit(setting, obj):
setattr(obj, setting.get_name(), int(setting.value) * 10)
rs = RadioSetting("vhfl", "VHF Low Limit",
RadioSettingValueInteger(130, 174, int(
_settings.vhfl) / 10))
rs.set_apply_callback(apply_limit, _settings)
adv.append(rs)
rs = RadioSetting("vhfh", "VHF High Limit",
RadioSettingValueInteger(130, 174, int(
_settings.vhfh) / 10))
rs.set_apply_callback(apply_limit, _settings)
adv.append(rs)
rs = RadioSetting("uhfl", "UHF Low Limit",
RadioSettingValueInteger(400, 520, int(
_settings.uhfl) / 10))
rs.set_apply_callback(apply_limit, _settings)
adv.append(rs)
rs = RadioSetting("uhfh", "UHF High Limit",
RadioSettingValueInteger(400, 520, int(
_settings.uhfh) / 10))
rs.set_apply_callback(apply_limit, _settings)
adv.append(rs)
rs = RadioSetting("relaym", "Relay Mode",
RadioSettingValueList(RELAY_MODE_LIST,
current_index=_settings.relaym))
adv.append(rs)
return group
def set_settings(self, uisettings):
_settings = self._memobj.settings
for element in uisettings:
if not isinstance(element, RadioSetting):
self.set_settings(element)
continue
if not element.changed():
continue
try:
name = element.get_name()
value = element.value
if element.has_apply_callback():
LOG.debug("Using apply callback")
element.run_apply_callback()
else:
obj = getattr(_settings, name)
setattr(_settings, name, value)
LOG.debug("Setting %s: %s" % (name, value))
except Exception:
LOG.debug(element.get_name())
raise
@classmethod
def match_model(cls, filedata, filename):
match_size = False
match_model = False
# testing the file data size
if len(filedata) == MEM_SIZE:
match_size = True
# DEBUG
if debug is True:
LOG.debug("BF-T1 matched!")
# testing the firmware model fingerprint
match_model = _model_match(cls, filedata)
return match_size and match_model
chirp-20261030/chirp/drivers/bf_t8.py 0000644 0001750 0001750 00000075402 15172631605 016207 0 ustar hibby hibby # Copyright 2022 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 .
import struct
import logging
from chirp import (
bitwise,
chirp_common,
directory,
errors,
memmap,
util,
)
from chirp.settings import (
RadioSetting,
RadioSettingGroup,
RadioSettings,
RadioSettingValueBoolean,
RadioSettingValueFloat,
RadioSettingValueInteger,
RadioSettingValueList,
)
LOG = logging.getLogger(__name__)
MEM_FORMAT = """
// #seekto 0x0000;
struct {
lbcd rxfreq[4]; // RX Frequency
lbcd txfreq[4]; // TX Frequency
u8 rx_tmode; // RX Tone Mode
u8 rx_tone; // PL/DPL Decode
u8 tx_tmode; // TX Tone Mode
u8 tx_tone; // PL/DPL Encode
u8 unknown1:3, //
skip:1, // Scan Add: 1 = Skip, 0 = Scan
unknown2:2,
isnarrow:1, // W/N: 1 = Narrow, 0 = Wide
lowpower:1; // TX Power: 1 = Low, 0 = High
u8 unknown3[3]; //
} memory[%d];
// #seekto 0x0630;
struct {
u8 squelch; // SQL
u8 vox; // Vox Lv
u8 tot; // TOT
u8 unk1:3, //
ste:1, // Tail Clear
bcl:1, // BCL
save:1, // Save
tdr:1, // TDR
beep:1; // Beep
u8 voice; // Voice
u8 abr; // Back Light
u8 ring; // Ring
u8 mdf; // Display Type
u8 mra; // MR Channel A
u8 mrb; // MR Channel B
u8 disp_ab; // Display A/B Selected
} settings;
"""
MEM_FORMAT_PT2 = """
// #seekto 0x063D;
struct {
u8 workmode; // Work Mode
u8 wx; // NOAA WX ch#
u8 area; // Area Selected
} settings2;
#seekto 0x0D00;
struct {
char name[6];
u8 unknown1[2];
} names[%d];
"""
MEM_FM_U_FORMAT = """
// #seekto 0x063B;
struct {
u16 fmcur; // Broadcast FM station
} fmpreset;
"""
MEM_FM_UL_FORMAT = """
// #seekto 0x063B;
struct {
ul16 fmcur; // Broadcast FM station
} fmpreset;
"""
CMD_ACK = b"\x06"
TONES = chirp_common.TONES
TMODES = ["", "Tone", "DTCS", "DTCS"]
AB_LIST = ["A", "B"]
ABR_LIST = ["OFF", "ON", "Key"]
AREA_LIST = ["China", "Japan", "Korea", "Malaysia", "American",
"Australia", "Iran", "Taiwan", "Europe", "Russia"]
MDF_LIST = ["Frequency", "Channel #", "Name"]
RING_LIST = ["OFF"] + ["%s" % x for x in range(1, 11)]
TOT_LIST = ["OFF"] + ["%s seconds" % x for x in range(30, 210, 30)]
TOT2_LIST = ["OFF"] + ["%s seconds" % x for x in range(60, 240, 30)]
VOICE_LIST = ["Off", "Chinese", "English"]
VOX_LIST = ["OFF"] + ["%s" % x for x in range(1, 6)]
WORKMODE_LIST = ["General", "PMR"]
WX_LIST = ["CH01 - 162.550",
"CH02 - 162.400",
"CH03 - 162.475",
"CH04 - 162.425",
"CH05 - 162.450",
"CH06 - 162.500",
"CH07 - 162.525",
"CH08 - 161.650",
"CH09 - 161.775",
"CH10 - 161.750",
"CH11 - 162.000"
]
FRS_FREQS1 = [462562500, 462587500, 462612500, 462637500, 462662500,
462687500, 462712500]
FRS_FREQS2 = [467562500, 467587500, 467612500, 467637500, 467662500,
467687500, 467712500]
FRS_FREQS3 = [462550000, 462575000, 462600000, 462625000, 462650000,
462675000, 462700000, 462725000]
FRS_FREQS = FRS_FREQS1 + FRS_FREQS2 + FRS_FREQS3
GMRS_FREQS = FRS_FREQS + FRS_FREQS3
MURS_FREQS = [151820000, 151880000, 151940000, 154570000, 154600000]
PMR_FREQS1 = [446006250, 446018750, 446031250, 446043750, 446056250,
446068750, 446081250, 446093750]
PMR_FREQS2 = [446106250, 446118750, 446131250, 446143750, 446156250,
446168750, 446181250, 446193750]
PMR_FREQS = PMR_FREQS1 + PMR_FREQS2
VOICE_CHOICES = ["Off", "On"]
VOICE_VALUES = [0x00, 0x02]
def _enter_programming_mode(radio):
serial = radio.pipe
exito = False
for i in range(0, 5):
serial.write(radio._magic)
ack = serial.read(1)
try:
if ack == CMD_ACK:
exito = True
break
except:
LOG.debug("Attempt #%s, failed, trying again" % i)
pass
# check if we had EXITO
if exito is False:
_exit_programming_mode(radio)
if not ack:
raise errors.RadioNoResponse()
msg = "The radio did not accept program mode after five tries.\n"
msg += "Check you interface cable and power cycle your radio."
raise errors.RadioError(msg)
try:
serial.write(b"\x02")
ident = serial.read(len(radio._fingerprint))
except:
_exit_programming_mode(radio)
raise errors.RadioError("Error communicating with radio")
if not ident == radio._fingerprint:
_exit_programming_mode(radio)
LOG.debug(util.hexprint(ident))
raise errors.RadioError("Radio returned unknown identification string")
try:
serial.write(CMD_ACK)
ack = serial.read(1)
except:
_exit_programming_mode(radio)
raise errors.RadioError("Error communicating with radio")
if ack != CMD_ACK:
_exit_programming_mode(radio)
raise errors.RadioError("Radio refused to enter programming mode")
def _exit_programming_mode(radio):
serial = radio.pipe
try:
serial.write(b"E")
except:
raise errors.RadioError("Radio refused to exit programming mode")
def _read_block(radio, block_addr, block_size):
serial = radio.pipe
cmd = struct.pack(">cHb", b'R', block_addr, block_size)
expectedresponse = b"W" + cmd[1:]
LOG.debug("Reading block %04x..." % (block_addr))
try:
serial.write(cmd)
response = serial.read(4 + block_size)
if not response:
raise errors.RadioNoResponse()
if response[:4] != expectedresponse:
raise Exception("Error reading block %04x." % (block_addr))
block_data = response[4:]
serial.write(CMD_ACK)
ack = serial.read(1)
except:
_exit_programming_mode(radio)
raise errors.RadioError("Failed to read block at %04x" % block_addr)
if ack != CMD_ACK:
_exit_programming_mode(radio)
raise Exception("No ACK reading block %04x." % (block_addr))
return block_data
def _write_block(radio, block_addr, block_size):
serial = radio.pipe
cmd = struct.pack(">cHb", b'W', block_addr, block_size)
data = radio.get_mmap()[block_addr:block_addr + block_size]
LOG.debug("Writing Data:")
LOG.debug(util.hexprint(cmd + data))
try:
serial.write(cmd + data)
if serial.read(1) != CMD_ACK:
raise Exception("No ACK")
except:
_exit_programming_mode(radio)
raise errors.RadioError("Failed to send block "
"to radio at %04x" % block_addr)
def do_download(radio):
LOG.debug("download")
_enter_programming_mode(radio)
data = b""
status = chirp_common.Status()
status.msg = "Cloning from radio"
status.cur = 0
status.max = radio._memsize
for addr in range(0, radio._memsize, radio.BLOCK_SIZE):
status.cur = addr + radio.BLOCK_SIZE
radio.status_fn(status)
block = _read_block(radio, addr, radio.BLOCK_SIZE)
data += block
LOG.debug("Address: %04x" % addr)
LOG.debug(util.hexprint(block))
_exit_programming_mode(radio)
return memmap.MemoryMapBytes(data)
def do_upload(radio):
status = chirp_common.Status()
status.msg = "Uploading to radio"
_enter_programming_mode(radio)
status.cur = 0
status.max = radio._memsize
for start_addr, end_addr in radio._ranges:
for addr in range(start_addr, end_addr, radio.BLOCK_SIZE_UP):
status.cur = addr + radio.BLOCK_SIZE_UP
radio.status_fn(status)
_write_block(radio, addr, radio.BLOCK_SIZE_UP)
_exit_programming_mode(radio)
class BFT8Radio(chirp_common.CloneModeRadio):
"""Baofeng BF-T8"""
VENDOR = "Baofeng"
MODEL = "BF-T8"
BAUD_RATE = 9600
BLOCK_SIZE = BLOCK_SIZE_UP = 0x10
ODD_SPLIT = True
HAS_NAMES = False
NAME_LENGTH = 0
VALID_CHARS = ""
CH_OFFSET = False
SKIP_VALUES = []
DTCS_CODES = sorted(chirp_common.DTCS_CODES)
DUPLEXES = ["", "-", "+", "split", "off"]
POWER_LEVELS = [chirp_common.PowerLevel("High", watts=2.00),
chirp_common.PowerLevel("Low", watts=0.50)]
VALID_BANDS = [(400000000, 470000000)]
_magic = b"\x02" + b"PROGRAM"
_fingerprint = b"\x2E" + b"BF-T6" + b"\x2E"
_upper = 99
_mem_params = (_upper, # number of channels
_upper # number of names
)
_frs = _gmrs = _murs = _pmr = False
_ranges = [
(0x0000, 0x0B60),
]
_memsize = 0x0B60
def get_features(self):
rf = chirp_common.RadioFeatures()
rf.has_settings = True
rf.has_bank = False
rf.has_ctone = True
rf.has_cross = True
rf.has_rx_dtcs = True
rf.has_tuning_step = False
rf.has_name = self.HAS_NAMES
rf.can_odd_split = self.ODD_SPLIT
rf.valid_name_length = self.NAME_LENGTH
rf.valid_characters = self.VALID_CHARS
rf.valid_skips = self.SKIP_VALUES
rf.valid_tmodes = ["", "Tone", "TSQL", "DTCS", "Cross"]
rf.valid_cross_modes = ["Tone->Tone", "Tone->DTCS", "DTCS->Tone",
"->Tone", "->DTCS", "DTCS->", "DTCS->DTCS"]
rf.valid_dtcs_codes = self.DTCS_CODES
rf.valid_power_levels = self.POWER_LEVELS
rf.valid_duplexes = self.DUPLEXES
rf.valid_modes = ["FM", "NFM"] # 25 kHz, 12.5 kHz.
rf.memory_bounds = (1, self._upper)
rf.valid_tuning_steps = [2.5, 5., 6.25, 10., 12.5, 25.]
rf.valid_bands = self.VALID_BANDS
return rf
def process_mmap(self):
mem_format = MEM_FORMAT + MEM_FM_UL_FORMAT + MEM_FORMAT_PT2
self._memobj = bitwise.parse(mem_format % self._mem_params, self._mmap)
def sync_in(self):
"""Download from radio"""
try:
data = do_download(self)
except errors.RadioError:
# Pass through any real errors we raise
raise
except:
# If anything unexpected happens, make sure we raise
# a RadioError and log the problem
LOG.exception('Unexpected error during download')
raise errors.RadioError('Unexpected error communicating '
'with the radio')
self._mmap = data
self.process_mmap()
def sync_out(self):
"""Upload to radio"""
try:
do_upload(self)
except errors.RadioError:
raise
except Exception:
# If anything unexpected happens, make sure we raise
# a RadioError and log the problem
LOG.exception('Unexpected error during upload')
raise errors.RadioError('Unexpected error communicating '
'with the radio')
def get_raw_memory(self, number):
return repr(self._memobj.memory[number - 1])
def _get_tone(self, mem, _mem):
rx_tone = tx_tone = None
tx_tmode = TMODES[_mem.tx_tmode]
rx_tmode = TMODES[_mem.rx_tmode]
if tx_tmode == "Tone":
tx_tone = TONES[_mem.tx_tone]
elif tx_tmode == "DTCS":
tx_tone = self.DTCS_CODES[_mem.tx_tone]
if rx_tmode == "Tone":
rx_tone = TONES[_mem.rx_tone]
elif rx_tmode == "DTCS":
rx_tone = self.DTCS_CODES[_mem.rx_tone]
tx_pol = _mem.tx_tmode == 0x03 and "R" or "N"
rx_pol = _mem.rx_tmode == 0x03 and "R" or "N"
chirp_common.split_tone_decode(mem, (tx_tmode, tx_tone, tx_pol),
(rx_tmode, rx_tone, rx_pol))
def _is_txinh(self, _mem):
raw_tx = b""
for i in range(0, 4):
raw_tx += _mem.txfreq[i].get_raw()
return raw_tx == b"\xFF\xFF\xFF\xFF"
def _get_mem(self, number):
return self._memobj.memory[number - 1]
def _get_nam(self, number):
return self._memobj.names[number - 1]
def get_memory(self, number):
_mem = self._get_mem(number)
if self.HAS_NAMES:
_nam = self._get_nam(number)
mem = chirp_common.Memory()
mem.number = number
mem.freq = int(_mem.rxfreq) * 10
# We'll consider any blank (i.e. 0 MHz frequency) to be empty
if mem.freq == 0:
mem.empty = True
return mem
if _mem.rxfreq.get_raw() == b"\xFF\xFF\xFF\xFF":
mem.freq = 0
mem.empty = True
return mem
if _mem.get_raw() == (b"\xFF" * 16):
LOG.debug("Initializing empty memory")
_mem.set_raw("\x00" * 13 + "\xFF" * 3)
if self._is_txinh(_mem):
mem.duplex = "off"
mem.offset = 0
elif int(_mem.rxfreq) == int(_mem.txfreq):
mem.duplex = ""
mem.offset = 0
else:
mem.duplex = int(_mem.rxfreq) > int(_mem.txfreq) and "-" or "+"
mem.offset = abs(int(_mem.rxfreq) - int(_mem.txfreq)) * 10
# wide/narrow
mem.mode = _mem.isnarrow and "NFM" or "FM"
mem.skip = _mem.skip and "S" or ""
if self.HAS_NAMES:
for char in _nam.name:
if str(char) == "\xFF":
char = " " # The OEM software may have 0xFF mid-name
mem.name += str(char)
mem.name = mem.name.rstrip()
# tone data
self._get_tone(mem, _mem)
# tx power
levels = self.POWER_LEVELS
try:
mem.power = levels[_mem.lowpower]
except IndexError:
LOG.error("Radio reported invalid power level %s (in %s)" %
(_mem.lowpower, levels))
mem.power = levels[0]
immutable = []
if self._frs:
if mem.freq in FRS_FREQS:
if mem.number >= 1 and mem.number <= 22:
FRS_FREQ = FRS_FREQS[mem.number - 1]
mem.freq = FRS_FREQ
mem.duplex == ''
mem.offset = 0
mem.mode = "NFM"
if mem.number >= 8 and mem.number <= 14:
mem.power = self.POWER_LEVELS[1]
immutable = ["empty", "freq", "duplex", "offset", "mode",
"power"]
else:
immutable = ["empty", "freq", "duplex", "offset", "mode"]
elif self._murs:
if mem.freq in MURS_FREQS:
if mem.number >= 1 and mem.number <= 5:
MURS_FREQ = MURS_FREQS[mem.number - 1]
mem.freq = MURS_FREQ
mem.duplex = ''
mem.offset = 0
if mem.number <= 3:
mem.mode = "NFM"
immutable = ["empty", "freq", "duplex", "offset", "mode"]
else:
immutable = ["empty", "freq", "duplex", "offset"]
elif self._pmr:
if mem.freq in PMR_FREQS:
if mem.number >= 1 and mem.number <= 16:
PMR_FREQ = PMR_FREQS[mem.number - 1]
mem.freq = PMR_FREQ
mem.duplex = ''
mem.offset = 0
mem.mode = "NFM"
mem.power = self.POWER_LEVELS[1]
immutable = ["empty", "freq", "duplex", "offset", "mode",
"power"]
elif self._gmrs:
if mem.freq in GMRS_FREQS:
if mem.number >= 1 and mem.number <= 30:
GMRS_FREQ = GMRS_FREQS[mem.number - 1]
mem.freq = GMRS_FREQ
immutable = ["empty", "freq"]
if mem.number >= 1 and mem.number <= 7:
mem.duplex = ''
mem.offset = 0
immutable += ["duplex", "offset"]
elif mem.number >= 8 and mem.number <= 14:
mem.duplex = ''
mem.offset = 0
mem.mode = "NFM"
mem.power = self.POWER_LEVELS[1]
immutable += ["duplex", "offset", "mode", "power"]
elif mem.number >= 15 and mem.number <= 22:
mem.duplex = ''
mem.offset = 0
immutable += ["duplex", "offset"]
elif mem.number >= 23 and mem.number <= 30:
mem.duplex = '+'
mem.offset = 5000000
immutable += ["duplex", "offset"]
elif mem.freq in FRS_FREQS1:
mem.duplex = ''
mem.offset = 0
immutable += ["duplex", "offset"]
elif mem.freq in FRS_FREQS2:
mem.duplex = ''
mem.offset = 0
mem.mode = "NFM"
mem.power = self.POWER_LEVELS[1]
immutable += ["duplex", "offset", "mode", "power"]
elif mem.freq in FRS_FREQS3:
if mem.duplex == '':
mem.offset = 0
if mem.duplex == '+':
mem.offset = 5000000
else:
if mem.freq not in GMRS_FREQS:
mem.duplex = 'off'
mem.offset = 0
immutable = ["duplex", "offset"]
mem.immutable = immutable
return mem
def _set_tone(self, mem, _mem):
((txmode, txtone, txpol),
(rxmode, rxtone, rxpol)) = chirp_common.split_tone_encode(mem)
_mem.tx_tmode = TMODES.index(txmode)
_mem.rx_tmode = TMODES.index(rxmode)
if txmode == "Tone":
_mem.tx_tone = TONES.index(txtone)
elif txmode == "DTCS":
_mem.tx_tmode = txpol == "R" and 0x03 or 0x02
_mem.tx_tone = self.DTCS_CODES.index(txtone)
if rxmode == "Tone":
_mem.rx_tone = TONES.index(rxtone)
elif rxmode == "DTCS":
_mem.rx_tmode = rxpol == "R" and 0x03 or 0x02
_mem.rx_tone = self.DTCS_CODES.index(rxtone)
def _set_mem(self, number):
return self._memobj.memory[number - 1]
def _set_nam(self, number):
return self._memobj.names[number - 1]
def set_memory(self, mem):
_mem = self._set_mem(mem.number)
if self.HAS_NAMES:
_nam = self._set_nam(mem.number)
# if empty memory
if mem.empty:
_mem.set_raw("\xFF" * (_mem.size() // 8))
if self.HAS_NAMES:
for i in range(0, self.NAME_LENGTH):
_nam.name[i].set_raw("\xFF")
return mem
_mem.set_raw("\x00" * 13 + "\xFF" * 3)
# frequency
_mem.rxfreq = mem.freq / 10
if mem.duplex == "off":
for i in range(0, 4):
_mem.txfreq[i].set_raw("\xFF")
elif mem.duplex == "split":
_mem.txfreq = mem.offset / 10
elif mem.duplex == "+":
_mem.txfreq = (mem.freq + mem.offset) / 10
elif mem.duplex == "-":
_mem.txfreq = (mem.freq - mem.offset) / 10
else:
_mem.txfreq = mem.freq / 10
# wide/narrow
_mem.isnarrow = mem.mode == "NFM"
_mem.skip = mem.skip == "S"
if self.HAS_NAMES:
for i in range(self.NAME_LENGTH):
try:
_nam.name[i] = mem.name[i]
except IndexError:
_nam.name[i] = "\xFF"
# tone data
self._set_tone(mem, _mem)
# tx power
if str(mem.power) == "High":
_mem.lowpower = 0
elif str(mem.power) == "Low":
_mem.lowpower = 1
else:
_mem.lowpower = 0
return mem
def get_settings(self):
_fmpreset = self._memobj.fmpreset
_settings = self._memobj.settings
_settings2 = self._memobj.settings2
basic = RadioSettingGroup("basic", "Basic Settings")
top = RadioSettings(basic)
# Menu 03
rs = RadioSettingValueInteger(0, 9, _settings.squelch)
rset = RadioSetting("squelch", "Squelch Level", rs)
basic.append(rset)
model_list = ["RB27B", "RB27V", "RB627B"]
if self.MODEL in model_list:
# Menu 09 (RB27x/RB627x)
rs = RadioSettingValueList(TOT2_LIST, current_index=_settings.tot)
else:
# Menu 11 / 09 (RB27)
rs = RadioSettingValueList(TOT_LIST, current_index=_settings.tot)
rset = RadioSetting("tot", "Time-out timer", rs)
basic.append(rset)
# Menu 06
rs = RadioSettingValueList(VOX_LIST, current_index=_settings.vox)
rset = RadioSetting("vox", "VOX Level", rs)
basic.append(rset)
# Menu 15 (BF-T8) / Menu 14 (FRS-A1)
if self.MODEL == "FRS-A1":
# Menu 14 (FRS-A1)
def apply_voice_listvalue(setting, obj):
LOG.debug("Setting value: " + str(setting.value) +
" from list")
val = str(setting.value)
index = VOICE_CHOICES.index(val)
val = VOICE_VALUES[index]
obj.set_value(val)
if _settings.voice in VOICE_VALUES:
idx = VOICE_VALUES.index(_settings.voice)
else:
idx = VOICE_VALUES.index(0x00)
rs = RadioSettingValueList(VOICE_CHOICES, current_index=idx)
rset = RadioSetting("voice", "Voice", rs)
rset.set_apply_callback(apply_voice_listvalue, _settings.voice)
basic.append(rset)
else:
# Menu 15 (BF-T8)
rs = RadioSettingValueList(
VOICE_LIST, current_index=_settings.voice)
rset = RadioSetting("voice", "Voice", rs)
basic.append(rset)
# Menu 12
rs = RadioSettingValueBoolean(_settings.bcl)
rset = RadioSetting("bcl", "Busy Channel Lockout", rs)
basic.append(rset)
# Menu 10 / 08 (RB27/RB627)
rs = RadioSettingValueBoolean(_settings.save)
rset = RadioSetting("save", "Battery Saver", rs)
basic.append(rset)
# Menu 08 / 07 (RB-27/RB627)
rs = RadioSettingValueBoolean(_settings.tdr)
rset = RadioSetting("tdr", "Dual Watch", rs)
basic.append(rset)
# Menu 05
rs = RadioSettingValueBoolean(_settings.beep)
rset = RadioSetting("beep", "Beep", rs)
basic.append(rset)
# Menu 04
rs = RadioSettingValueList(ABR_LIST, current_index=_settings.abr)
rset = RadioSetting("abr", "Back Light", rs)
basic.append(rset)
# Menu 13 / 11 (RB-27/RB627)
rs = RadioSettingValueList(RING_LIST, current_index=_settings.ring)
rset = RadioSetting("ring", "Ring", rs)
basic.append(rset)
rs = RadioSettingValueBoolean(not _settings.ste)
rset = RadioSetting("ste", "Squelch Tail Eliminate", rs)
basic.append(rset)
# Menu 15 (FRS-A1)
if self.MODEL == "FRS-A1":
rs = RadioSettingValueList(MDF_LIST, current_index=_settings.mdf)
rset = RadioSetting("mdf", "Display Type", rs)
basic.append(rset)
if self.CH_OFFSET:
rs = RadioSettingValueInteger(1, self._upper, _settings.mra + 1)
else:
rs = RadioSettingValueInteger(1, self._upper, _settings.mra)
rset = RadioSetting("mra", "MR A Channel #", rs)
basic.append(rset)
if self.CH_OFFSET:
rs = RadioSettingValueInteger(1, self._upper, _settings.mrb + 1)
else:
rs = RadioSettingValueInteger(1, self._upper, _settings.mrb)
rset = RadioSetting("mrb", "MR B Channel #", rs)
basic.append(rset)
rs = RadioSettingValueList(AB_LIST, current_index=_settings.disp_ab)
rset = RadioSetting("disp_ab", "Selected Display Line", rs)
basic.append(rset)
if not self.MODEL.startswith("RB627"):
if self.MODEL == "FRS-A1":
del WX_LIST[7:]
rs = RadioSettingValueList(WX_LIST, current_index=_settings2.wx)
rset = RadioSetting("settings2.wx", "NOAA WX Radio", rs)
basic.append(rset)
def myset_freq(setting, obj, atrb, mult):
""" Callback to set frequency by applying multiplier"""
value = int(float(str(setting.value)) * mult)
setattr(obj, atrb, value)
return
# FM Broadcast Settings
val = _fmpreset.fmcur
val = val / 10.0
val_low = 76.0
if self.MODEL == "FRS-A1":
val_low = 87.0
if val < val_low or val > 108.0:
val = 90.4
rx = RadioSettingValueFloat(val_low, 108.0, val, 0.1, 1)
rset = RadioSetting("fmpreset.fmcur", "Broadcast FM Radio (MHz)", rx)
rset.set_apply_callback(myset_freq, _fmpreset, "fmcur", 10)
basic.append(rset)
model_list = ["BF-T8", "BF-U9", "AR-8"]
if self.MODEL in model_list:
rs = RadioSettingValueList(WORKMODE_LIST,
current_index=_settings2.workmode)
rset = RadioSetting("settings2.workmode", "Work Mode", rs)
basic.append(rset)
rs = RadioSettingValueList(
AREA_LIST, current_index=_settings2.area)
rs.set_mutable(False)
rset = RadioSetting("settings2.area", "Area", rs)
basic.append(rset)
return top
def set_settings(self, settings):
for element in settings:
if not isinstance(element, RadioSetting):
self.set_settings(element)
continue
else:
try:
if "." in element.get_name():
bits = element.get_name().split(".")
obj = self._memobj
for bit in bits[:-1]:
obj = getattr(obj, bit)
setting = bits[-1]
else:
obj = self._memobj.settings
setting = element.get_name()
if element.has_apply_callback():
LOG.debug("Using apply callback")
element.run_apply_callback()
elif setting == "mra" and self.CH_OFFSET:
setattr(obj, setting, int(element.value) - 1)
elif setting == "mrb" and self.CH_OFFSET:
setattr(obj, setting, int(element.value) - 1)
elif setting == "ste":
setattr(obj, setting, not int(element.value))
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
@classmethod
def match_model(cls, filedata, filename):
# This radio has always been post-metadata, so never do
# old-school detection
return False
class BFU9Alias(chirp_common.Alias):
VENDOR = "Baofeng"
MODEL = "BF-U9"
class AR8Alias(chirp_common.Alias):
VENDOR = "Arcshell"
MODEL = "AR-8"
@directory.register
class BaofengBFT8Generic(BFT8Radio):
ALIASES = [BFU9Alias, AR8Alias, ]
@directory.register
class RetevisRT16(BFT8Radio):
VENDOR = "Retevis"
MODEL = "RT16"
_upper = 22
_frs = True
@directory.register
class RetevisRB27B(BFT8Radio):
VENDOR = "Retevis"
MODEL = "RB27B"
DTCS_CODES = tuple(sorted(chirp_common.DTCS_CODES + (645,)))
HAS_NAMES = True
NAME_LENGTH = 6
VALID_CHARS = chirp_common.CHARSET_UPPER_NUMERIC + "-"
CH_OFFSET = True
SKIP_VALUES = ["", "S"]
POWER_LEVELS = [chirp_common.PowerLevel("High", watts=2.00),
chirp_common.PowerLevel("Low", watts=0.50)]
VALID_BANDS = [(400000000, 520000000)]
_upper = 22
_frs = True
_gmrs = _murs = _pmr = False
_ranges = [
(0x0000, 0x0640),
(0x0D00, 0x1040),
]
_memsize = 0x1040
def process_mmap(self):
mem_format = MEM_FORMAT + MEM_FM_U_FORMAT + MEM_FORMAT_PT2
self._memobj = bitwise.parse(mem_format % self._mem_params, self._mmap)
@directory.register
class RetevisRB27(RetevisRB27B):
VENDOR = "Retevis"
MODEL = "RB27"
DUPLEXES = ['', '-', '+', 'off']
POWER_LEVELS = [chirp_common.PowerLevel("High", watts=5.00),
chirp_common.PowerLevel("Low", watts=0.50)]
VALID_BANDS = [(136000000, 174000000),
(400000000, 520000000)]
ODD_SPLIT = False
_upper = 99
_gmrs = False # sold as GMRS radio but supports full band TX/RX
_frs = _murs = _pmr = False
def process_mmap(self):
mem_format = MEM_FORMAT + MEM_FM_U_FORMAT + MEM_FORMAT_PT2
self._memobj = bitwise.parse(mem_format % self._mem_params, self._mmap)
@directory.register
class RetevisRB27V(RetevisRB27B):
VENDOR = "Retevis"
MODEL = "RB27V"
VALID_BANDS = [(136000000, 174000000)]
_upper = 5
_murs = True
_frs = _gmrs = _pmr = False
def process_mmap(self):
mem_format = MEM_FORMAT + MEM_FM_U_FORMAT + MEM_FORMAT_PT2
self._memobj = bitwise.parse(mem_format % self._mem_params, self._mmap)
@directory.register
class RetevisRB627B(RetevisRB27B):
VENDOR = "Retevis"
MODEL = "RB627B"
POWER_LEVELS = [chirp_common.PowerLevel("High", watts=0.50),
chirp_common.PowerLevel("Low", watts=0.50)]
_upper = 16
_pmr = True
_frs = _gmrs = _murs = False
@directory.register
class FRSA1Radio(BFT8Radio):
"""BTECH FRS-A1"""
VENDOR = "BTECH"
MODEL = "FRS-A1"
ODD_SPLIT = False
HAS_NAMES = True
NAME_LENGTH = 6
SKIP_VALUES = ["", "S"]
DTCS_CODES = tuple(sorted(chirp_common.DTCS_CODES + (645,)))
DUPLEXES = ['', '-', '+', 'off']
_fingerprint = b"BF-T8A" + b"\x2E"
_upper = 22
_frs = _upper == 22
_ranges = [
(0x0000, 0x0640),
(0x0D00, 0x0DC0),
]
_memsize = 0x0DC0
def get_features(self):
rf = BFT8Radio.get_features(self)
rf.valid_name_length = 6
rf.valid_characters = chirp_common.CHARSET_UPPER_NUMERIC + "-"
return rf
chirp-20261030/chirp/drivers/bj9900.py 0000644 0001750 0001750 00000031626 14751356113 016122 0 ustar hibby hibby #
# Copyright 2015 Marco Filippi IZ3GME
#
# 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 .
"""Baojie BJ-9900 management module"""
from chirp import chirp_common, util, memmap, errors, directory, bitwise
import struct
import time
import logging
LOG = logging.getLogger(__name__)
CMD_ACK = b'\x06'
@directory.register
class BJ9900Radio(chirp_common.CloneModeRadio,
chirp_common.ExperimentalRadio):
"""Baojie BJ-9900"""
VENDOR = "Baojie"
MODEL = "BJ-9900"
VARIANT = ""
BAUD_RATE = 115200
DUPLEX = ["", "-", "+", "split"]
MODES = ["NFM", "FM"]
TMODES = ["", "Tone", "TSQL", "DTCS", "Cross"]
CROSS_MODES = ["Tone->Tone", "Tone->DTCS", "DTCS->Tone",
"->Tone", "->DTCS", "DTCS->", "DTCS->DTCS"]
STEPS = [5.0, 6.25, 10.0, 12.5, 25.0]
VALID_BANDS = [(109000000, 136000000), (136000000, 174000000),
(400000000, 470000000)]
CHARSET = list(chirp_common.CHARSET_ALPHANUMERIC + '-')
CHARSET.remove(" ")
POWER_LEVELS = [
chirp_common.PowerLevel("Low", watts=20.00),
chirp_common.PowerLevel("High", watts=40.00)]
_memsize = 0x18F1
# dat file format is
# 2 char per byte hex string
# on CR LF terminated lines of 96 char
# plus an empty line at the end
_datsize = (_memsize * 2) // 96 * 98 + 2
# block are read in same order as original sw even though they are not
# in physical order
_blocks = [
(0x400, 0x1BFF, 0x30),
(0x300, 0x32F, 0x30),
(0x380, 0x3AF, 0x30),
(0x200, 0x22F, 0x30),
(0x240, 0x26F, 0x30),
(0x270, 0x2A0, 0x31),
]
MEM_FORMAT = """
#seekto 0x%X;
struct {
u32 rxfreq;
u16 is_rxdigtone:1,
rxdtcs_pol:1,
rxtone:14;
u8 rxdtmf:4,
spmute:4;
u8 unknown1;
u32 txfreq;
u16 is_txdigtone:1,
txdtcs_pol:1,
txtone:14;
u8 txdtmf:4,
pttid:4;
u8 power:1,
wide:1,
compandor:1,
unknown3:5;
u8 namelen;
char name[7];
} memory[128];
"""
@classmethod
def get_prompts(cls):
rp = chirp_common.RadioPrompts()
rp.pre_upload = rp.pre_download = _(
"1. Turn radio off.\n"
"2. Remove front head.\n"
"3. Connect data cable to radio, use the same connector where\n"
" head was connected to, not the mic connector .\n"
"4. Click OK.\n")
rp.experimental = _(
'This is experimental support for BJ-9900 '
'which is still under development.\n'
'Please ensure you have a good backup with OEM software.\n'
'Also please send in bug and enhancement requests!\n'
'You have been warned. Proceed at your own risk!')
return rp
def _read(self, addr, blocksize):
# read a single block
msg = struct.pack(">4sHH", b"READ", addr, blocksize)
LOG.debug("sending " + util.hexprint(msg))
self.pipe.write(msg)
block = self.pipe.read(blocksize)
LOG.debug("received " + util.hexprint(block))
if len(block) != blocksize:
raise errors.RadioError(
("Unable to read block at addr %04X expected"
" %i got %i bytes") % (addr, blocksize, len(block)))
return block
def _clone_in(self):
start = time.time()
data = b""
status = chirp_common.Status()
status.msg = _("Cloning from radio")
status.max = self._memsize
for addr_from, addr_to, blocksize in self._blocks:
for addr in range(addr_from, addr_to, blocksize):
data += self._read(addr, blocksize)
status.cur = len(data)
self.status_fn(status)
LOG.info("Clone completed in %i seconds" % (time.time() - start))
# This driver will accept any amount of garbage, and fails tests
# as a result. This assertion appears correct based on the file
# data matching below. However, untested as of 2024-Feb.
assert data[-2:] == b'\r\n', 'Unrecognized radio image'
return memmap.MemoryMapBytes(data)
def _write(self, addr, block):
# write a single block
msg = struct.pack(">4sHH", b"WRIE", addr, len(block)) + block
LOG.debug("sending " + util.hexprint(msg))
self.pipe.write(msg)
data = self.pipe.read(1)
LOG.debug("received " + util.hexprint(data))
if data != CMD_ACK:
raise errors.RadioError(
"Radio refused to accept block 0x%04x" % addr)
def _clone_out(self):
start = time.time()
status = chirp_common.Status()
status.msg = _("Cloning to radio")
status.max = self._memsize
pos = 0
for addr_from, addr_to, blocksize in self._blocks:
for addr in range(addr_from, addr_to, blocksize):
self._write(addr, self._mmap[pos:(pos + blocksize)])
pos += blocksize
status.cur = pos
self.status_fn(status)
LOG.info("Clone completed in %i seconds" % (time.time() - start))
def sync_in(self):
try:
self._mmap = self._clone_in()
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._clone_out()
except errors.RadioError:
raise
except Exception as e:
raise errors.RadioError("Failed to communicate with radio: %s" % e)
def process_mmap(self):
if len(self._mmap) == self._datsize:
self._mmap = memmap.MemoryMapBytes([
chr(int(self._mmap.get(i, 2), 16))
for i in range(0, self._datsize, 2)
if self._mmap.get(i, 2) != b"\r\n"
])
try:
self._memobj = bitwise.parse(
self.MEM_FORMAT % self._memstart, self._mmap)
except AttributeError:
# main variant have no _memstart attribute
return
def get_features(self):
rf = chirp_common.RadioFeatures()
rf.has_bank = False
rf.has_dtcs_polarity = True
rf.has_nostep_tuning = False
rf.valid_modes = list(self.MODES)
rf.valid_tmodes = list(self.TMODES)
rf.valid_cross_modes = list(self.CROSS_MODES)
rf.valid_duplexes = list(self.DUPLEX)
rf.has_tuning_step = False
rf.valid_tuning_steps = list(self.STEPS)
rf.valid_bands = self.VALID_BANDS
rf.valid_skips = [""]
rf.valid_power_levels = self.POWER_LEVELS
rf.valid_characters = "".join(self.CHARSET)
rf.valid_name_length = 7
rf.memory_bounds = (1, 128)
rf.can_odd_split = True
rf.has_settings = False
rf.has_cross = True
rf.has_ctone = True
rf.has_rx_dtcs = True
rf.has_sub_devices = self.VARIANT == ""
return rf
def get_sub_devices(self):
return [BJ9900RadioLeft(self._mmap), BJ9900RadioRight(self._mmap)]
def get_raw_memory(self, number):
return repr(self._memobj.memory[number - 1])
def set_memory(self, mem):
_mem = self._memobj.memory[mem.number - 1]
if mem.empty:
_mem.fill_raw(b"\xff")
_mem.namelen = 0
return
_mem.rxfreq = mem.freq / 10
if mem.duplex == "split":
_mem.txfreq = mem.offset // 10
elif mem.duplex == "+":
_mem.txfreq = (mem.freq + mem.offset) // 10
elif mem.duplex == "-":
_mem.txfreq = (mem.freq - mem.offset) // 10
else:
_mem.txfreq = mem.freq // 10
_mem.namelen = len(mem.name)
_mem.name = mem.name.ljust(7)
rxmode = ""
txmode = ""
if mem.tmode == "Tone":
txmode = "Tone"
elif mem.tmode == "TSQL":
rxmode = "Tone"
txmode = "TSQL"
elif mem.tmode == "DTCS":
rxmode = "DTCSSQL"
txmode = "DTCS"
elif mem.tmode == "Cross":
txmode, rxmode = mem.cross_mode.split("->", 1)
if rxmode == "":
_mem.rxdtcs_pol = 1
_mem.is_rxdigtone = 1
_mem.rxtone = 0x3FFF
elif rxmode == "Tone":
_mem.rxdtcs_pol = 0
_mem.is_rxdigtone = 0
_mem.rxtone = int(mem.ctone * 10)
elif rxmode == "DTCSSQL":
_mem.rxdtcs_pol = 1 if mem.dtcs_polarity[1] == "R" else 0
_mem.is_rxdigtone = 1
_mem.rxtone = mem.dtcs
elif rxmode == "DTCS":
_mem.rxdtcs_pol = 1 if mem.dtcs_polarity[1] == "R" else 0
_mem.is_rxdigtone = 1
_mem.rxtone = mem.rx_dtcs
if txmode == "":
_mem.txdtcs_pol = 1
_mem.is_txdigtone = 1
_mem.txtone = 0x3FFF
elif txmode == "Tone":
_mem.txdtcs_pol = 0
_mem.is_txdigtone = 0
_mem.txtone = int(mem.rtone * 10)
elif txmode == "TSQL":
_mem.txdtcs_pol = 0
_mem.is_txdigtone = 0
_mem.txtone = int(mem.ctone * 10)
elif txmode == "DTCS":
_mem.txdtcs_pol = 1 if mem.dtcs_polarity[0] == "R" else 0
_mem.is_txdigtone = 1
_mem.txtone = mem.dtcs
if (mem.power):
_mem.power = self.POWER_LEVELS.index(mem.power)
_mem.wide = self.MODES.index(mem.mode)
# not supported yet
_mem.compandor = 0
_mem.pttid = 0
_mem.txdtmf = 0
_mem.rxdtmf = 0
_mem.spmute = 0
# set to mimic radio behaviour
_mem.unknown3 = 0
def get_memory(self, number):
_mem = self._memobj.memory[number - 1]
mem = chirp_common.Memory()
mem.number = number
if _mem.get_raw()[0] == 0xFF:
mem.empty = True
return mem
mem.freq = int(_mem.rxfreq) * 10
if int(_mem.rxfreq) == int(_mem.txfreq) or _mem.txfreq == 0xFFFFFFFF:
mem.duplex = ""
mem.offset = 0
elif abs(int(_mem.rxfreq) * 10 - int(_mem.txfreq) * 10) > 70000000:
mem.duplex = "split"
mem.offset = int(_mem.txfreq) * 10
else:
mem.duplex = int(_mem.rxfreq) > int(_mem.txfreq) and "-" or "+"
mem.offset = abs(int(_mem.rxfreq) - int(_mem.txfreq)) * 10
mem.name = str(_mem.name)[:_mem.namelen]
dtcs_pol = ["N", "N"]
if _mem.rxtone == 0x3FFF:
rxmode = ""
elif _mem.is_rxdigtone == 0:
# ctcss
rxmode = "Tone"
mem.ctone = int(_mem.rxtone) / 10.0
else:
# digital
rxmode = "DTCS"
mem.rx_dtcs = int(_mem.rxtone & 0x3FFF)
if _mem.rxdtcs_pol == 1:
dtcs_pol[1] = "R"
if _mem.txtone == 0x3FFF:
txmode = ""
elif _mem.is_txdigtone == 0:
# ctcss
txmode = "Tone"
mem.rtone = int(_mem.txtone) / 10.0
else:
# digital
txmode = "DTCS"
mem.dtcs = int(_mem.txtone & 0x3FFF)
if _mem.txdtcs_pol == 1:
dtcs_pol[0] = "R"
if txmode == "Tone" and not rxmode:
mem.tmode = "Tone"
elif txmode == rxmode and txmode == "Tone" and mem.rtone == mem.ctone:
mem.tmode = "TSQL"
elif txmode == rxmode and txmode == "DTCS" and mem.dtcs == mem.rx_dtcs:
mem.tmode = "DTCS"
elif rxmode or txmode:
mem.tmode = "Cross"
mem.cross_mode = "%s->%s" % (txmode, rxmode)
mem.dtcs_polarity = "".join(dtcs_pol)
mem.power = self.POWER_LEVELS[_mem.power]
mem.mode = self.MODES[_mem.wide]
return mem
@classmethod
def match_model(cls, filedata, filename):
return len(filedata) == cls._memsize or \
(len(filedata) == cls._datsize and filedata[-4:] == b"\r\n\r\n")
class BJ9900RadioLeft(BJ9900Radio):
"""Baojie BJ-9900 Left VFO subdevice"""
VARIANT = "Left"
_memstart = 0x0
class BJ9900RadioRight(BJ9900Radio):
"""Baojie BJ-9900 Right VFO subdevice"""
VARIANT = "Right"
_memstart = 0xC00
chirp-20261030/chirp/drivers/bjuv55.py 0000644 0001750 0001750 00000050004 14670320477 016320 0 ustar hibby hibby # Copyright 2013 Jens Jensen AF5MI
# Based on work by Jim Unroe, Dan Smith, et al.
# Special thanks to Mats SM0BTP for equipment donation.
#
# 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 .
import logging
from chirp.drivers import uv5r
from chirp.drivers import baofeng_common as bfc
from chirp import chirp_common, directory
from chirp import bitwise
from chirp.settings import RadioSetting, RadioSettingGroup, \
RadioSettingValueInteger, RadioSettingValueList, \
RadioSettingValueBoolean, RadioSettingValueString, \
RadioSettingValueFloat, InvalidValueError, RadioSettings
LOG = logging.getLogger(__name__)
BJUV55_MODEL = b"\x50\xBB\xDD\x55\x63\x98\x4D"
COLOR_LIST = ["Off", "Blue", "Red", "Pink"]
STEPS = list(uv5r.STEPS)
STEPS.remove(2.5)
STEP_LIST = [str(x) for x in STEPS]
MEM_FORMAT = """
#seekto 0x0008;
struct {
lbcd rxfreq[4];
lbcd txfreq[4];
ul16 rxtone;
ul16 txtone;
u8 unused1:3,
isuhf:1,
scode:4;
u8 unknown1:7,
txtoneicon:1;
u8 mailicon:3,
unknown2:4,
lowpower:1;
u8 unknown3:1,
wide:1,
unknown4:2,
bcl:1,
scan:1,
pttid:2;
} memory[128];
#seekto 0x0B08;
struct {
u8 code[5];
u8 unused[11];
} pttid[15];
#seekto 0x0C88;
struct {
u8 inspection[5];
u8 monitor[5];
u8 alarmcode[5];
u8 unknown1;
u8 stun[5];
u8 kill[5];
u8 revive[5];
u8 unknown2;
u8 master_control_id[5];
u8 vice_control_id[5];
u8 code[5];
u8 unused1:6,
aniid:2;
u8 unknown[2];
u8 dtmfon;
u8 dtmfoff;
} ani;
#seekto 0x0E28;
struct {
u8 squelch;
u8 step;
u8 tdrab;
u8 tdr;
u8 vox;
u8 timeout;
u8 unk2[6];
u8 abr;
u8 beep;
u8 ani;
u8 unknown3[2];
u8 voice;
u8 ring_time;
u8 dtmfst;
u8 unknown5;
u8 unknown12:6,
screv:2;
u8 pttid;
u8 pttlt;
u8 mdfa;
u8 mdfb;
u8 bcl;
u8 autolk;
u8 sftd;
u8 unknown6[3];
u8 wtled;
u8 rxled;
u8 txled;
u8 unknown7[5];
u8 save;
u8 unknown8;
u8 displayab:1,
unknown1:2,
fmradio:1,
alarm:1,
unknown2:1,
reset:1,
menu:1;
u8 vfomrlock;
u8 workmode;
u8 keylock;
u8 workmode_channel;
u8 password[6];
u8 unknown10[11];
} settings;
#seekto 0x0E7E;
struct {
u8 mrcha;
u8 mrchb;
} wmchannel;
#seekto 0x0F10;
struct {
u8 freq[8];
u8 unknown1;
u8 offset[4];
u8 unknown2;
ul16 rxtone;
ul16 txtone;
u8 unused1:7,
band:1;
u8 unknown3;
u8 unused2:2,
sftd:2,
scode:4;
u8 unknown4;
u8 unused3:1,
step:3,
unused4:4;
u8 txpower:1,
widenarr:1,
unknown5:6;
} vfoa;
#seekto 0x0F30;
struct {
u8 freq[8];
u8 unknown1;
u8 offset[4];
u8 unknown2;
ul16 rxtone;
ul16 txtone;
u8 unused1:7,
band:1;
u8 unknown3;
u8 unused2:2,
sftd:2,
scode:4;
u8 unknown4;
u8 unused3:1,
step:3,
unused4:4;
u8 txpower:1,
widenarr:1,
unknown5:6;
} vfob;
#seekto 0x0F57;
u8 fm_preset;
#seekto 0x1008;
struct {
char name[6];
u8 unknown2[10];
} names[128];
#seekto 0x%04X;
struct {
char line1[7];
char line2[7];
} poweron_msg;
#seekto 0x1838;
struct {
char line1[7];
char line2[7];
} firmware_msg;
#seekto 0x1849;
u8 power_vhf_hi[14]; // 136-174 MHz, 3 MHz divisions
u8 power_uhf_hi[14]; // 400-470 MHz, 5 MHz divisions
#seekto 0x1889;
u8 power_vhf_lo[14];
u8 power_uhf_lo[14];
struct limit {
u8 enable;
bbcd lower[2];
bbcd upper[2];
};
#seekto 0x1908;
struct {
struct limit vhf;
u8 unk11[11];
struct limit uhf;
} limits;
"""
@directory.register
class BaojieBJUV55Radio(uv5r.BaofengUV5R):
VENDOR = "Baojie"
MODEL = "BJ-UV55"
_basetype = [b"BJ55"]
_idents = [BJUV55_MODEL]
_mem_params = (0x1928 # poweron_msg offset
)
_fw_ver_file_start = 0x1938
_fw_ver_file_stop = 0x193E
def get_features(self):
rf = super(BaojieBJUV55Radio, self).get_features()
rf.valid_name_length = 6
return rf
def process_mmap(self):
self._memobj = bitwise.parse(MEM_FORMAT % self._mem_params, self._mmap)
def set_memory(self, mem):
super(BaojieBJUV55Radio, self).set_memory(mem)
_mem = self._memobj.memory[mem.number]
if (mem.freq - mem.offset) > (400 * 1000000):
_mem.isuhf = True
else:
_mem.isuhf = False
if mem.tmode in ["Tone", "TSQL"]:
_mem.txtoneicon = True
else:
_mem.txtoneicon = False
def _get_settings(self):
_settings = self._memobj.settings
basic = RadioSettingGroup("basic", "Basic Settings")
advanced = RadioSettingGroup("advanced", "Advanced Settings")
group = RadioSettings(basic, advanced)
rs = RadioSetting("squelch", "Carrier Squelch Level",
RadioSettingValueInteger(0, 9, _settings.squelch))
basic.append(rs)
rs = RadioSetting("save", "Battery Saver",
RadioSettingValueInteger(0, 4, _settings.save))
basic.append(rs)
rs = RadioSetting("abr", "Backlight",
RadioSettingValueBoolean(_settings.abr))
basic.append(rs)
rs = RadioSetting("tdr", "Dual Watch (BDR)",
RadioSettingValueBoolean(_settings.tdr))
advanced.append(rs)
rs = RadioSetting("tdrab", "Dual Watch TX Priority",
RadioSettingValueList(
uv5r.TDRAB_LIST,
current_index=_settings.tdrab))
advanced.append(rs)
rs = RadioSetting("alarm", "Alarm",
RadioSettingValueBoolean(_settings.alarm))
advanced.append(rs)
rs = RadioSetting("beep", "Beep",
RadioSettingValueBoolean(_settings.beep))
basic.append(rs)
rs = RadioSetting("timeout", "Timeout Timer",
RadioSettingValueList(
uv5r.TIMEOUT_LIST,
current_index=_settings.timeout))
basic.append(rs)
rs = RadioSetting("screv", "Scan Resume",
RadioSettingValueList(
uv5r.RESUME_LIST,
current_index=_settings.screv))
advanced.append(rs)
rs = RadioSetting("mdfa", "Display Mode (A)",
RadioSettingValueList(
uv5r.MODE_LIST, current_index=_settings.mdfa))
basic.append(rs)
rs = RadioSetting("mdfb", "Display Mode (B)",
RadioSettingValueList(
uv5r.MODE_LIST, current_index=_settings.mdfb))
basic.append(rs)
rs = RadioSetting("bcl", "Busy Channel Lockout",
RadioSettingValueBoolean(_settings.bcl))
advanced.append(rs)
rs = RadioSetting("autolk", "Automatic Key Lock",
RadioSettingValueBoolean(_settings.autolk))
advanced.append(rs)
rs = RadioSetting("fmradio", "Broadcast FM Radio",
RadioSettingValueBoolean(_settings.fmradio))
advanced.append(rs)
rs = RadioSetting("wtled", "Standby LED Color",
RadioSettingValueList(
COLOR_LIST, current_index=_settings.wtled))
basic.append(rs)
rs = RadioSetting("rxled", "RX LED Color",
RadioSettingValueList(
COLOR_LIST, current_index=_settings.rxled))
basic.append(rs)
rs = RadioSetting("txled", "TX LED Color",
RadioSettingValueList(
COLOR_LIST, current_index=_settings.txled))
basic.append(rs)
rs = RadioSetting("reset", "RESET Menu",
RadioSettingValueBoolean(_settings.reset))
advanced.append(rs)
rs = RadioSetting("menu", "All Menus",
RadioSettingValueBoolean(_settings.menu))
advanced.append(rs)
if len(self._mmap.get_packed()) == 0x1808:
# Old image, without aux block
return group
other = RadioSettingGroup("other", "Other Settings")
group.append(other)
def _filter(name):
filtered = ""
for char in str(name):
if char in chirp_common.CHARSET_ASCII:
filtered += char
else:
filtered += " "
return filtered
_msg = self._memobj.poweron_msg
rs = RadioSetting("poweron_msg.line1", "Power-On Message 1",
RadioSettingValueString(0, 7, _filter(_msg.line1)))
other.append(rs)
rs = RadioSetting("poweron_msg.line2", "Power-On Message 2",
RadioSettingValueString(0, 7, _filter(_msg.line2)))
other.append(rs)
limit = "limits"
vhf_limit = getattr(self._memobj, limit).vhf
rs = RadioSetting("%s.vhf.lower" % limit, "VHF Lower Limit (MHz)",
RadioSettingValueInteger(
1, 1000, vhf_limit.lower))
other.append(rs)
rs = RadioSetting("%s.vhf.upper" % limit, "VHF Upper Limit (MHz)",
RadioSettingValueInteger(
1, 1000, vhf_limit.upper))
other.append(rs)
rs = RadioSetting("%s.vhf.enable" % limit, "VHF TX Enabled",
RadioSettingValueBoolean(vhf_limit.enable))
other.append(rs)
uhf_limit = getattr(self._memobj, limit).uhf
rs = RadioSetting("%s.uhf.lower" % limit, "UHF Lower Limit (MHz)",
RadioSettingValueInteger(
1, 1000, uhf_limit.lower))
other.append(rs)
rs = RadioSetting("%s.uhf.upper" % limit, "UHF Upper Limit (MHz)",
RadioSettingValueInteger(
1, 1000, uhf_limit.upper))
other.append(rs)
rs = RadioSetting("%s.uhf.enable" % limit, "UHF TX Enabled",
RadioSettingValueBoolean(uhf_limit.enable))
other.append(rs)
workmode = RadioSettingGroup("workmode", "Work Mode Settings")
group.append(workmode)
options = ["A", "B"]
rs = RadioSetting("displayab", "Display Selected",
RadioSettingValueList(
options, current_index=_settings.displayab))
workmode.append(rs)
options = ["Frequency", "Channel"]
rs = RadioSetting("workmode", "VFO/MR Mode",
RadioSettingValueList(
options, current_index=_settings.workmode))
workmode.append(rs)
rs = RadioSetting("keylock", "Keypad Lock",
RadioSettingValueBoolean(_settings.keylock))
workmode.append(rs)
_mrcna = self._memobj.wmchannel.mrcha
rs = RadioSetting("wmchannel.mrcha", "MR A Channel",
RadioSettingValueInteger(0, 127, _mrcna))
workmode.append(rs)
_mrcnb = self._memobj.wmchannel.mrchb
rs = RadioSetting("wmchannel.mrchb", "MR B Channel",
RadioSettingValueInteger(0, 127, _mrcnb))
workmode.append(rs)
def my_validate(value):
value = chirp_common.parse_freq(value)
if 17400000 <= value and value < 40000000:
raise InvalidValueError("Can't be between 174.00000-400.00000")
return chirp_common.format_freq(value)
def apply_freq(setting, obj):
value = chirp_common.parse_freq(str(setting.value)) / 10
obj.band = value >= 40000000
for i in range(7, -1, -1):
obj.freq[i] = value % 10
value /= 10
val1a = RadioSettingValueString(0, 10,
bfc.bcd_decode_freq(
self._memobj.vfoa.freq))
val1a.set_validate_callback(my_validate)
rs = RadioSetting("vfoa.freq", "VFO A Frequency", val1a)
rs.set_apply_callback(apply_freq, self._memobj.vfoa)
workmode.append(rs)
val1b = RadioSettingValueString(0, 10,
bfc.bcd_decode_freq(
self._memobj.vfob.freq))
val1b.set_validate_callback(my_validate)
rs = RadioSetting("vfob.freq", "VFO B Frequency", val1b)
rs.set_apply_callback(apply_freq, self._memobj.vfob)
workmode.append(rs)
options = ["Off", "+", "-"]
rs = RadioSetting("vfoa.sftd", "VFO A Shift",
RadioSettingValueList(
options, current_index=self._memobj.vfoa.sftd))
workmode.append(rs)
rs = RadioSetting("vfob.sftd", "VFO B Shift",
RadioSettingValueList(
options, current_index=self._memobj.vfob.sftd))
workmode.append(rs)
def convert_bytes_to_offset(bytes):
real_offset = 0
for byte in bytes:
real_offset = (real_offset * 10) + byte
return chirp_common.format_freq(real_offset * 10000)
def apply_offset(setting, obj):
value = chirp_common.parse_freq(str(setting.value)) / 10000
for i in range(3, -1, -1):
obj.offset[i] = value % 10
value /= 10
val1a = RadioSettingValueString(
0, 10, convert_bytes_to_offset(self._memobj.vfoa.offset))
rs = RadioSetting("vfoa.offset", "VFO A Offset (0.00-69.95)", val1a)
rs.set_apply_callback(apply_offset, self._memobj.vfoa)
workmode.append(rs)
val1b = RadioSettingValueString(
0, 10, convert_bytes_to_offset(self._memobj.vfob.offset))
rs = RadioSetting("vfob.offset", "VFO B Offset (0.00-69.95)", val1b)
rs.set_apply_callback(apply_offset, self._memobj.vfob)
workmode.append(rs)
options = ["High", "Low"]
rs = RadioSetting(
"vfoa.txpower", "VFO A Power",
RadioSettingValueList(
options, current_index=self._memobj.vfoa.txpower))
workmode.append(rs)
rs = RadioSetting(
"vfob.txpower", "VFO B Power",
RadioSettingValueList(
options, current_index=self._memobj.vfob.txpower))
workmode.append(rs)
options = ["Wide", "Narrow"]
rs = RadioSetting(
"vfoa.widenarr", "VFO A Bandwidth",
RadioSettingValueList(
options, current_index=self._memobj.vfoa.widenarr))
workmode.append(rs)
rs = RadioSetting(
"vfob.widenarr", "VFO B Bandwidth",
RadioSettingValueList(
options, current_index=self._memobj.vfob.widenarr))
workmode.append(rs)
options = ["%s" % x for x in range(1, 16)]
rs = RadioSetting("vfoa.scode", "VFO A PTT-ID",
RadioSettingValueList(
options, current_index=self._memobj.vfoa.scode))
workmode.append(rs)
rs = RadioSetting("vfob.scode", "VFO B PTT-ID",
RadioSettingValueList(
options, current_index=self._memobj.vfob.scode))
workmode.append(rs)
rs = RadioSetting("vfoa.step", "VFO A Tuning Step",
RadioSettingValueList(
STEP_LIST, current_index=self._memobj.vfoa.step))
workmode.append(rs)
rs = RadioSetting("vfob.step", "VFO B Tuning Step",
RadioSettingValueList(
STEP_LIST, current_index=self._memobj.vfob.step))
workmode.append(rs)
fm_preset = RadioSettingGroup("fm_preset", "FM Radio Preset")
group.append(fm_preset)
preset = self._memobj.fm_preset / 10.0 + 87
rs = RadioSetting("fm_preset", "FM Preset(MHz)",
RadioSettingValueFloat(87, 107.5, preset, 0.1, 1))
fm_preset.append(rs)
dtmf = RadioSettingGroup("dtmf", "DTMF Settings")
group.append(dtmf)
dtmfchars = "0123456789 *#ABCD"
for i in range(0, 15):
_codeobj = self._memobj.pttid[i].code
_code = "".join([dtmfchars[x] for x in _codeobj if int(x) < 0x1F])
val = RadioSettingValueString(0, 5, _code, False)
val.set_charset(dtmfchars)
rs = RadioSetting("pttid/%i.code" % i,
"PTT ID Code %i" % (i + 1), val)
def apply_code(setting, obj):
code = []
for j in range(0, 5):
try:
code.append(dtmfchars.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)
_codeobj = self._memobj.ani.code
_code = "".join([dtmfchars[x] for x in _codeobj if int(x) < 0x1F])
val = RadioSettingValueString(0, 5, _code, False)
val.set_charset(dtmfchars)
rs = RadioSetting("ani.code", "ANI Code", val)
def apply_code(setting, obj):
code = []
for j in range(0, 5):
try:
code.append(dtmfchars.index(str(setting.value)[j]))
except IndexError:
code.append(0xFF)
obj.code = code
rs.set_apply_callback(apply_code, self._memobj.ani)
dtmf.append(rs)
options = ["Off", "BOT", "EOT", "Both"]
rs = RadioSetting("ani.aniid", "ANI ID",
RadioSettingValueList(
options, current_index=self._memobj.ani.aniid))
dtmf.append(rs)
_codeobj = self._memobj.ani.alarmcode
_code = "".join([dtmfchars[x] for x in _codeobj if int(x) < 0x1F])
val = RadioSettingValueString(0, 5, _code, False)
val.set_charset(dtmfchars)
rs = RadioSetting("ani.alarmcode", "Alarm Code", val)
def apply_code(setting, obj):
alarmcode = []
for j in range(5):
try:
alarmcode.append(dtmfchars.index(str(setting.value)[j]))
except IndexError:
alarmcode.append(0xFF)
obj.alarmcode = alarmcode
rs.set_apply_callback(apply_code, self._memobj.ani)
dtmf.append(rs)
rs = RadioSetting("dtmfst", "DTMF Sidetone",
RadioSettingValueList(
uv5r.DTMFST_LIST,
current_index=_settings.dtmfst))
dtmf.append(rs)
rs = RadioSetting("ani.dtmfon", "DTMF Speed (on)",
RadioSettingValueList(
uv5r.DTMFSPEED_LIST,
current_index=self._memobj.ani.dtmfon))
dtmf.append(rs)
rs = RadioSetting("ani.dtmfoff", "DTMF Speed (off)",
RadioSettingValueList(
uv5r.DTMFSPEED_LIST,
current_index=self._memobj.ani.dtmfoff))
dtmf.append(rs)
return group
def _set_fm_preset(self, settings):
for element in settings:
try:
val = element.value
value = int(val.get_value() * 10 - 870)
LOG.debug("Setting fm_preset = %s" % (value))
self._memobj.fm_preset = value
except Exception:
LOG.debug(element.get_name())
raise
chirp-20261030/chirp/drivers/boblov_x3plus.py 0000644 0001750 0001750 00000045654 15257672455 020030 0 ustar hibby hibby """
Radio driver for the Boblov X3 Plus Motorcycle Helmet Radio
"""
# Copyright 2018 Robert C Jennings
#
# 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 .
import logging
import struct
import time
from datetime import datetime
from chirp import (
bitwise,
chirp_common,
directory,
errors,
memmap,
util,
)
from chirp.settings import (
RadioSetting,
RadioSettingGroup,
RadioSettings,
RadioSettingValueBoolean,
RadioSettingValueInteger,
RadioSettingValueList,
)
LOG = logging.getLogger(__name__)
@directory.register
class BoblovX3Plus(chirp_common.CloneModeRadio,
chirp_common.ExperimentalRadio):
"""Boblov X3 Plus motorcycle/cycling helmet radio"""
VENDOR = 'Boblov'
MODEL = 'X3Plus'
BAUD_RATE = 9600
NEEDS_COMPAT_SERIAL = True
CHANNELS = 16
MEM_FORMAT = """
#seekto 0x0010;
struct {
lbcd rxfreq[4];
lbcd txfreq[4];
lbcd rxtone[2];
lbcd txtone[2];
u8 unknown1:1,
compander:1,
scramble:1,
skip:1,
highpower:1,
narrow:1,
unknown2:1,
bcl:1;
u8 unknown3[3];
} memory[16];
#seekto 0x03C0;
struct {
u8 unknown1:4,
voiceprompt:2,
batterysaver:1,
beep:1;
u8 squelchlevel;
u8 unknown2;
u8 timeouttimer;
u8 voxlevel;
u8 unknown3;
u8 unknown4;
u8 voxdelay;
} settings;
"""
# Radio command data
CMD_ACK = '\x06'
CMD_IDENTIFY = '\x02'
CMD_PROGRAM_ENTER = '.VKOGRAM'
CMD_PROGRAM_EXIT = '\x62' # 'b'
CMD_READ = 'R'
CMD_WRITE = 'W'
BLOCK_SIZE = 0x08
VOICE_LIST = ['Off', 'Chinese', 'English']
TIMEOUTTIMER_LIST = ['Off', '30 seconds', '60 seconds', '90 seconds',
'120 seconds', '150 seconds', '180 seconds',
'210 seconds', '240 seconds', '270 seconds',
'300 seconds']
VOXLEVEL_LIST = ['Off', '1', '2', '3', '4', '5', '6', '7', '8', '9']
VOXDELAY_LIST = ['1 seconds', '2 seconds',
'3 seconds', '4 seconds', '5 seconds']
X3P_POWER_LEVELS = [chirp_common.PowerLevel('Low', watts=0.5),
chirp_common.PowerLevel('High', watts=2.00)]
_memsize = 0x03F0
@classmethod
def match_model(cls, filedata, filename):
return (len(filedata) == cls._memsize and
b'P310' in filedata[0x03D0:0x03D8])
_ranges = [
(0x0000, 0x03F0),
]
@classmethod
def get_prompts(cls):
rp = chirp_common.RadioPrompts()
rp.experimental = _(
"The X3Plus driver is currently experimental.\n"
"There are no known issues but you should proceed with"
" caution.\n"
"Please save an unedited copy of your first successful\n"
"download to a CHIRP Radio Images (*.img) file.\n")
return rp
def get_features(self):
"""Return a RadioFeatures object for this radio"""
rf = chirp_common.RadioFeatures()
rf.has_settings = True
rf.valid_modes = ['NFM', 'FM'] # 12.5 kHz, 25 kHz.
rf.valid_power_levels = self.X3P_POWER_LEVELS
rf.valid_skips = ['', 'S']
rf.valid_tmodes = ['', 'Tone', 'TSQL', 'DTCS', 'Cross']
rf.valid_duplexes = ['', '-', '+', 'split', 'off']
rf.can_odd_split = True
rf.has_rx_dtcs = True
rf.has_ctone = True
rf.has_cross = True
rf.valid_cross_modes = [
'Tone->Tone',
'DTCS->',
'->DTCS',
'Tone->DTCS',
'DTCS->Tone',
'->Tone',
'DTCS->DTCS']
rf.has_tuning_step = False
rf.has_bank = False
rf.has_name = False
rf.memory_bounds = (1, self.CHANNELS)
rf.valid_bands = [(400000000, 470000000)]
return rf
def process_mmap(self):
"""Process a newly-loaded or downloaded memory map"""
self._memobj = bitwise.parse(self.MEM_FORMAT, self._mmap)
def sync_in(self):
"Initiate a radio-to-PC clone operation"
LOG.debug('Cloning from radio')
status = chirp_common.Status()
status.msg = 'Cloning from radio'
status.cur = 0
status.max = self._memsize
self.status_fn(status)
self._enter_programming_mode()
data = ''
for addr in range(0, self._memsize, self.BLOCK_SIZE):
status.cur = addr + self.BLOCK_SIZE
self.status_fn(status)
block = self._read_block(addr, self.BLOCK_SIZE)
data += block
LOG.debug('Address: %04x', addr)
LOG.debug(util.hexprint(block))
self._exit_programming_mode()
self._mmap = memmap.MemoryMap(data)
self.process_mmap()
def sync_out(self):
"Initiate a PC-to-radio clone operation"
LOG.debug('Upload to radio')
status = chirp_common.Status()
status.msg = 'Uploading to radio'
status.cur = 0
status.max = self._memsize
self.status_fn(status)
self._enter_programming_mode()
for start_addr, end_addr in self._ranges:
for addr in range(start_addr, end_addr, self.BLOCK_SIZE):
status.cur = addr + self.BLOCK_SIZE
self.status_fn(status)
self._write_block(addr, self.BLOCK_SIZE)
self._exit_programming_mode()
def get_raw_memory(self, number):
"""Return a raw string describing the memory at @number"""
return repr(self._memobj.memory[number - 1])
@staticmethod
def _decode_tone(val):
val = int(val)
if val == 16665:
return '', None, None
elif val >= 12000:
return 'DTCS', val - 12000, 'R'
elif val >= 8000:
return 'DTCS', val - 8000, 'N'
return 'Tone', val / 10.0, None
@staticmethod
def _encode_tone(memval, mode, value, pol):
if mode == '':
memval[0].set_raw(0xFF)
memval[1].set_raw(0xFF)
elif mode == 'Tone':
memval.set_value(int(value * 10))
elif mode == 'DTCS':
flag = 0x80 if pol == 'N' else 0xC0
memval.set_value(value)
memval[1].set_bits(flag)
else:
raise Exception('Internal error: invalid mode `%s`' % mode)
def get_memory(self, number):
"""Return a Memory object for the memory at location @number"""
try:
rmem = self._memobj.memory[number - 1]
except KeyError:
raise errors.InvalidMemoryLocation('Unknown channel %s' % number)
if number < 1 or number > self.CHANNELS:
raise errors.InvalidMemoryLocation(
'Channel number must be 1 and %s' % self.CHANNELS)
mem = chirp_common.Memory()
mem.number = number
mem.freq = int(rmem.rxfreq) * 10
# A blank (0 MHz) or 0xFFFFFFFF frequency is considered empty
if mem.freq == 0 or (
rmem.rxfreq.get_raw(asbytes=False) == '\xFF\xFF\xFF\xFF'):
LOG.debug('empty channel %d', number)
mem.freq = 0
mem.empty = True
return mem
if rmem.txfreq.get_raw(asbytes=False) == '\xFF\xFF\xFF\xFF':
mem.duplex = 'off'
mem.offset = 0
elif int(rmem.rxfreq) == int(rmem.txfreq):
mem.duplex = ''
mem.offset = 0
else:
mem.duplex = '-' if int(rmem.rxfreq) > int(rmem.txfreq) else '+'
mem.offset = abs(int(rmem.rxfreq) - int(rmem.txfreq)) * 10
mem.mode = 'NFM' if rmem.narrow else 'FM'
mem.skip = 'S' if rmem.skip else ''
mem.power = self.X3P_POWER_LEVELS[rmem.highpower]
txtone = self._decode_tone(rmem.txtone)
rxtone = self._decode_tone(rmem.rxtone)
chirp_common.split_tone_decode(mem, txtone, rxtone)
mem.extra = RadioSettingGroup('Extra', 'extra')
mem.extra.append(RadioSetting('bcl', 'Busy Channel Lockout',
RadioSettingValueBoolean(
current=(not rmem.bcl))))
mem.extra.append(RadioSetting('scramble', 'Scramble',
RadioSettingValueBoolean(
current=(not rmem.scramble))))
mem.extra.append(RadioSetting('compander', 'Compander',
RadioSettingValueBoolean(
current=(not rmem.compander))))
return mem
def set_memory(self, memory):
"""Set the memory object @memory"""
rmem = self._memobj.memory[memory.number - 1]
if memory.empty:
rmem.set_raw('\xFF' * (rmem.size() // 8))
return
rmem.rxfreq = memory.freq / 10
set_txtone = True
if memory.duplex == 'off':
for i in range(0, 4):
rmem.txfreq[i].set_raw('\xFF')
# If receive only then txtone value should be none
self._encode_tone(rmem.txtone, mode='', value=None, pol=None)
set_txtone = False
elif memory.duplex == 'split':
rmem.txfreq = memory.offset / 10
elif memory.duplex == '+':
rmem.txfreq = (memory.freq + memory.offset) / 10
elif memory.duplex == '-':
rmem.txfreq = (memory.freq - memory.offset) / 10
else:
rmem.txfreq = memory.freq / 10
txtone, rxtone = chirp_common.split_tone_encode(memory)
if set_txtone:
self._encode_tone(rmem.txtone, *txtone)
self._encode_tone(rmem.rxtone, *rxtone)
rmem.narrow = 'N' in memory.mode
rmem.skip = memory.skip == 'S'
for setting in memory.extra:
# NOTE: Only three settings right now, all are inverted
setattr(rmem, setting.get_name(), not int(setting.value))
def get_settings(self):
"""
Return a RadioSettings list containing one or more RadioSettingGroup
or RadioSetting objects. These represent general settings that can
be adjusted on the radio.
"""
cur = self._memobj.settings
basic = RadioSettingGroup('basic', 'Basic Settings')
rs = RadioSetting('squelchlevel', 'Squelch level',
RadioSettingValueInteger(
minval=0, maxval=9,
current=cur.squelchlevel))
basic.append(rs)
rs = RadioSetting('timeouttimer', 'Timeout timer',
RadioSettingValueList(
options=self.TIMEOUTTIMER_LIST,
current=self.TIMEOUTTIMER_LIST[cur.timeouttimer]))
basic.append(rs)
rs = RadioSetting('voiceprompt', 'Voice prompt',
RadioSettingValueList(
options=self.VOICE_LIST,
current=self.VOICE_LIST[cur.voiceprompt]))
basic.append(rs)
rs = RadioSetting('voxlevel', 'Vox level',
RadioSettingValueList(
options=self.VOXLEVEL_LIST,
current=self.VOXLEVEL_LIST[cur.voxlevel]))
basic.append(rs)
rs = RadioSetting('voxdelay', 'VOX delay',
RadioSettingValueList(
options=self.VOXDELAY_LIST,
current=self.VOXDELAY_LIST[cur.voxdelay]))
basic.append(rs)
basic.append(RadioSetting('batterysaver', 'Battery saver',
RadioSettingValueBoolean(
current=cur.batterysaver)))
basic.append(RadioSetting('beep', 'Beep',
RadioSettingValueBoolean(
current=cur.beep)))
return RadioSettings(basic)
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().
"""
for element in settings:
if not isinstance(element, RadioSetting):
self.set_settings(element)
continue
else:
try:
if '.' in element.get_name():
bits = element.get_name().split('.')
obj = self._memobj
for bit in bits[:-1]:
obj = getattr(obj, bit)
setting = bits[-1]
else:
obj = self._memobj.settings
setting = element.get_name()
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
def _write(self, data, timeout=3):
"""
Write data to the serial port and consume the echoed response
The radio echos the data it is sent before replying. Send the
data to the radio, consume the reply, and ensure that the reply
is the same as the data sent.
"""
serial = self.pipe
expected = len(data)
resp = b''
start = datetime.now()
# LOG.debug('WRITE(%02d): %s', expected, util.hexprint(data).rstrip())
serial.write(data)
while True:
if not expected:
break
rbytes = serial.read(expected)
resp += rbytes
expected -= len(rbytes)
if (datetime.now() - start).seconds > timeout:
raise errors.RadioError('Timeout while reading from radio')
if resp != data:
raise errors.RadioError('Echoed response did not match sent data')
def _read(self, length, timeout=3):
"""Read data from the serial port"""
resp = b''
serial = self.pipe
remaining = length
start = datetime.now()
if not remaining:
return resp
while True:
rbytes = serial.read(remaining)
resp += rbytes
remaining -= len(rbytes)
if not remaining:
break
if (datetime.now() - start).seconds > timeout:
raise errors.RadioError('Timeout while reading from radio')
time.sleep(0.1)
# LOG.debug('READ(%02d): %s', length, util.hexprint(resp).rstrip())
return resp
def _read_block(self, block_addr, block_size):
LOG.debug('Reading block %04x...', block_addr)
cmd = struct.pack('>cHb', self.CMD_READ, block_addr,
block_size)
resp_prefix = self.CMD_WRITE + cmd[1:]
try:
msg = ('Failed to write command to radio for block '
'read at %04x' % block_addr)
self._write(cmd)
msg = ('Failed to read response from radio for block '
'read at %04x' % block_addr)
response = self._read(len(cmd) + block_size)
if response[:len(cmd)] != resp_prefix:
raise errors.RadioError('Error reading block %04x, '
'Command not returned.' % (block_addr))
msg = ('Failed to write ACK to radio after block read at '
'%04x' % block_addr)
self._write(self.CMD_ACK)
msg = ('Failed to read ACK from radio after block read at '
'%04x' % block_addr)
ack = self._read(1)
except Exception:
LOG.debug(msg, exc_info=True)
raise errors.RadioError(msg)
if ack != self.CMD_ACK:
raise errors.RadioError('No ACK reading block '
'%04x.' % (block_addr))
return response[len(cmd):]
def _write_block(self, block_addr, block_size):
cmd = struct.pack('>cHb', self.CMD_WRITE, block_addr, block_size)
data = self.get_mmap()[block_addr:block_addr + 8]
LOG.debug('Writing Data:\n%s%s',
util.hexprint(cmd), util.hexprint(data))
try:
self._write(cmd + data)
if self._read(1) != self.CMD_ACK:
raise Exception('No ACK')
except Exception:
msg = 'Failed to send block to radio at %04x' % block_addr
LOG.debug(msg, exc_info=True)
raise errors.RadioError(msg)
def _enter_programming_mode(self):
LOG.debug('Entering programming mode')
try:
msg = 'Error communicating with radio entering programming mode.'
self._write(self.CMD_PROGRAM_ENTER)
time.sleep(0.5)
ack = self._read(1)
if not ack:
raise errors.RadioNoResponse()
elif ack != self.CMD_ACK:
raise errors.RadioError('Radio refused to enter '
'programming mode')
msg = 'Error communicating with radio during identification'
self._write(self.CMD_IDENTIFY)
ident = self._read(8)
if not ident.startswith('SMP558'):
LOG.debug(util.hexprint(ident))
raise errors.RadioError('Radio returned unknown ID string')
msg = ('Error communicating with radio while querying '
'model identifier')
self._write(self.CMD_ACK)
msg = 'Error communicating with radio on final handshake'
ack = self._read(1)
if ack != self.CMD_ACK:
raise errors.RadioError('Radio refused to enter programming '
'mode failed on final handshake.')
except Exception:
LOG.debug(msg, exc_info=True)
raise errors.RadioError(msg)
def _exit_programming_mode(self):
try:
self._write(self.CMD_PROGRAM_EXIT)
except Exception:
msg = 'Radio refused to exit programming mode'
LOG.debug(msg, exc_info=True)
raise errors.RadioError(msg)
LOG.debug('Exited programming mode')
chirp-20261030/chirp/drivers/btech.py 0000644 0001750 0001750 00000645077 15266622247 016313 0 ustar hibby hibby # Copyright 2016-2026:
# * Pavel Milanes CO7WT,
# * Jim Unroe KC9HI,
#
# 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 .
import struct
import logging
from time import sleep
from chirp.drivers import baofeng_common as bfc
from chirp import chirp_common, directory, memmap
from chirp import bitwise, errors, util
from chirp.settings import RadioSettingGroup, RadioSetting, \
RadioSettingValueBoolean, RadioSettingValueList, \
RadioSettingValueString, RadioSettingValueInteger, \
RadioSettingValueFloat, RadioSettings, InvalidValueError
from chirp import settings
LOG = logging.getLogger(__name__)
# A note about the memory in these radios
#
# The real memory of these radios extends to 0x4000
# On read the factory software only uses up to 0x3200
# On write it just uploads the contents up to 0x3100
#
# The mem beyond 0x3200 holds the ID data
MEM_SIZE = 0x4000
BLOCK_SIZE = 0x40
TX_BLOCK_SIZE = 0x10
ACK_CMD = 0x06
MODES = ["FM", "NFM"]
SKIP_VALUES = ["S", ""]
TONES = chirp_common.TONES
DTCS = tuple(sorted(chirp_common.DTCS_CODES + (645,)))
# lists related to "extra" settings
PTTID_LIST = ["OFF", "BOT", "EOT", "BOTH"]
PTTIDCODE_LIST = ["%s" % x for x in range(1, 16)]
OPTSIG_LIST = ["OFF", "DTMF", "2TONE", "5TONE"]
SPMUTE_LIST = ["Tone/DTCS", "Tone/DTCS and Optsig", "Tone/DTCS or Optsig"]
# lists
LIST_AB = ["A", "B"]
LIST_ABC = LIST_AB + ["C"]
LIST_ABCD = LIST_AB + ["C", "D"]
LIST_ANIL = ["3", "4", "5"]
LIST_APO = ["Off"] + ["%s minutes" % x for x in range(30, 330, 30)]
LIST_COLOR4 = ["Off", "Blue", "Orange", "Purple"]
LIST_COLOR8 = ["White", "Red", "Blue", "Green", "Yellow", "Indigo",
"Purple", "Gray"]
LIST_COLOR9 = ["Black"] + LIST_COLOR8
LIST_DTMFST = ["OFF", "Keyboard", "ANI", "Keyboard + ANI"]
LIST_EARPH = ["Off", "Earphone", "Earphone + Speaker"]
LIST_EMCTP = ["TX alarm sound", "TX ANI", "Both"]
LIST_EMCTPX = ["Off"] + LIST_EMCTP
LIST_LANGUA = ["English", "Chinese"]
LIST_MDF = ["Frequency", "Channel", "Name"]
LIST_OFF1TO9 = ["Off"] + ["%s seconds" % x for x in range(1, 10)]
LIST_OFF1TO10 = ["Off"] + ["%s seconds" % x for x in range(1, 11)]
LIST_OFF1TO50 = ["Off"] + ["%s seconds" % x for x in range(1, 51)]
LIST_OFF1TO60 = ["Off"] + ["%s seconds" % x for x in range(1, 61)]
LIST_PONMSG = ["Full", "Message", "Battery voltage"]
LIST_REPM = ["Off", "Carrier", "CTCSS or DCS", "Tone", "DTMF"]
LIST_REPS = ["1000 Hz", "1450 Hz", "1750 Hz", "2100 Hz"]
LIST_REPSW = ["Off", "RX", "TX"]
LIST_RPTDL = ["Off"] + ["%s ms" % x for x in range(1, 11)]
LIST_SCMODE = ["Off", "PTT-SC", "MEM-SC", "PON-SC"]
LIST_SHIFT = ["Off", "+", "-"]
LIST_SKIPTX = ["Off", "Skip 1", "Skip 2"]
STEPS = [2.5, 5.0, 6.25, 10.0, 12.5, 25.0]
LIST_STEP = [str(x) for x in STEPS]
LIST_SYNC = ["Off", "AB", "CD", "AB+CD"]
LIST_SYNCV2 = ["Off", "AB", "AC", "BC", "ABC"]
# the first 12 TMR choices common to all 4-line color display mobile radios
LIST_TMR12 = ["OFF", "M+A", "M+B", "M+C", "M+D", "M+A+B", "M+A+C", "M+A+D",
"M+B+C", "M+B+D", "M+C+D", "M+A+B+C"]
# the 16 choice list for color display mobile radios that correctly implement
# the full 16 TMR choices
LIST_TMR16 = LIST_TMR12 + ["M+A+B+D", "M+A+C+D", "M+B+C+D", "A+B+C+D"]
# the 15 choice list for color mobile radios that are missing the M+A+B+D
# choice in the TMR menu
LIST_TMR15 = LIST_TMR12 + ["M+A+C+D", "M+B+C+D", "A+B+C+D"]
# the 7 TMR choices for the 3-line color display mobile radios
LIST_TMR7 = ["OFF", "M+A", "M+B", "M+C", "M+AB", "M+AC", "M+BC", "M+ABC"]
LIST_TMRTX = ["Track", "Fixed"]
LIST_TOT = ["%s sec" % x for x in range(15, 615, 15)]
LIST_TXDISP = ["Power", "Mic Volume"]
LIST_TXP = ["High", "Low"]
LIST_TXP3 = ["High", "Mid", "Low"]
LIST_SCREV = ["TO (timeout)", "CO (carrier operated)", "SE (search)"]
LIST_VFOMR = ["Frequency", "Channel"]
LIST_VOICE = ["Off"] + LIST_LANGUA
LIST_VOX = ["Off"] + ["%s" % x for x in range(1, 11)]
LIST_VOXT = ["%s seconds" % x for x in range(0, 21)]
LIST_WIDE = ["Wide", "Narrow"]
# lists related to DTMF, 2TONE and 5TONE settings
LIST_5TONE_STANDARDS = ["CCIR1", "CCIR2", "PCCIR", "ZVEI1", "ZVEI2", "ZVEI3",
"PZVEI", "DZVEI", "PDZVEI", "EEA", "EIA", "EURO",
"CCITT", "NATEL", "MODAT", "none"]
LIST_5TONE_STANDARDS_without_none = ["CCIR1", "CCIR2", "PCCIR", "ZVEI1",
"ZVEI2", "ZVEI3",
"PZVEI", "DZVEI", "PDZVEI", "EEA", "EIA",
"EURO", "CCITT", "NATEL", "MODAT"]
LIST_5TONE_STANDARD_PERIODS = ["20", "30", "40", "50", "60", "70", "80", "90",
"100", "110", "120", "130", "140", "150", "160",
"170", "180", "190", "200"]
LIST_5TONE_DIGITS = ["0", "1", "2", "3", "4", "5", "6", "7", "8", "9", "A",
"B", "C", "D", "E", "F"]
LIST_5TONE_DELAY = ["%s ms" % x for x in range(0, 1010, 10)]
LIST_5TONE_RESET = ["%s ms" % x for x in range(100, 8100, 100)]
LIST_5TONE_RESET_COLOR = ["%s ms" % x for x in range(100, 20100, 100)]
LIST_DTMF_SPEED = ["%s ms" % x for x in range(50, 2010, 10)]
LIST_DTMF_DIGITS = ["0", "1", "2", "3", "4", "5", "6", "7", "8", "9", "A", "B",
"C", "D", "#", "*"]
LIST_DTMF_VALUES = [0x0A, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09,
0x0D, 0x0E, 0x0F, 0x00, 0x0C, 0x0B]
LIST_DTMF_SPECIAL_DIGITS = ["*", "#", "A", "B", "C", "D"]
LIST_DTMF_SPECIAL_VALUES = [0x0B, 0x0C, 0x0D, 0x0E, 0x0F, 0x00]
LIST_DTMF_DELAY = ["%s ms" % x for x in range(100, 4100, 100)]
CHARSET_DTMF_DIGITS = "0123456789AaBbCcDd#*"
LIST_2TONE_DEC = ["A-B", "A-C", "A-D",
"B-A", "B-C", "B-D",
"C-A", "C-B", "C-D",
"D-A", "D-B", "D-C"]
LIST_2TONE_RESPONSE = ["None", "Alert", "Transpond", "Alert+Transpond"]
# This is a general serial timeout for all serial read functions.
STIMEOUT = 1.5
# this var controls the verbosity in the debug and by default it's low (False)
# make it True and you will to get a very verbose debug.log
debug = False
# valid chars on the LCD, Note that " " (space) is stored as "\xFF"
VALID_CHARS = chirp_common.CHARSET_ALPHANUMERIC + \
"`{|}!\"#$%&'()*+,-./:;<=>?@[]^_"
GMRS_FREQS1 = [462562500, 462587500, 462612500, 462637500, 462662500,
462687500, 462712500]
GMRS_FREQS2 = [467562500, 467587500, 467612500, 467637500, 467662500,
467687500, 467712500]
GMRS_FREQS3 = [462550000, 462575000, 462600000, 462625000, 462650000,
462675000, 462700000, 462725000]
GMRS_FREQS = GMRS_FREQS1 + GMRS_FREQS2 + GMRS_FREQS3 * 2
# #### ID strings #####################################################
# BTECH UV2501 pre-production units
UV2501pp_fp = b"M2C294"
# BTECH UV2501 pre-production units 2 + and 1st Gen radios
UV2501pp2_fp = b"M29204"
# B-TECH UV-2501 second generation (2G) radios
UV2501G2_fp = b"BTG214"
# B-TECH UV-2501 third generation (3G) radios
UV2501G3_fp = b"BTG324"
# B-TECH UV-2501+220 pre-production units
UV2501_220pp_fp = b"M3C281"
# B-TECH UV-2501+220
UV2501_220_fp = b"M3G201"
# new variant, let's call it Generation 2
UV2501_220G2_fp = b"BTG211"
# B-TECH UV-2501+220 third generation (3G)
UV2501_220G3_fp = b"BTG311"
# B-TECH UV-5001 pre-production units + 1st Gen radios
UV5001pp_fp = b"V19204"
# B-TECH UV-5001 alpha units
UV5001alpha_fp = b"V28204"
# B-TECH UV-5001 second generation (2G) radios
UV5001G2_fp = b"BTG214"
# B-TECH UV-5001 second generation (2G2)
UV5001G22_fp = b"V2G204"
# B-TECH UV-5001 third generation (3G)
UV5001G3_fp = b"BTG304"
# B-TECH UV-25X2
UV25X2_fp = b"UC2012"
# B-TECH UV-25X2 Second Generation (G2)
UV25X2G2_fp = b"UCB282"
# B-TECH UV-25X4
UV25X4_fp = b"UC4014"
# B-TECH UV-25X4 Second Generation (G2)
UV25X4G2_fp = b"UCB284"
# B-TECH UV-50X2
UV50X2_fp = b"UC2M12"
# B-TECH GMRS-50X1
GMRS50X1_fp = b"NC1802"
GMRS50X1_fp1 = b"NC1932"
# B-TECH GMRS-20V2
GMRS20V2_fp = b"WP4204"
GMRS20V2_fp1 = b"VW2112"
GMRS20V2_fp2 = b"VWG728"
# special var to know when we found a BTECH Gen 3
BTECH3 = [UV2501G3_fp, UV2501_220G3_fp, UV5001G3_fp]
# WACCOM Mini-8900
MINI8900_fp = b"M28854"
# QYT KT-UV980
KTUV980_fp = b"H28854"
# QYT KT8900
KT8900_fp = b"M29154"
# New generations KT8900
KT8900_fp1 = b"M2C234"
KT8900_fp2 = b"M2G1F4"
KT8900_fp3 = b"M2G2F4"
KT8900_fp4 = b"M2G304"
KT8900_fp5 = b"M2G314"
KT8900_fp6 = b"M2G424"
KT8900_fp7 = b"M27184"
KT8900_fp8 = b"M2C194"
# KT8900R
KT8900R_fp = b"M3G1F4"
# Second Generation
KT8900R_fp1 = b"M3G214"
# another model
KT8900R_fp2 = b"M3C234"
# another model G4?
KT8900R_fp3 = b"M39164"
# another model
KT8900R_fp4 = b"M3G314"
# AC3MB: another id
KT8900R_fp5 = b"M3B064"
# KT7900D (quad band)
KT7900D_fp = b"VC4004"
KT7900D_fp1 = b"VC4284"
KT7900D_fp2 = b"VC4264"
KT7900D_fp3 = b"VC4114"
KT7900D_fp4 = b"VC4104"
KT7900D_fp5 = b"VC4254"
KT7900D_fp6 = b"VC5264"
KT7900D_fp7 = b"VC9204"
KT7900D_fp8 = b"VC9214"
KT7900D_fp9 = b"VC5302"
KT7900D_fp10 = b"VC5254"
# QB25 (quad band) - a clone of KT7900D
QB25_fp = b"QB-25"
# KT8900D (dual band)
KT8900D_fp = b"VC2002"
KT8900D_fp1 = b"VC8632"
KT8900D_fp2 = b"VC3402"
KT8900D_fp3 = b"VC7062"
KT8900D_fp4 = b"VC3062"
KT8900D_fp5 = b"VC8192"
# LUITON LT-588UV
LT588UV_fp = b"V2G1F4"
# Added by rstrickoff gen 2 id
LT588UV_fp1 = b"V2G214"
# QYT KT-8R (quad band ht)
KT8R_fp = b"MCB264"
KT8R_fp1 = b"MCB284"
KT8R_fp2 = b"MC5264"
KT8R_fp3 = b"MCB254"
# QYT KT5800 (dual band)
KT5800_fp = b"VCB222"
# QYT KT980Plus (dual band)
KT980PLUS_fp = b"VC2002"
KT980PLUS_fp1 = b"VC6042"
# Radioddity DB25-G (gmrs)
DB25G_fp = b"VC6182"
DB25G_fp1 = b"VC7062"
# QYT KT-WP12 and KT-9900
KTWP12_fp = b"WP3094"
# Anysecu WP-9900
WP9900_fp = b"WP3094"
# QYT KT-5000
KT5000_fp = b"WP9114"
# ### MAGICS
# for the Waccom Mini-8900
MSTRING_MINI8900 = b"\x55\xA5\xB5\x45\x55\x45\x4d\x02"
# for the B-TECH UV-2501+220 (including pre production ones)
MSTRING_220 = b"\x55\x20\x15\x12\x12\x01\x4d\x02"
# for the QYT KT8900 & R
MSTRING_KT8900 = b"\x55\x20\x15\x09\x16\x45\x4D\x02"
MSTRING_KT8900R = b"\x55\x20\x15\x09\x25\x01\x4D\x02"
# magic string for all other models
MSTRING = b"\x55\x20\x15\x09\x20\x45\x4d\x02"
# for the QYT KT7900D & KT8900D
MSTRING_KT8900D = b"\x55\x20\x16\x08\x01\xFF\xDC\x02"
# for the BTECH UV-25X2 and UV-50X2
MSTRING_UV25X2 = b"\x55\x20\x16\x12\x28\xFF\xDC\x02"
# for the BTECH UV-25X4
MSTRING_UV25X4 = b"\x55\x20\x16\x11\x18\xFF\xDC\x02"
# for the BTECH GMRS-50X1
MSTRING_GMRS50X1 = b"\x55\x20\x18\x10\x18\xFF\xDC\x02"
# for the BTECH GMRS-20V2
MSTRING_GMRS20V2 = b"\x55\x20\x21\x03\x27\xFF\xDC\x02"
# for the QYT KT-8R
MSTRING_KT8R = b"\x55\x20\x17\x07\x03\xFF\xDC\x02"
# for the QYT KT-WP12, KT-9900 and Anysecu WP-9900
MSTRING_KTWP12 = b"\x55\x20\x18\x11\x02\xFF\xDC\x02"
# for the QYT KT-5000
MSTRING_KT5000 = b"\x55\x20\x21\x01\x23\xFF\xDC\x02"
def _rawrecv(radio, amount):
"""Raw read from the radio device, less intensive way"""
data = b""
try:
data = radio.pipe.read(amount)
# DEBUG
if debug is True:
LOG.debug("<== (%d) bytes:\n\n%s" %
(len(data), util.hexprint(data)))
# fail if no data is received
if len(data) == 0:
raise errors.RadioNoResponse()
# notice on the logs if short
if len(data) < amount:
LOG.warning("Short reading %d bytes from the %d requested." %
(len(data), amount))
except errors.RadioError:
raise
except Exception:
raise errors.RadioError("Error reading data from radio")
return data
def _send(radio, data):
"""Send data to the radio device"""
try:
radio.pipe.write(bytes(data))
# DEBUG
if debug is True:
LOG.debug("==> (%d) bytes:\n\n%s" %
(len(data), util.hexprint(data)))
except:
raise errors.RadioError("Error sending data to radio")
def _make_frame(cmd, addr, length, data=""):
"""Pack the info in the header format"""
frame = b"\x06" + struct.pack(">BHB", ord(cmd), addr, length)
# add the data if set
if len(data) != 0:
frame += data
return frame
def _recv(radio, addr):
"""Get data from the radio all at once to lower syscalls load"""
# Get the full 69 bytes at a time to reduce load
# 1 byte ACK + 4 bytes header + 64 bytes of data (BLOCK_SIZE)
# get the whole block Bug #11851 BLOCKSIZE fix for KT-8900D
block = _rawrecv(radio, radio._block_size + 5)
# basic check Bug #11851 BLOCKSIZE fix for KT-8900D
if len(block) < (radio._block_size + 5):
raise errors.RadioError("Short read of the block 0x%04x" % addr)
# checking for the ack
# occasionally the ack returned may be \x05 so support both
if block[0] not in b"\x06\x05":
raise errors.RadioError("Bad ack from radio in block 0x%04x" % addr)
# header validation
c, a, l = struct.unpack(">BHB", block[1:5])
if a != addr or l != radio._block_size or c != ord("X"):
LOG.debug("Invalid header for block 0x%04x" % addr)
LOG.debug("CMD: %s ADDR: %04x SIZE: %02x" % (c, a, l))
raise errors.RadioError("Invalid header for block 0x%04x:" % addr)
# return the data
return block[5:]
def _do_ident(radio, status, upload=False):
"""Put the radio in PROGRAM mode & identify it"""
# set the serial discipline
radio.pipe.baudrate = 9600
radio.pipe.parity = "N"
# set timeout for download
radio.pipe.timeout = STIMEOUT
radio.pipe.reset_input_buffer()
# send the magic word
_send(radio, radio._magic)
# Now you get a 50 byte reply if all goes well
ident = _rawrecv(radio, 50)
# checking for the ack
if ident[0] != ACK_CMD:
raise errors.RadioError("Bad ack from radio")
# removed 'basic check' since it duplicates the check in _rawrecv()
# check if ident is OK
itis = False
for fp in radio._fileid:
if fp in ident:
# got it!
itis = True
# checking if we are dealing with a Gen 3 BTECH
if radio.VENDOR == "BTECH" and fp in BTECH3:
radio.btech3 = True
break
if itis is False:
LOG.debug("Incorrect model ID, got this:\n\n" + util.hexprint(ident))
raise errors.RadioError("Radio identification failed.")
# pause here for the radio to catch up
sleep(0.1)
# The color display models introduced what earlier versions of CHIRP
# called an "id2". The OEM CPS made a request to read a 16-byte block
# in high memory (0x3DF0). CHIRP attempted to mirror the CPS but the CPS
# appears to pipeline requests rather than synchronizing each transaction
# before issuing the next request acks. This has acks showing up further
# downstream in reads where it doesn't belong. When downloading, the OEM
# CPS actually reads the first block in an apparent attempt to clear the
# read buffer and then starts actual download starting with the first block
# again.
#
# Extensive testing across supported radios showed that the response is
# never used by the driver and upload/download succeeds without requesting
# it. Removing it makes the cloning process the same for all models.
# DEBUG
LOG.info("Positive ident, this is a %s %s" % (radio.VENDOR, radio.MODEL))
return True
def _download(radio):
"""Get the memory map"""
# UI progress
status = chirp_common.Status()
# put radio in program mode and identify it
_do_ident(radio, status)
# reset the progress bar in the UI Bug #11851 BLOCKSIZE fix for KT-8900D
status.max = MEM_SIZE // radio._block_size
status.msg = "Cloning from radio..."
status.cur = 0
radio.status_fn(status)
data = b""
for addr in range(0, MEM_SIZE, radio._block_size):
# sending the read request
_send(radio, _make_frame("S", addr, radio._block_size))
# read
d = _recv(radio, addr)
# aggregate the data
data += d
# UI Update
status.cur = addr // BLOCK_SIZE
status.msg = "Cloning from radio..."
radio.status_fn(status)
# ACK the final block of the download
_send(radio, b"\x06")
return data
def _upload(radio):
"""Upload procedure"""
# The UPLOAD mem is restricted to lower than 0x3100,
# so we will override that here locally
MEM_SIZE = radio.UPLOAD_MEM_SIZE
# UI progress
status = chirp_common.Status()
# put radio in program mode and identify it
_do_ident(radio, status, True)
# get the data to upload to radio
data = radio.get_mmap().get_byte_compatible()
# Reset the UI progress
status.max = MEM_SIZE // TX_BLOCK_SIZE
status.cur = 0
status.msg = "Cloning to radio..."
radio.status_fn(status)
# the fun start here
for addr in range(0, MEM_SIZE, TX_BLOCK_SIZE):
# getting the block of data to send
d = data[addr:addr + TX_BLOCK_SIZE]
# build the frame to send
frame = _make_frame("X", addr, TX_BLOCK_SIZE, d)
# There is no longer a need to strip the first block ack for some
# models.
# send the frame
_send(radio, frame)
# receiving the response
ack = _rawrecv(radio, 1)
# basic check
if len(ack) != 1:
raise errors.RadioError("No ACK when writing block 0x%04x" % addr)
first = ack
if first != b"\x06":
second = _rawrecv(radio, 1)
if second != b"\x06":
raise errors.RadioError(
"Unexpected ACK sequence: %s %s" %
(util.hexprint(first), util.hexprint(second)))
# UI Update
status.cur = addr // TX_BLOCK_SIZE
status.msg = "Cloning to radio..."
radio.status_fn(status)
def model_match(cls, data):
"""Match the opened/downloaded image to the correct version"""
rid = data[0x3f70:0x3f76]
if rid in cls._fileid:
return True
return False
def _decode_ranges(low, high):
"""Unpack the data in the ranges zones in the memmap and return
a tuple with the integer corresponding to the MHz it means"""
ilow = int(low[0]) * 100 + int(low[1]) * 10 + int(low[2])
ihigh = int(high[0]) * 100 + int(high[1]) * 10 + int(high[2])
ilow *= 1000000
ihigh *= 1000000
return (ilow, ihigh)
class BTechMobileCommon(chirp_common.CloneModeRadio,
chirp_common.ExperimentalRadio):
"""BTECH's UV-5001 and alike radios"""
VENDOR = "BTECH"
MODEL = ""
_block_size = BLOCK_SIZE # Bug 11851 should default to BLOCK_SIZE
IDENT = ""
BANDS = 2
COLOR_LCD = False
COLOR_LCD2 = False # BTech Original GMRS Radios
COLOR_LCD3 = False # Color HT Radios
COLOR_LCD4 = False # Waterproof Mobile Radios
NAME_LENGTH = 6
UPLOAD_MEM_SIZE = 0X3100
_power_levels = [chirp_common.PowerLevel("High", watts=25),
chirp_common.PowerLevel("Low", watts=10)]
_vhf_range = (130000000, 180000000)
_220_range = (200000000, 271000000)
_uhf_range = (400000000, 521000000)
_350_range = (350000000, 391000000)
_upper = 199
_magic = MSTRING
_fileid = []
btech3 = False
_gmrs = False
@classmethod
def get_prompts(cls):
rp = chirp_common.RadioPrompts()
rp.experimental = \
('This driver is experimental.\n'
'\n'
'Please keep a copy of your memories with the original software '
'if you treasure them, this driver is new and may contain'
' bugs.\n'
'\n'
)
rp.pre_download = _(
"Follow these instructions to download your info:\n"
"1 - Turn off your radio\n"
"2 - Connect your interface cable\n"
"3 - Turn on your radio\n"
"4 - Do the download of your radio data\n")
rp.pre_upload = _(
"Follow these instructions to upload your info:\n"
"1 - Turn off your radio\n"
"2 - Connect your interface cable\n"
"3 - Turn on your radio\n"
"4 - Do the upload of your radio data\n")
return rp
def get_features(self):
"""Get the radio's features"""
# we will use the following var as global
global POWER_LEVELS
rf = chirp_common.RadioFeatures()
rf.has_settings = True
rf.has_bank = False
rf.has_tuning_step = False
rf.can_odd_split = True
rf.has_name = True
rf.has_offset = True
rf.has_mode = True
rf.has_dtcs = True
rf.has_rx_dtcs = True
rf.has_dtcs_polarity = True
rf.has_ctone = True
rf.has_cross = True
rf.valid_modes = MODES
rf.valid_characters = VALID_CHARS
rf.valid_name_length = self.NAME_LENGTH
rf.valid_duplexes = ["", "-", "+", "split", "off"]
rf.valid_tmodes = ['', 'Tone', 'TSQL', 'DTCS', 'Cross']
rf.valid_cross_modes = [
"Tone->Tone",
"DTCS->",
"->DTCS",
"Tone->DTCS",
"DTCS->Tone",
"->Tone",
"DTCS->DTCS"]
rf.valid_skips = SKIP_VALUES
rf.valid_dtcs_codes = DTCS
rf.valid_tuning_steps = STEPS
rf.memory_bounds = (0, self._upper)
# power levels
POWER_LEVELS = self._power_levels
rf.valid_power_levels = POWER_LEVELS
# normal dual bands. The configured ranges use whole MHz boundaries,
# while supported channel centers can reach 50 kHz above the limit.
rf.valid_bands = [self._vhf_range,
(self._uhf_range[0], self._uhf_range[1] + 50001)]
# 220 band
if self.BANDS == 3 or self.BANDS == 4:
rf.valid_bands.append(self._220_range)
# 350 band
if self.BANDS == 4:
rf.valid_bands.append(self._350_range)
return rf
def sync_in(self):
"""Download from radio"""
data = _download(self)
self._mmap = memmap.MemoryMapBytes(data)
self.process_mmap()
def sync_out(self):
"""Upload to radio"""
try:
_upload(self)
except errors.RadioError:
raise
except Exception as e:
raise errors.RadioError("Error: %s" % e)
def get_raw_memory(self, number):
return repr(self._memobj.memory[number])
def _decode_tone(self, val):
"""Parse the tone data to decode from mem, it returns:
Mode (''|DTCS|Tone), Value (None|###), Polarity (None,N,R)"""
pol = None
if val in [0, 65535]:
return '', None, None
elif val > 0x0258:
a = val / 10.0
return 'Tone', a, pol
else:
if val > 0x69:
index = val - 0x6A
pol = "R"
else:
index = val - 1
pol = "N"
tone = DTCS[index]
return 'DTCS', tone, pol
def _encode_tone(self, memval, mode, val, pol):
"""Parse the tone data to encode from UI to mem"""
if mode == '' or mode is None:
memval.set_raw("\x00\x00")
elif mode == 'Tone':
memval.set_value(val * 10)
elif mode == 'DTCS':
# detect the index in the DTCS list
try:
index = DTCS.index(val)
if pol == "N":
index += 1
else:
index += 0x6A
memval.set_value(index)
except:
msg = "Digital Tone '%d' is not supported" % val
LOG.error(msg)
raise errors.RadioError(msg)
else:
msg = "Internal error: invalid mode '%s'" % mode
LOG.error(msg)
raise errors.InvalidDataError(msg)
def _is_txinh(self, _mem):
raw_tx = b""
for i in range(0, 4):
raw_tx += _mem.txfreq[i].get_raw()
return raw_tx == b"\xFF\xFF\xFF\xFF"
def get_memory(self, number):
"""Get the mem representation from the radio image"""
_mem = self._memobj.memory[number]
_names = self._memobj.names[number]
# Create a high-level memory object to return to the UI
mem = chirp_common.Memory()
# Memory number
mem.number = number
if _mem.get_raw()[:1] == b"\xFF":
mem.empty = True
return mem
# Freq and offset
mem.freq = int(_mem.rxfreq) * 10
# tx freq can be blank
if self._is_txinh(_mem):
# TX freq not set
mem.offset = 0
mem.duplex = "off"
else:
# TX freq set
offset = (int(_mem.txfreq) * 10) - mem.freq
if offset != 0:
if chirp_common.is_split(self.get_features().valid_bands,
mem.freq, int(_mem.txfreq) * 10):
mem.duplex = "split"
mem.offset = int(_mem.txfreq) * 10
elif offset < 0:
mem.offset = abs(offset)
mem.duplex = "-"
elif offset > 0:
mem.offset = offset
mem.duplex = "+"
else:
mem.offset = 0
# name TAG of the channel
mem.name = str(_names.name).rstrip("\xFF").replace("\xFF", " ")
# power
mem.power = POWER_LEVELS[int(_mem.power)]
# wide/narrow
mem.mode = MODES[int(_mem.wide)]
# skip
mem.skip = SKIP_VALUES[_mem.add]
# tone data
rxtone = txtone = None
txtone = self._decode_tone(_mem.txtone)
rxtone = self._decode_tone(_mem.rxtone)
chirp_common.split_tone_decode(mem, txtone, rxtone)
# Extra
mem.extra = RadioSettingGroup("extra", "Extra")
if not self.COLOR_LCD or \
(self.COLOR_LCD and not self.VENDOR == "BTECH"):
scramble = RadioSetting("scramble", "Scramble",
RadioSettingValueBoolean(bool(
_mem.scramble)))
scramble.set_doc(
"Enables voice inversion on this channel. Both radios must "
"use compatible scrambling; this does not provide secure "
"encryption.")
mem.extra.append(scramble)
bcl = RadioSetting("bcl", "Busy channel lockout",
RadioSettingValueBoolean(bool(_mem.bcl)))
bcl.set_doc(
"Prevents transmitting on this channel while the radio is "
"receiving a signal, helping avoid interruptions.")
mem.extra.append(bcl)
pttid = RadioSetting("pttid", "PTT ID",
RadioSettingValueList(PTTID_LIST,
current_index=_mem.pttid))
pttid.set_doc(
"Controls when this channel sends its selected PTT-ID: at the "
"beginning of transmission (BOT), the end (EOT), both, or never.")
mem.extra.append(pttid)
# validating scode
scode = _mem.scode if _mem.scode != 15 else 0
pttidcode = RadioSetting("scode", "PTT ID signal code",
RadioSettingValueList(
PTTIDCODE_LIST,
current_index=scode))
pttidcode.set_doc(
"Selects which of the 15 programmed signalling codes this "
"channel uses for PTT-ID and automatic calling.")
mem.extra.append(pttidcode)
optsig = RadioSetting("optsig", "Optional signaling",
RadioSettingValueList(
OPTSIG_LIST,
current_index=_mem.optsig))
optsig.set_doc(
"Selects whether this channel requires DTMF, 2-tone, or 5-tone "
"optional signalling, or uses none.")
mem.extra.append(optsig)
spmute = RadioSetting("spmute", "Speaker mute",
RadioSettingValueList(
SPMUTE_LIST,
current_index=_mem.spmute))
spmute.set_doc(
"Controls which signals open this channel's speaker: its "
"CTCSS/DCS tone, the selected optional signalling, or the "
"configured combination of both.")
mem.extra.append(spmute)
immutable = []
if self._gmrs:
if self.MODEL == "GMRS-50X1":
if mem.number >= 1 and mem.number <= 30:
if mem.freq in GMRS_FREQS:
GMRS_FREQ = GMRS_FREQS[mem.number - 1]
mem.freq = GMRS_FREQ
immutable = ["empty", "freq"]
if mem.number >= 1 and mem.number <= 7:
mem.duplex = ''
mem.offset = 0
mem.power = POWER_LEVELS[2]
immutable += ["duplex", "offset", "power"]
elif mem.number >= 8 and mem.number <= 14:
mem.duplex = 'off'
mem.offset = 0
immutable += ["duplex", "offset"]
elif mem.number >= 15 and mem.number <= 22:
mem.duplex = ''
mem.offset = 0
immutable += ["duplex", "offset"]
elif mem.number >= 23 and mem.number <= 30:
mem.duplex = '+'
mem.offset = 5000000
immutable += ["duplex", "offset"]
else:
# Not a GMRS channel, so restrict duplex since it will be
# forced to off.
mem.duplex = 'off'
mem.offset = 0
immutable = ["duplex", "offset"]
elif self.MODEL in ["GMRS-20V2", "GMRS-50V2"]:
if mem.freq in GMRS_FREQS:
if mem.freq in GMRS_FREQS1:
# Non-repeater GMRS channels (limit duplex)
mem.duplex = ''
mem.offset = 0
mem.power = POWER_LEVELS[2]
immutable = ["duplex", "offset", "power"]
elif mem.freq in GMRS_FREQS2:
# Non-repeater FRS channels (receive only)
mem.duplex = 'off'
mem.offset = 0
immutable = ["duplex", "offset"]
elif mem.freq in GMRS_FREQS3:
# GMRS repeater channels, always either simplex or
# +5 MHz
if mem.duplex != '+':
mem.duplex = ''
mem.offset = 0
else:
mem.offset = 5000000
else:
# Not a GMRS channel, so restrict duplex since it will be
# forced to off.
mem.duplex = 'off'
mem.offset = 0
immutable = ["duplex", "offset"]
elif self.MODEL == "DB25-G":
if mem.freq in GMRS_FREQS:
if mem.freq in GMRS_FREQS1:
# Non-repeater GMRS channels (limit duplex)
mem.duplex = ''
mem.offset = 0
immutable = ["duplex", "offset"]
elif mem.freq in GMRS_FREQS2:
# Non-repeater FRS channels (receive only)
mem.duplex = 'off'
mem.offset = 0
immutable = ["duplex", "offset"]
elif mem.freq in GMRS_FREQS3:
# GMRS repeater channels, always either simplex or
# +5 MHz
if mem.duplex != '+':
mem.duplex = ''
mem.offset = 0
else:
mem.offset = 5000000
mem.immutable = immutable
return mem
def set_memory(self, mem):
"""Set the memory data in the eeprom img from the UI"""
# get the eprom representation of this channel
_mem = self._memobj.memory[mem.number]
_names = self._memobj.names[mem.number]
mem_was_empty = False
# same method as used in get_memory for determining if mem is empty
# doing this BEFORE overwriting it with new values ...
if _mem.get_raw(asbytes=False)[0] == "\xFF":
LOG.debug("This mem was empty before")
mem_was_empty = True
# if empty memory
if mem.empty:
# the channel itself
_mem.set_raw("\xFF" * 16)
# the name tag
_names.set_raw("\xFF" * 16)
return
if mem_was_empty:
# Zero the whole memory if we're making it unempty for
# the first time
LOG.debug('Zeroing new memory')
_mem.set_raw('\x00' * 16)
# frequency
_mem.rxfreq = mem.freq / 10
# duplex
if mem.duplex == "+":
_mem.txfreq = (mem.freq + mem.offset) / 10
elif mem.duplex == "-":
_mem.txfreq = (mem.freq - mem.offset) / 10
elif mem.duplex == "off":
for i in _mem.txfreq:
i.set_raw("\xFF")
elif mem.duplex == "split":
_mem.txfreq = mem.offset / 10
else:
_mem.txfreq = mem.freq / 10
# tone data
((txmode, txtone, txpol), (rxmode, rxtone, rxpol)) = \
chirp_common.split_tone_encode(mem)
self._encode_tone(_mem.txtone, txmode, txtone, txpol)
self._encode_tone(_mem.rxtone, rxmode, rxtone, rxpol)
# name TAG of the channel
_names.name = mem.name.rstrip(' ').ljust(self.NAME_LENGTH, "\xFF")
# power, # default power level is high
_mem.power = 0 if mem.power is None else POWER_LEVELS.index(mem.power)
# wide/narrow
_mem.wide = MODES.index(mem.mode)
# scan add property
_mem.add = SKIP_VALUES.index(mem.skip)
# resetting unknowns, this have to be set by hand
_mem.unknown0 = 0
_mem.unknown1 = 0
_mem.unknown2 = 0
_mem.unknown3 = 0
_mem.unknown4 = 0
_mem.unknown5 = 0
_mem.unknown6 = 0
def _zero_settings():
_mem.spmute = 0
_mem.optsig = 0
_mem.scramble = 0
_mem.bcl = 0
_mem.pttid = 0
_mem.scode = 0
if self.COLOR_LCD and _mem.scramble:
LOG.info('Resetting scramble bit for BTECH COLOR_LCD variant')
_mem.scramble = 0
# extra settings
if len(mem.extra) > 0:
# there are setting, parse
LOG.debug("Extra-Setting supplied. Setting them.")
# Zero them all first so any not provided by model don't
# stay set
_zero_settings()
for setting in mem.extra:
setattr(_mem, setting.get_name(), setting.value)
else:
if mem.empty:
LOG.debug("New mem is empty.")
else:
LOG.debug("New mem is NOT empty")
# set extra-settings to default ONLY when a previously empty or
# deleted memory was edited to prevent errors such as #4121
if mem_was_empty:
LOG.debug("old mem was empty. Setting default for extras.")
_zero_settings()
return mem
def get_settings(self):
"""Translate the bit in the mem_struct into settings in the UI"""
_mem = self._memobj
basic = RadioSettingGroup("basic", "Basic Settings")
advanced = RadioSettingGroup("advanced", "Advanced Settings")
other = RadioSettingGroup("other", "Other Settings")
work = RadioSettingGroup("work", "Work Mode Settings")
top = RadioSettings(basic, advanced, other, work)
# Basic
if self.COLOR_LCD:
val = min(_mem.settings.tmr, len(self.LIST_TMR) - 1)
rs = RadioSettingValueList(self.LIST_TMR, current_index=val)
tmr = RadioSetting("settings.tmr", "Transceiver multi-receive", rs)
tmr.set_doc(
"Selects which displays are watched in the background in "
"addition to the primary display.")
basic.append(tmr)
else:
tdr = RadioSetting("settings.tdr", "Transceiver dual receive",
RadioSettingValueBoolean(_mem.settings.tdr))
tdr.set_doc(
"Enables Dual Watch so the radio alternates between displays "
"A and B and pauses on a display that receives a signal.")
basic.append(tdr)
val = min(_mem.settings.sql, 9)
rs = RadioSettingValueInteger(0, 9, val)
sql = RadioSetting("settings.sql", "Squelch level", rs)
sql.set_doc(
"Sets how strong a received signal must be before the speaker "
"unmutes. 0 leaves squelch open; higher values reject more weak "
"signals and noise.")
basic.append(sql)
if self.MODEL == "GMRS-50X1" or self.MODEL == "GMRS-50V2":
rs = RadioSettingValueBoolean(_mem.settings.autolk)
autolk = RadioSetting("settings.autolk", "Auto keylock", rs)
basic.append(autolk)
if self.MODEL == "DB25-G":
val = min(_mem.settings.mgain2, 127)
rs = RadioSettingValueInteger(0, 127, val)
mgain2 = RadioSetting("settings.mgain2", "Mic gain", rs)
basic.append(mgain2)
val = min(_mem.settings.tot, len(LIST_TOT) - 1)
rs = RadioSettingValueList(LIST_TOT, current_index=val)
tot = RadioSetting("settings.tot", "Time out timer", rs)
tot.set_doc(
"Limits one continuous transmission to the selected time. This "
"helps prevent overheating and an accidentally stuck "
"transmitter.")
basic.append(tot)
if self.MODEL == "KT-8R":
rs = RadioSettingValueBoolean(_mem.settings.save)
save = RadioSetting("settings.save", "Battery Save", rs)
basic.append(save)
model_list = ["KT-8R", "KT-WP12", "WP-9900"]
if self.MODEL not in model_list:
if self.VENDOR == "BTECH" or self.COLOR_LCD:
val = min(_mem.settings.apo, len(LIST_APO) - 1)
rs = RadioSettingValueList(LIST_APO, current_index=val)
apo = RadioSetting("settings.apo", "Auto power off timer", rs)
apo.set_doc(
"Automatically powers the radio off after the selected "
"period without receiver activity. Off disables "
"automatic power-off.")
basic.append(apo)
else:
val = min(_mem.settings.apo, len(LIST_OFF1TO10) - 1)
rs = RadioSettingValueList(LIST_OFF1TO10, current_index=val)
toa = RadioSetting("settings.apo", "Time out alert timer", rs)
toa.set_doc(
"Sets how many seconds before the Time-out Timer expires "
"that the radio flashes a pre-alert. Off disables the "
"warning.")
basic.append(toa)
val = min(_mem.settings.abr, len(LIST_OFF1TO50) - 1)
rs = RadioSettingValueList(LIST_OFF1TO50, current_index=val)
abr = RadioSetting("settings.abr", "Backlight timer", rs)
if self.MODEL == "KT8900D":
abr.set_doc(
"This menu item is marked unused in the KT8900D manual; "
"changing it is not expected to affect the radio.")
else:
abr.set_doc(
"Sets how long the LCD backlight remains on after radio "
"activity. Off keeps the backlight on continuously.")
basic.append(abr)
rs = RadioSettingValueBoolean(_mem.settings.beep)
beep = RadioSetting("settings.beep", "Key beep", rs)
beep.set_doc(
"Enables the audible confirmation tone for front-panel and "
"microphone keypad presses.")
basic.append(beep)
if self.MODEL == "GMRS-20V2":
val = min(_mem.settings.volume, 58)
rs = RadioSettingValueInteger(0, 58, val)
volume = RadioSetting("settings.volume", "Volume", rs)
basic.append(volume)
if self.MODEL == "KT-WP12" or self.MODEL == "WP-9900":
val = min(_mem.settings.volume, 50)
rs = RadioSettingValueInteger(1, 51, val + 1)
volume = RadioSetting("settings.volume", "Volume", rs)
basic.append(volume)
if self.MODEL == "KT-8R":
rs = RadioSettingValueBoolean(_mem.settings.dsub)
dsub = RadioSetting("settings.dsub", "CTCSS/DCS code display", rs)
basic.append(dsub)
if self.MODEL == "KT-8R" or self.COLOR_LCD4:
rs = RadioSettingValueBoolean(_mem.settings.dtmfst)
dtmfst = RadioSetting("settings.dtmfst", "DTMF side tone", rs)
basic.append(dtmfst)
else:
val = min(_mem.settings.dtmfst, len(LIST_DTMFST) - 1)
rs = RadioSettingValueList(LIST_DTMFST, current_index=val)
dtmfst = RadioSetting("settings.dtmfst", "DTMF side tone", rs)
dtmfst.set_doc(
"Chooses which transmitted DTMF tones are heard through the "
"speaker: manually keyed tones, automatic ANI/PTT-ID tones, "
"both, or neither.")
basic.append(dtmfst)
if not self.COLOR_LCD:
rs = RadioSettingValueBoolean(_mem.settings.prisc)
prisc = RadioSetting("settings.prisc", "Priority scan", rs)
prisc.set_doc(
"Checks the selected Priority Channel periodically while "
"scanning other channels.")
basic.append(prisc)
val = min(_mem.settings.prich, self._upper)
rs = RadioSettingValueInteger(0, self._upper, val)
prich = RadioSetting("settings.prich", "Priority channel", rs)
prich.set_doc(
"Selects the memory channel checked periodically when "
"Priority Scan is enabled.")
basic.append(prich)
val = min(_mem.settings.screv, len(LIST_SCREV) - 1)
rs = RadioSettingValueList(LIST_SCREV, current_index=val)
screv = RadioSetting("settings.screv", "Scan resume method", rs)
screv.set_doc(
"Controls scanning after a signal is found. TO resumes after a "
"short delay, CO resumes when the signal disappears, and SE "
"stops until scanning is started again.")
basic.append(screv)
val = min(_mem.settings.pttlt, 30)
rs = RadioSettingValueInteger(0, 30, val)
pttlt = RadioSetting("settings.pttlt", "PTT transmit delay", rs)
pttlt.set_doc(
"Sets the delay before an automatic PTT-ID is transmitted, "
"allowing the receiving path time to open first.")
basic.append(pttlt)
if self.VENDOR == "BTECH" and self.COLOR_LCD and not \
self.MODEL == "GMRS-20V2":
val = min(_mem.settings.emctp, len(LIST_EMCTPX) - 1)
rs = RadioSettingValueList(LIST_EMCTPX, current_index=val)
emctp = RadioSetting("settings.emctp", "Alarm mode", rs)
basic.append(emctp)
else:
val = min(_mem.settings.emctp, len(LIST_EMCTP) - 1)
rs = RadioSettingValueList(LIST_EMCTP, current_index=val)
emctp = RadioSetting("settings.emctp", "Alarm mode", rs)
emctp.set_doc(
"Chooses whether the alarm sounds locally, transmits the "
"alarm and ANI/PTT-ID, or does both.")
basic.append(emctp)
val = min(_mem.settings.emcch, self._upper)
rs = RadioSettingValueInteger(0, self._upper, val)
emcch = RadioSetting("settings.emcch", "Alarm channel", rs)
emcch.set_doc(
"Selects the memory channel used to transmit the alarm and "
"ANI/PTT-ID when the alarm mode includes transmission.")
basic.append(emcch)
if self.COLOR_LCD:
rs = RadioSettingValueBoolean(_mem.settings.sigbp)
sigbp = RadioSetting("settings.sigbp", "Signal beep", rs)
sigbp.set_doc(
"Sounds a pager alert after matching DTMF, 2-tone, or 5-tone "
"optional signalling is received.")
basic.append(sigbp)
else:
val = min(_mem.settings.ringt, len(LIST_OFF1TO9) - 1)
rs = RadioSettingValueList(LIST_OFF1TO9, current_index=val)
ringt = RadioSetting("settings.ringt", "Ring time", rs)
ringt.set_doc(
"Sets how long the pager alert sounds after matching optional "
"signalling is received. Off disables the alert.")
basic.append(ringt)
val = min(_mem.settings.camdf, len(LIST_MDF) - 1)
rs = RadioSettingValueList(LIST_MDF, current_index=val)
camdf = RadioSetting("settings.camdf", "Display mode A", rs)
camdf.set_doc(
"In Channel (MR) mode, chooses whether display A shows the "
"frequency, channel number, or programmed channel name.")
basic.append(camdf)
val = min(_mem.settings.cbmdf, len(LIST_MDF) - 1)
rs = RadioSettingValueList(LIST_MDF, current_index=val)
cbmdf = RadioSetting("settings.cbmdf", "Display mode B", rs)
cbmdf.set_doc(
"In Channel (MR) mode, chooses whether display B shows the "
"frequency, channel number, or programmed channel name.")
basic.append(cbmdf)
if self.COLOR_LCD:
val = min(_mem.settings.ccmdf, len(LIST_MDF) - 1)
rs = RadioSettingValueList(LIST_MDF, current_index=val)
ccmdf = RadioSetting("settings.ccmdf", "Display mode C", rs)
ccmdf.set_doc(
"In Channel (MR) mode, chooses whether display C shows the "
"frequency, channel number, or programmed channel name.")
basic.append(ccmdf)
if not self.COLOR_LCD4:
val = min(_mem.settings.cdmdf, len(LIST_MDF) - 1)
rs = RadioSettingValueList(LIST_MDF, current_index=val)
cdmdf = RadioSetting("settings.cdmdf", "Display mode D", rs)
cdmdf.set_doc(
"In Channel (MR) mode, chooses whether display D shows "
"the frequency, channel number, or programmed channel "
"name.")
basic.append(cdmdf)
if self.MODEL in ["UV-50X2_G2", "UV-25X2_G2", "UV-25X4_G2"]:
val = min(_mem.settings.langua, len(LIST_VOX) - 1)
rs = RadioSettingValueList(LIST_VOX, current_index=val)
vox = RadioSetting("settings.langua", "VOX", rs)
basic.append(vox)
elif self.MODEL == "GMRS-50V2":
val = min(_mem.settings.vox, len(LIST_VOX) - 1)
rs = RadioSettingValueList(LIST_VOX, current_index=val)
vox = RadioSetting("settings.vox", "VOX", rs)
basic.append(vox)
else:
val = min(_mem.settings.langua, len(LIST_LANGUA) - 1)
rs = RadioSettingValueList(LIST_LANGUA, current_index=val)
langua = RadioSetting("settings.langua", "Language", rs)
langua.set_doc(
"Selects English or Chinese for the radio's on-screen "
"menu prompts.")
basic.append(langua)
if self.MODEL == "KT-8R":
val = min(_mem.settings.voice, len(LIST_VOICE) - 1)
rs = RadioSettingValueList(LIST_VOICE, current_index=val)
voice = RadioSetting("settings.voice", "Voice prompt", rs)
basic.append(voice)
if self.MODEL == "KT-8R" or self.COLOR_LCD4:
val = min(_mem.settings.vox, len(LIST_VOX) - 1)
rs = RadioSettingValueList(LIST_VOX, current_index=val)
vox = RadioSetting("settings.vox", "VOX", rs)
basic.append(vox)
val = min(_mem.settings.voxt, len(LIST_VOX) - 1)
rs = RadioSettingValueList(LIST_VOXT, current_index=val)
voxt = RadioSetting("settings.voxt", "VOX delay time", rs)
basic.append(voxt)
if self.VENDOR == "BTECH":
if self.COLOR_LCD:
if self.MODEL == "GMRS-20V2":
val = min(_mem.settings.sync, len(LIST_SYNCV2) - 1)
rs = RadioSettingValueList(LIST_SYNCV2, current_index=val)
sync = RadioSetting("settings.sync",
"Channel display sync", rs)
basic.append(sync)
else:
val = min(_mem.settings.sync, len(LIST_SYNC) - 1)
rs = RadioSettingValueList(LIST_SYNC, current_index=val)
sync = RadioSetting("settings.sync",
"Channel display sync", rs)
basic.append(sync)
else:
rs = RadioSettingValueBoolean(_mem.settings.sync)
sync = RadioSetting("settings.sync", "A/B channel sync", rs)
basic.append(sync)
if self.COLOR_LCD4:
rs = RadioSettingValueBoolean(_mem.settings.autolock)
autolk = RadioSetting("settings.autolock", "Auto keylock", rs)
basic.append(autolk)
if not self.COLOR_LCD:
val = min(_mem.settings.ponmsg, len(LIST_PONMSG) - 1)
rs = RadioSettingValueList(LIST_PONMSG, current_index=val)
ponmsg = RadioSetting("settings.ponmsg", "Power-on message", rs)
ponmsg.set_doc(
"Chooses the startup display: a full-screen test, the "
"programmed power-on message, or the supply voltage.")
basic.append(ponmsg)
if self.COLOR_LCD and not (self.COLOR_LCD2 or self.COLOR_LCD3 or
self.COLOR_LCD4):
val = min(_mem.settings.mainfc, len(LIST_COLOR9) - 1)
rs = RadioSettingValueList(LIST_COLOR9, current_index=val)
mainfc = RadioSetting("settings.mainfc",
"Main LCD foreground color", rs)
mainfc.set_doc(
"Selects the foreground and text color of the main LCD area.")
basic.append(mainfc)
if not self.COLOR_LCD4:
val = min(_mem.settings.mainbc, len(LIST_COLOR9) - 1)
rs = RadioSettingValueList(LIST_COLOR9, current_index=val)
mainbc = RadioSetting("settings.mainbc",
"Main LCD background color", rs)
mainbc.set_doc(
"Selects the background color of the main LCD area.")
basic.append(mainbc)
val = min(_mem.settings.menufc, len(LIST_COLOR9) - 1)
rs = RadioSettingValueList(LIST_COLOR9, current_index=val)
menufc = RadioSetting("settings.menufc",
"Menu foreground color", rs)
menufc.set_doc(
"Selects the foreground and text color of the on-screen "
"menu.")
basic.append(menufc)
if not self.COLOR_LCD4:
val = min(_mem.settings.menubc, len(LIST_COLOR9) - 1)
rs = RadioSettingValueList(LIST_COLOR9, current_index=val)
menubc = RadioSetting("settings.menubc",
"Menu background color", rs)
menubc.set_doc(
"Selects the background color of the on-screen menu.")
basic.append(menubc)
val = min(_mem.settings.stafc, len(LIST_COLOR9) - 1)
rs = RadioSettingValueList(LIST_COLOR9, current_index=val)
stafc = RadioSetting("settings.stafc",
"Top status foreground color", rs)
stafc.set_doc(
"Selects the foreground and text color of the top status "
"bar.")
basic.append(stafc)
if not self.COLOR_LCD4:
val = min(_mem.settings.stabc, len(LIST_COLOR9) - 1)
rs = RadioSettingValueList(LIST_COLOR9, current_index=val)
stabc = RadioSetting("settings.stabc",
"Top status background color", rs)
stabc.set_doc(
"Selects the background color of the top status bar.")
basic.append(stabc)
val = min(_mem.settings.sigfc, len(LIST_COLOR9) - 1)
rs = RadioSettingValueList(LIST_COLOR9, current_index=val)
sigfc = RadioSetting("settings.sigfc",
"Bottom status foreground color", rs)
sigfc.set_doc(
"Selects the foreground and text color of the bottom "
"signal/status bar.")
basic.append(sigfc)
if not self.COLOR_LCD4:
val = min(_mem.settings.sigbc, len(LIST_COLOR9) - 1)
rs = RadioSettingValueList(LIST_COLOR9, current_index=val)
sigbc = RadioSetting("settings.sigbc",
"Bottom status background color", rs)
sigbc.set_doc(
"Selects the background color of the bottom signal/status "
"bar.")
basic.append(sigbc)
val = min(_mem.settings.rxfc, len(LIST_COLOR9) - 1)
rs = RadioSettingValueList(LIST_COLOR9, current_index=val)
rxfc = RadioSetting("settings.rxfc",
"Receiving character color", rs)
rxfc.set_doc(
"Selects the text color used to mark the display that is "
"receiving.")
basic.append(rxfc)
val = min(_mem.settings.txfc, len(LIST_COLOR9) - 1)
rs = RadioSettingValueList(LIST_COLOR9, current_index=val)
txfc = RadioSetting("settings.txfc",
"Transmitting character color", rs)
txfc.set_doc(
"Selects the text color used to mark the display that is "
"transmitting.")
basic.append(txfc)
if not self.COLOR_LCD4:
val = min(_mem.settings.txdisp, len(LIST_TXDISP) - 1)
rs = RadioSettingValueList(LIST_TXDISP, current_index=val)
txdisp = RadioSetting("settings.txdisp",
"Transmitting status display", rs)
txdisp.set_doc(
"Chooses whether the bottom meter shows transmitter "
"power or microphone audio level while transmitting.")
basic.append(txdisp)
elif self.COLOR_LCD2 or self.COLOR_LCD3:
val = min(_mem.settings.stfc, len(LIST_COLOR8) - 1)
rs = RadioSettingValueList(LIST_COLOR8, current_index=val)
stfc = RadioSetting("settings.stfc", "ST-FC", rs)
basic.append(stfc)
val = min(_mem.settings.mffc, len(LIST_COLOR8) - 1)
rs = RadioSettingValueList(LIST_COLOR8, current_index=val)
mffc = RadioSetting("settings.mffc", "MF-FC", rs)
basic.append(mffc)
val = min(_mem.settings.sfafc, len(LIST_COLOR8) - 1)
rs = RadioSettingValueList(LIST_COLOR8, current_index=val)
sfafc = RadioSetting("settings.sfafc", "SFA-FC", rs)
basic.append(sfafc)
val = min(_mem.settings.sfbfc, len(LIST_COLOR8) - 1)
rs = RadioSettingValueList(LIST_COLOR8, current_index=val)
sfbfc = RadioSetting("settings.sfbfc", "SFB-FC", rs)
basic.append(sfbfc)
val = min(_mem.settings.sfcfc, len(LIST_COLOR8) - 1)
rs = RadioSettingValueList(LIST_COLOR8, current_index=val)
sfcfc = RadioSetting("settings.sfcfc", "SFC-FC", rs)
basic.append(sfcfc)
val = min(_mem.settings.sfdfc, len(LIST_COLOR8) - 1)
rs = RadioSettingValueList(LIST_COLOR8, current_index=val)
sfdfc = RadioSetting("settings.sfdfc", "SFD-FC", rs)
basic.append(sfdfc)
val = min(_mem.settings.subfc, len(LIST_COLOR8) - 1)
rs = RadioSettingValueList(LIST_COLOR8, current_index=val)
subfc = RadioSetting("settings.subfc", "SUB-FC", rs)
basic.append(subfc)
val = min(_mem.settings.fmfc, len(LIST_COLOR8) - 1)
rs = RadioSettingValueList(LIST_COLOR8, current_index=val)
fmfc = RadioSetting("settings.fmfc", "FM-FC", rs)
basic.append(fmfc)
val = min(_mem.settings.sigfc, len(LIST_COLOR8) - 1)
rs = RadioSettingValueList(LIST_COLOR8, current_index=val)
sigfc = RadioSetting("settings.sigfc", "SIG-FC", rs)
basic.append(sigfc)
if not self.MODEL == "KT-8R":
val = min(_mem.settings.modfc, len(LIST_COLOR8) - 1)
rs = RadioSettingValueList(LIST_COLOR8, current_index=val)
modfc = RadioSetting("settings.modfc", "MOD-FC", rs)
basic.append(modfc)
val = min(_mem.settings.menufc, len(LIST_COLOR8) - 1)
rs = RadioSettingValueList(LIST_COLOR8, current_index=val)
menufc = RadioSetting("settings.menufc", "MENUFC", rs)
basic.append(menufc)
val = min(_mem.settings.txfc, len(LIST_COLOR8) - 1)
rs = RadioSettingValueList(LIST_COLOR8, current_index=val)
txfc = RadioSetting("settings.txfc", "TX-FC", rs)
basic.append(txfc)
if self.MODEL == "KT-8R":
val = min(_mem.settings.rxfc, len(LIST_COLOR8) - 1)
rs = RadioSettingValueList(LIST_COLOR8, current_index=val)
rxfc = RadioSetting("settings.rxfc", "RX-FC", rs)
basic.append(rxfc)
if not self.MODEL == "KT-8R" and not self.MODEL == "GMRS-50V2":
val = min(_mem.settings.txdisp, len(LIST_TXDISP) - 1)
rs = RadioSettingValueList(LIST_TXDISP, current_index=val)
txdisp = RadioSetting("settings.txdisp",
"Transmitting status display", rs)
basic.append(txdisp)
elif self.COLOR_LCD4:
val = min(_mem.settings.asfc, len(LIST_COLOR8) - 1)
rs = RadioSettingValueList(LIST_COLOR8, current_index=val)
asfc = RadioSetting("settings.asfc", "Above Stat fore color", rs)
basic.append(asfc)
val = min(_mem.settings.mainfc, len(LIST_COLOR8) - 1)
rs = RadioSettingValueList(LIST_COLOR8, current_index=val)
mainfc = RadioSetting("settings.mainfc", "Main fore color", rs)
basic.append(mainfc)
val = min(_mem.settings.a_fc, len(LIST_COLOR8) - 1)
rs = RadioSettingValueList(LIST_COLOR8, current_index=val)
a_fc = RadioSetting("settings.a_fc", "A - fore color", rs)
basic.append(a_fc)
val = min(_mem.settings.b_fc, len(LIST_COLOR8) - 1)
rs = RadioSettingValueList(LIST_COLOR8, current_index=val)
b_fc = RadioSetting("settings.b_fc", "B - fore color", rs)
basic.append(b_fc)
val = min(_mem.settings.c_fc, len(LIST_COLOR8) - 1)
rs = RadioSettingValueList(LIST_COLOR8, current_index=val)
c_fc = RadioSetting("settings.c_fc", "C - fore color", rs)
basic.append(c_fc)
val = min(_mem.settings.subfc, len(LIST_COLOR8) - 1)
rs = RadioSettingValueList(LIST_COLOR8, current_index=val)
subfc = RadioSetting("settings.subfc", "Sub fore color", rs)
basic.append(subfc)
val = min(_mem.settings.battfc, len(LIST_COLOR8) - 1)
rs = RadioSettingValueList(LIST_COLOR8, current_index=val)
battfc = RadioSetting("settings.battfc", "Battery fore color", rs)
basic.append(battfc)
val = min(_mem.settings.sigfc, len(LIST_COLOR8) - 1)
rs = RadioSettingValueList(LIST_COLOR8, current_index=val)
sigfc = RadioSetting("settings.sigfc", "Signal fore color", rs)
basic.append(sigfc)
val = min(_mem.settings.menufc, len(LIST_COLOR8) - 1)
rs = RadioSettingValueList(LIST_COLOR8, current_index=val)
menufc = RadioSetting("settings.menufc", "Menu fore color", rs)
basic.append(menufc)
val = min(_mem.settings.txfc, len(LIST_COLOR8) - 1)
rs = RadioSettingValueList(LIST_COLOR8, current_index=val)
txfc = RadioSetting("settings.txfc", "TX fore color", rs)
basic.append(txfc)
val = min(_mem.settings.rxfc, len(LIST_COLOR8) - 1)
rs = RadioSettingValueList(LIST_COLOR8, current_index=val)
rxfc = RadioSetting("settings.rxfc", "RX fore color", rs)
basic.append(rxfc)
else:
val = min(_mem.settings.wtled, len(LIST_COLOR4) - 1)
rs = RadioSettingValueList(LIST_COLOR4, current_index=val)
wtled = RadioSetting("settings.wtled",
"Standby backlight Color", rs)
wtled.set_doc(
"Selects the LCD backlight color while the radio is in "
"standby, or turns standby illumination off.")
basic.append(wtled)
val = min(_mem.settings.rxled, len(LIST_COLOR4) - 1)
rs = RadioSettingValueList(LIST_COLOR4, current_index=val)
rxled = RadioSetting("settings.rxled", "RX backlight Color", rs)
rxled.set_doc(
"Selects the LCD backlight color while receiving, or turns "
"receive illumination off.")
basic.append(rxled)
val = min(_mem.settings.txled, len(LIST_COLOR4) - 1)
rs = RadioSettingValueList(LIST_COLOR4, current_index=val)
txled = RadioSetting("settings.txled", "TX backlight Color", rs)
txled.set_doc(
"Selects the LCD backlight color while transmitting, or "
"turns transmit illumination off.")
basic.append(txled)
val = min(_mem.settings.anil, len(LIST_ANIL) - 1)
rs = RadioSettingValueList(LIST_ANIL, current_index=val)
anil = RadioSetting("settings.anil", "ANI length", rs)
anil.set_doc(
"Sets the expected ANI radio-ID length to three, four, or five "
"digits.")
basic.append(anil)
val = min(_mem.settings.reps, len(LIST_REPS) - 1)
rs = RadioSettingValueList(LIST_REPS, current_index=val)
reps = RadioSetting("settings.reps", "Relay signal (tone burst)", rs)
reps.set_doc(
"Selects the tone-burst frequency sent by the CALL key for "
"repeater access.")
basic.append(reps)
if self.COLOR_LCD4:
rs = RadioSettingValueBoolean(_mem.settings.dsub)
dsub = RadioSetting("settings.dsub", "Subtone display", rs)
basic.append(dsub)
if self.MODEL == "GMRS-20V2":
val = min(_mem.settings.repsw, len(LIST_REPSW) - 1)
rs = RadioSettingValueList(LIST_REPSW, current_index=val)
repsw = RadioSetting("settings.repsw", "Repeater SW", rs)
basic.append(repsw)
model_list = ["KT-8R", "KT-WP12", "WP-9900"]
if self.MODEL not in model_list:
val = min(_mem.settings.repm, len(LIST_REPM) - 1)
rs = RadioSettingValueList(LIST_REPM, current_index=val)
repm = RadioSetting("settings.repm", "Relay condition", rs)
repm.set_doc(
"Configures repeater forwarding when two radios are linked: "
"forward after a carrier, matching CTCSS/DCS, matching "
"2-tone/5-tone, or matching DTMF signal.")
basic.append(repm)
if self.VENDOR == "BTECH" or self.COLOR_LCD:
if self.COLOR_LCD:
val = min(_mem.settings.tmrmr, len(LIST_OFF1TO50) - 1)
rs = RadioSettingValueList(LIST_OFF1TO50, current_index=val)
tmrmr = RadioSetting("settings.tmrmr", "TMR return time", rs)
tmrmr.set_doc(
"After activity on a watched display clears, sets how "
"long PTT continues to use that display before returning "
"to the primary display. Off always transmits on the "
"primary display.")
basic.append(tmrmr)
else:
val = min(_mem.settings.tdrab, len(LIST_OFF1TO50) - 1)
rs = RadioSettingValueList(LIST_OFF1TO50, current_index=val)
tdrab = RadioSetting("settings.tdrab", "TDR return time", rs)
basic.append(tdrab)
rs = RadioSettingValueBoolean(_mem.settings.ste)
ste = RadioSetting("settings.ste", "Squelch tail eliminate", rs)
ste.set_doc(
"Transmits a compatible end-of-transmission signal to reduce "
"the squelch burst heard by another radio. Both radios must "
"support and enable this feature.")
basic.append(ste)
if self.COLOR_LCD4:
val = min(_mem.settings.rpste, len(LIST_OFF1TO10) - 1)
rs = RadioSettingValueList(LIST_OFF1TO10, current_index=val)
rpste = RadioSetting("settings.rpste", "Repeater STE", rs)
rpste.set_doc(
"Selects the squelch-tail elimination timing used with a "
"compatible repeater. Use Off when the repeater does not "
"support it.")
basic.append(rpste)
else:
val = min(_mem.settings.rpste, len(LIST_OFF1TO9) - 1)
rs = RadioSettingValueList(LIST_OFF1TO9, current_index=val)
rpste = RadioSetting("settings.rpste", "Repeater STE", rs)
rpste.set_doc(
"Selects the squelch-tail elimination timing used with a "
"compatible repeater. Use Off when the repeater does not "
"support it.")
basic.append(rpste)
if self.COLOR_LCD4:
val = min(_mem.settings.rptdl, len(LIST_OFF1TO60) - 1)
rs = RadioSettingValueList(LIST_OFF1TO60, current_index=val)
rptdl = RadioSetting("settings.rptdl",
"Repeater STE delay", rs)
rptdl.set_doc(
"Sets the delay before repeater squelch-tail elimination "
"is applied. Match it to the repeater, or use Off when it "
"is not required.")
basic.append(rptdl)
else:
val = min(_mem.settings.rptdl, len(LIST_RPTDL) - 1)
rs = RadioSettingValueList(LIST_RPTDL, current_index=val)
rptdl = RadioSetting("settings.rptdl",
"Repeater STE delay", rs)
rptdl.set_doc(
"Sets the delay before repeater squelch-tail elimination "
"is applied. Match it to the repeater, or use Off when it "
"is not required.")
basic.append(rptdl)
if self.MODEL == "DB25-G":
val = min(_mem.settings.mgain, 1)
rs = RadioSettingValueBoolean(val)
mgain = RadioSetting("settings.mgain", "Auto power-on", rs)
basic.append(mgain)
if str(_mem.fingerprint.fp) in BTECH3:
val = min(_mem.settings.mgain, 120)
rs = RadioSettingValueInteger(0, 120, val)
mgain = RadioSetting("settings.mgain", "Mic gain", rs)
basic.append(mgain)
if str(_mem.fingerprint.fp) in BTECH3 or self.COLOR_LCD:
val = min(_mem.settings.dtmfg, 60)
rs = RadioSettingValueInteger(0, 60, val)
dtmfg = RadioSetting("settings.dtmfg", "DTMF gain", rs)
dtmfg.set_doc(
"Adjusts transmitted DTMF tone level from 0 (quietest) to 60 "
"(loudest).")
basic.append(dtmfg)
if self.VENDOR == "BTECH" and self.COLOR_LCD:
val = min(_mem.settings.mgain, 127)
rs = RadioSettingValueInteger(0, 127, val)
mgain = RadioSetting("settings.mgain", "Mic gain", rs)
basic.append(mgain)
val = min(_mem.settings.skiptx, len(LIST_SKIPTX) - 1)
rs = RadioSettingValueList(LIST_SKIPTX, current_index=val)
skiptx = RadioSetting("settings.skiptx", "Skip TX", rs)
basic.append(skiptx)
val = min(_mem.settings.scmode, len(LIST_SCMODE) - 1)
rs = RadioSettingValueList(LIST_SCMODE, current_index=val)
scmode = RadioSetting("settings.scmode", "Scan mode", rs)
basic.append(scmode)
if self.MODEL in ["KT-8R", "UV-25X2", "UV-25X4", "UV-50X2",
"GMRS-20V2", "UV-50X2_G2", "GMRS-50V2",
"UV-25X2_G2", "UV-25X4_G2"]:
val = min(_mem.settings.tmrtx, len(LIST_TMRTX) - 1)
rs = RadioSettingValueList(LIST_TMRTX, current_index=val)
tmrtx = RadioSetting("settings.tmrtx", "TX in multi-standby", rs)
basic.append(tmrtx)
if self.MODEL in ["UV-50X2_G2", "GMRS-50V2", "UV-25X2_G2",
"UV-25X4_G2"]:
val = min(_mem.settings.earpho, len(LIST_EARPH) - 1)
rs = RadioSettingValueList(LIST_EARPH, current_index=val)
earpho = RadioSetting("settings.earpho", "Earphone", rs)
basic.append(earpho)
# Advanced
def _filter(name):
filtered = ""
for char in str(name):
if char in VALID_CHARS:
filtered += char
else:
filtered += " "
return filtered
if self.COLOR_LCD and not (self.COLOR_LCD2 or self.COLOR_LCD3 or
self.COLOR_LCD4):
_msg = self._memobj.poweron_msg
line1 = RadioSetting("poweron_msg.line1",
"Power-on message line 1",
RadioSettingValueString(0, 8, _filter(
_msg.line1)))
line1.set_doc(
"Sets line 1 of the custom message shown when the radio "
"starts.")
advanced.append(line1)
line2 = RadioSetting("poweron_msg.line2",
"Power-on message line 2",
RadioSettingValueString(0, 8, _filter(
_msg.line2)))
line2.set_doc(
"Sets line 2 of the custom message shown when the radio "
"starts.")
advanced.append(line2)
line3 = RadioSetting("poweron_msg.line3",
"Power-on message line 3",
RadioSettingValueString(0, 8, _filter(
_msg.line3)))
line3.set_doc(
"Sets line 3 of the custom message shown when the radio "
"starts.")
advanced.append(line3)
line4 = RadioSetting("poweron_msg.line4",
"Power-on message line 4",
RadioSettingValueString(0, 8, _filter(
_msg.line4)))
line4.set_doc(
"Sets line 4 of the custom message shown when the radio "
"starts.")
advanced.append(line4)
line5 = RadioSetting("poweron_msg.line5",
"Power-on message line 5",
RadioSettingValueString(0, 8, _filter(
_msg.line5)))
line5.set_doc(
"Sets line 5 of the custom message shown when the radio "
"starts.")
advanced.append(line5)
line6 = RadioSetting("poweron_msg.line6",
"Power-on message line 6",
RadioSettingValueString(0, 8, _filter(
_msg.line6)))
line6.set_doc(
"Sets line 6 of the custom message shown when the radio "
"starts.")
advanced.append(line6)
line7 = RadioSetting("poweron_msg.line7",
"Power-on message line 7",
RadioSettingValueString(0, 8, _filter(
_msg.line7)))
line7.set_doc(
"Sets line 7 of the custom message shown when the radio "
"starts.")
advanced.append(line7)
line8 = RadioSetting("poweron_msg.line8", "Static message",
RadioSettingValueString(0, 8, _filter(
_msg.line8)))
line8.set_doc(
"Sets the static text shown on the color display when the "
"radio starts.")
advanced.append(line8)
elif self.COLOR_LCD2 or self.COLOR_LCD3 or self.COLOR_LCD4:
_msg = self._memobj.static_msg
line = RadioSetting("static_msg.line", "Static message",
RadioSettingValueString(0, 16, _filter(
_msg.line)))
advanced.append(line)
else:
_msg = self._memobj.poweron_msg
line1 = RadioSetting("poweron_msg.line1",
"Power-on message line 1",
RadioSettingValueString(0, 6, _filter(
_msg.line1)))
line1.set_doc(
"Sets line 1 of the custom message shown when the radio "
"starts.")
advanced.append(line1)
line2 = RadioSetting("poweron_msg.line2",
"Power-on message line 2",
RadioSettingValueString(0, 6, _filter(
_msg.line2)))
line2.set_doc(
"Sets line 2 of the custom message shown when the radio "
"starts.")
advanced.append(line2)
if self.MODEL in ("UV-2501", "UV-5001"):
vfomren = RadioSetting("settings2.vfomren", "VFO/MR switching",
RadioSettingValueBoolean(
_mem.settings2.vfomren))
advanced.append(vfomren)
reseten = RadioSetting("settings2.reseten", "RESET",
RadioSettingValueBoolean(
_mem.settings2.reseten))
advanced.append(reseten)
menuen = RadioSetting("settings2.menuen", "Menu",
RadioSettingValueBoolean(
_mem.settings2.menuen))
advanced.append(menuen)
# Other
def convert_bytes_to_limit(bytes):
limit = ""
for byte in bytes:
if byte < 10:
limit += chr(byte + 0x30)
else:
break
return limit
if self.MODEL in ["UV-2501+220", "KT8900R"]:
_ranges = self._memobj.ranges220
ranges = "ranges220"
else:
_ranges = self._memobj.ranges
ranges = "ranges"
_limit = convert_bytes_to_limit(_ranges.vhf_low)
val = RadioSettingValueString(0, 3, _limit)
val.set_mutable(False)
vhf_low = RadioSetting("%s.vhf_low" % ranges, "VHF low", val)
vhf_low.set_doc(
"Read-only factory VHF low frequency limit in MHz. It shows the "
"range accepted by this radio image.")
other.append(vhf_low)
_limit = convert_bytes_to_limit(_ranges.vhf_high)
val = RadioSettingValueString(0, 3, _limit)
val.set_mutable(False)
vhf_high = RadioSetting("%s.vhf_high" % ranges, "VHF high", val)
vhf_high.set_doc(
"Read-only factory VHF high frequency limit in MHz. It shows the "
"range accepted by this radio image.")
other.append(vhf_high)
if self.BANDS == 3 or self.BANDS == 4:
_limit = convert_bytes_to_limit(_ranges.vhf2_low)
val = RadioSettingValueString(0, 3, _limit)
val.set_mutable(False)
vhf2_low = RadioSetting("%s.vhf2_low" % ranges, "VHF2 low", val)
other.append(vhf2_low)
_limit = convert_bytes_to_limit(_ranges.vhf2_high)
val = RadioSettingValueString(0, 3, _limit)
val.set_mutable(False)
vhf2_high = RadioSetting("%s.vhf2_high" % ranges, "VHF2 high", val)
other.append(vhf2_high)
_limit = convert_bytes_to_limit(_ranges.uhf_low)
val = RadioSettingValueString(0, 3, _limit)
val.set_mutable(False)
uhf_low = RadioSetting("%s.uhf_low" % ranges, "UHF low", val)
uhf_low.set_doc(
"Read-only factory UHF low frequency limit in MHz. It shows the "
"range accepted by this radio image.")
other.append(uhf_low)
_limit = convert_bytes_to_limit(_ranges.uhf_high)
val = RadioSettingValueString(0, 3, _limit)
val.set_mutable(False)
uhf_high = RadioSetting("%s.uhf_high" % ranges, "UHF high", val)
uhf_high.set_doc(
"Read-only factory UHF high frequency limit in MHz. It shows the "
"range accepted by this radio image.")
other.append(uhf_high)
if self.BANDS == 4:
_limit = convert_bytes_to_limit(_ranges.uhf2_low)
val = RadioSettingValueString(0, 3, _limit)
val.set_mutable(False)
uhf2_low = RadioSetting("%s.uhf2_low" % ranges, "UHF2 low", val)
other.append(uhf2_low)
_limit = convert_bytes_to_limit(_ranges.uhf2_high)
val = RadioSettingValueString(0, 3, _limit)
val.set_mutable(False)
uhf2_high = RadioSetting("%s.uhf2_high" % ranges, "UHF2 high", val)
other.append(uhf2_high)
val = RadioSettingValueString(0, 6, _filter(_mem.fingerprint.fp))
val.set_mutable(False)
fp = RadioSetting("fingerprint.fp", "Fingerprint", val)
fp.set_doc(
"Read-only radio firmware fingerprint used to identify "
"compatible images.")
other.append(fp)
# Work
if self.COLOR_LCD4:
val = min(_mem.settings2.dispab, len(LIST_ABC) - 1)
rs = RadioSettingValueList(LIST_ABC, current_index=val)
dispab = RadioSetting("settings2.dispab", "Display", rs)
dispab.set_doc(
"Selects the active display used for tuning, transmitting, "
"and other front-panel operations.")
work.append(dispab)
elif self.COLOR_LCD:
val = min(_mem.settings2.dispab, len(LIST_ABCD) - 1)
rs = RadioSettingValueList(LIST_ABCD, current_index=val)
dispab = RadioSetting("settings2.dispab", "Display", rs)
dispab.set_doc(
"Selects the active display used for tuning, transmitting, "
"and other front-panel operations.")
work.append(dispab)
else:
val = min(_mem.settings2.dispab, len(LIST_AB) - 1)
rs = RadioSettingValueList(LIST_AB, current_index=val)
dispab = RadioSetting("settings2.dispab", "Display", rs)
dispab.set_doc(
"Selects the active display used for tuning, transmitting, "
"and other front-panel operations.")
work.append(dispab)
if self.COLOR_LCD:
val = min(_mem.settings2.vfomra, len(LIST_VFOMR) - 1)
rs = RadioSettingValueList(LIST_VFOMR, current_index=val)
vfomra = RadioSetting("settings2.vfomra", "VFO/MR A mode", rs)
vfomra.set_doc(
"Selects Frequency (VFO) mode for direct tuning or Channel "
"(MR) mode for programmed memories on display A.")
work.append(vfomra)
val = min(_mem.settings2.vfomrb, len(LIST_VFOMR) - 1)
rs = RadioSettingValueList(LIST_VFOMR, current_index=val)
vfomrb = RadioSetting("settings2.vfomrb", "VFO/MR B mode", rs)
vfomrb.set_doc(
"Selects Frequency (VFO) mode for direct tuning or Channel "
"(MR) mode for programmed memories on display B.")
work.append(vfomrb)
val = min(_mem.settings2.vfomrc, len(LIST_VFOMR) - 1)
rs = RadioSettingValueList(LIST_VFOMR, current_index=val)
vfomrc = RadioSetting("settings2.vfomrc", "VFO/MR C mode", rs)
vfomrc.set_doc(
"Selects Frequency (VFO) mode for direct tuning or Channel "
"(MR) mode for programmed memories on display C.")
work.append(vfomrc)
if not self.COLOR_LCD4:
val = min(_mem.settings2.vfomrd, len(LIST_VFOMR) - 1)
rs = RadioSettingValueList(LIST_VFOMR, current_index=val)
vfomrd = RadioSetting("settings2.vfomrd", "VFO/MR D mode", rs)
vfomrd.set_doc(
"Selects Frequency (VFO) mode for direct tuning or "
"Channel (MR) mode for programmed memories on display D.")
work.append(vfomrd)
else:
val = min(_mem.settings2.vfomr, len(LIST_VFOMR) - 1)
rs = RadioSettingValueList(LIST_VFOMR, current_index=val)
vfomr = RadioSetting("settings2.vfomr", "VFO/MR mode", rs)
vfomr.set_doc(
"Selects Frequency (VFO) mode for direct tuning or Channel "
"(MR) mode for programmed memories.")
work.append(vfomr)
rs = RadioSettingValueBoolean(_mem.settings2.keylock)
keylock = RadioSetting("settings2.keylock", "Keypad lock", rs)
keylock.set_doc(
"Sets the current keypad-lock state. Hold the # key on the "
"microphone to lock or unlock the controls.")
work.append(keylock)
val = min(_mem.settings2.mrcha, self._upper)
rs = RadioSettingValueInteger(0, self._upper, val)
mrcha = RadioSetting("settings2.mrcha", "MR A channel", rs)
mrcha.set_doc(
"Selects the memory channel shown on display A when that display "
"is in Channel (MR) mode.")
work.append(mrcha)
val = min(_mem.settings2.mrchb, self._upper)
rs = RadioSettingValueInteger(0, self._upper, val)
mrchb = RadioSetting("settings2.mrchb", "MR B channel", rs)
mrchb.set_doc(
"Selects the memory channel shown on display B when that display "
"is in Channel (MR) mode.")
work.append(mrchb)
if self.COLOR_LCD:
val = min(_mem.settings2.mrchc, self._upper)
rs = RadioSettingValueInteger(0, self._upper, val)
mrchc = RadioSetting("settings2.mrchc", "MR C channel", rs)
mrchc.set_doc(
"Selects the memory channel shown on display C when that "
"display is in Channel (MR) mode.")
work.append(mrchc)
if not self.COLOR_LCD4:
val = min(_mem.settings2.mrchd, self._upper)
rs = RadioSettingValueInteger(0, self._upper, val)
mrchd = RadioSetting("settings2.mrchd", "MR D channel", rs)
mrchd.set_doc(
"Selects the memory channel shown on display D when that "
"display is in Channel (MR) mode.")
work.append(mrchd)
def my_validate(value):
_vhf_lower = int(convert_bytes_to_limit(_ranges.vhf_low))
_vhf_upper = int(convert_bytes_to_limit(_ranges.vhf_high))
_uhf_lower = int(convert_bytes_to_limit(_ranges.uhf_low))
_uhf_upper = int(convert_bytes_to_limit(_ranges.uhf_high))
if self.BANDS == 3 or self.BANDS == 4:
_vhf2_lower = int(convert_bytes_to_limit(_ranges.vhf2_low))
_vhf2_upper = int(convert_bytes_to_limit(_ranges.vhf2_high))
if self.BANDS == 4:
_uhf2_lower = int(convert_bytes_to_limit(_ranges.uhf2_low))
_uhf2_upper = int(convert_bytes_to_limit(_ranges.uhf2_high))
value = chirp_common.parse_freq(value)
msg = ("Can't be less then %i.0000")
if value > 99000000 and value < _vhf_lower * 1000000:
raise InvalidValueError(msg % (_vhf_lower))
msg = ("Can't be betweeb %i.9975-%i.0000")
if self.BANDS == 2:
if (_vhf_upper + 1) * 1000000 <= value and \
value < _uhf_lower * 1000000:
raise InvalidValueError(msg % (_vhf_upper, _uhf_lower))
if self.BANDS == 3:
if (_vhf_upper + 1) * 1000000 <= value and \
value < _vhf2_lower * 1000000:
raise InvalidValueError(msg % (_vhf_upper, _vhf2_lower))
if (_vhf2_upper + 1) * 1000000 <= value and \
value < _uhf_lower * 1000000:
raise InvalidValueError(msg % (_vhf2_upper, _uhf_lower))
if self.BANDS == 4:
if (_vhf_upper + 1) * 1000000 <= value and \
value < _vhf2_lower * 1000000:
raise InvalidValueError(msg % (_vhf_upper, _vhf2_lower))
if (_vhf2_upper + 1) * 1000000 <= value and \
value < _uhf2_lower * 1000000:
raise InvalidValueError(msg % (_vhf2_upper, _uhf2_lower))
if (_uhf2_upper + 1) * 1000000 <= value and \
value < _uhf_lower * 1000000:
raise InvalidValueError(msg % (_uhf2_upper, _uhf_lower))
msg = ("Can't be greater then %i.9975")
if value > 99000000 and value >= _uhf_upper * 1000000:
raise InvalidValueError(msg % (_uhf_upper))
return chirp_common.format_freq(value)
def apply_freq(setting, obj):
value = chirp_common.parse_freq(str(setting.value)) / 10
for i in range(7, -1, -1):
obj.freq[i] = value % 10
value /= 10
_vhf_upper = (convert_bytes_to_limit(_ranges.vhf_high))
_uhf_lower = (convert_bytes_to_limit(_ranges.uhf_low))
val1a = RadioSettingValueString(0, 10,
bfc.bcd_decode_freq(_mem.vfo.a.freq))
val1a.set_validate_callback(my_validate)
vfoafreq = RadioSetting("vfo.a.freq", "VFO A frequency", val1a)
vfoafreq.set_doc(
"Sets display A's receive frequency in Frequency (VFO) mode.")
vfoafreq.set_apply_callback(apply_freq, _mem.vfo.a)
work.append(vfoafreq)
val = bfc.bcd_decode_freq(_mem.vfo.b.freq)
if self.MODEL in ["GMRS-50V2", "GMRS-50X1"]:
if val[:3] > _vhf_upper and val[:3] < _uhf_lower:
val = "462.562500"
val1b = RadioSettingValueString(0, 10, val)
val1b.set_validate_callback(my_validate)
vfobfreq = RadioSetting("vfo.b.freq", "VFO B frequency", val1b)
vfobfreq.set_doc(
"Sets display B's receive frequency in Frequency (VFO) mode.")
vfobfreq.set_apply_callback(apply_freq, _mem.vfo.b)
work.append(vfobfreq)
if self.COLOR_LCD:
val1c = RadioSettingValueString(0, 10,
bfc.bcd_decode_freq(
_mem.vfo.c.freq))
val1c.set_validate_callback(my_validate)
vfocfreq = RadioSetting("vfo.c.freq", "VFO C frequency", val1c)
vfocfreq.set_doc(
"Sets display C's receive frequency in Frequency (VFO) "
"mode.")
vfocfreq.set_apply_callback(apply_freq, _mem.vfo.c)
work.append(vfocfreq)
if not self.COLOR_LCD4:
val = bfc.bcd_decode_freq(_mem.vfo.d.freq)
if self.MODEL in ["GMRS-50V2", "GMRS-50X1"]:
if val[:3] > _vhf_upper and val[:3] < _uhf_lower:
val = "462.562500"
val1d = RadioSettingValueString(0, 10, val)
val1d.set_validate_callback(my_validate)
vfodfreq = RadioSetting("vfo.d.freq", "VFO D frequency", val1d)
vfodfreq.set_doc(
"Sets display D's receive frequency in Frequency (VFO) "
"mode.")
vfodfreq.set_apply_callback(apply_freq, _mem.vfo.d)
work.append(vfodfreq)
if not self.MODEL == "GMRS-50X1":
val = min(_mem.vfo.a.shiftd, len(LIST_SHIFT) - 1)
rs = RadioSettingValueList(LIST_SHIFT, current_index=val)
vfoashiftd = RadioSetting("vfo.a.shiftd", "VFO A shift", rs)
vfoashiftd.set_doc(
"Sets VFO A's repeater shift direction. + transmits above "
"the receive frequency, - below it, and Off uses the receive "
"frequency.")
work.append(vfoashiftd)
val = min(_mem.vfo.b.shiftd, len(LIST_SHIFT) - 1)
rs = RadioSettingValueList(LIST_SHIFT, current_index=val)
vfobshiftd = RadioSetting("vfo.b.shiftd", "VFO B shift", rs)
vfobshiftd.set_doc(
"Sets VFO B's repeater shift direction. + transmits above "
"the receive frequency, - below it, and Off uses the receive "
"frequency.")
work.append(vfobshiftd)
if self.COLOR_LCD:
val = min(_mem.vfo.c.shiftd, len(LIST_SHIFT) - 1)
rs = RadioSettingValueList(LIST_SHIFT, current_index=val)
vfocshiftd = RadioSetting("vfo.c.shiftd", "VFO C shift", rs)
vfocshiftd.set_doc(
"Sets VFO C's repeater shift direction. + transmits "
"above the receive frequency, - below it, and Off uses "
"the receive frequency.")
work.append(vfocshiftd)
if not self.COLOR_LCD4:
val = min(_mem.vfo.d.shiftd, len(LIST_SHIFT) - 1)
rs = RadioSettingValueList(LIST_SHIFT, current_index=val)
vfodshiftd = RadioSetting("vfo.d.shiftd",
"VFO D shift", rs)
vfodshiftd.set_doc(
"Sets VFO D's repeater shift direction. + transmits "
"above the receive frequency, - below it, and Off "
"uses the receive frequency.")
work.append(vfodshiftd)
def convert_bytes_to_offset(bytes):
real_offset = 0
for byte in bytes:
real_offset = (real_offset * 10) + byte
return chirp_common.format_freq(real_offset * 1000)
def apply_offset(setting, obj):
value = chirp_common.parse_freq(str(setting.value)) / 1000
for i in range(5, -1, -1):
obj.offset[i] = value % 10
value /= 10
if not self.MODEL == "GMRS-50X1":
if self.COLOR_LCD:
val1a = RadioSettingValueString(0, 10, convert_bytes_to_offset(
_mem.vfo.a.offset))
vfoaoffset = RadioSetting("vfo.a.offset",
"VFO A offset (0.000-999.999)",
val1a)
vfoaoffset.set_doc(
"Sets the difference in MHz between VFO A's receive and "
"transmit frequencies. Shift determines whether it is "
"added or subtracted.")
vfoaoffset.set_apply_callback(apply_offset, _mem.vfo.a)
work.append(vfoaoffset)
val1b = RadioSettingValueString(0, 10, convert_bytes_to_offset(
_mem.vfo.b.offset))
vfoboffset = RadioSetting("vfo.b.offset",
"VFO B offset (0.000-999.999)",
val1b)
vfoboffset.set_doc(
"Sets the difference in MHz between VFO B's receive and "
"transmit frequencies. Shift determines whether it is "
"added or subtracted.")
vfoboffset.set_apply_callback(apply_offset, _mem.vfo.b)
work.append(vfoboffset)
val1c = RadioSettingValueString(0, 10, convert_bytes_to_offset(
_mem.vfo.c.offset))
vfocoffset = RadioSetting("vfo.c.offset",
"VFO C offset (0.000-999.999)",
val1c)
vfocoffset.set_doc(
"Sets the difference in MHz between VFO C's receive and "
"transmit frequencies. Shift determines whether it is "
"added or subtracted.")
vfocoffset.set_apply_callback(apply_offset, _mem.vfo.c)
work.append(vfocoffset)
if not self.COLOR_LCD4:
val1d = RadioSettingValueString(0, 10,
convert_bytes_to_offset(
_mem.vfo.d.offset))
vfodoffset = RadioSetting("vfo.d.offset",
"VFO D offset (0.000-999.999)",
val1d)
vfodoffset.set_doc(
"Sets the difference in MHz between VFO D's receive "
"and transmit frequencies. Shift determines whether "
"it is added or subtracted.")
vfodoffset.set_apply_callback(apply_offset, _mem.vfo.d)
work.append(vfodoffset)
else:
val1a = RadioSettingValueString(0, 10, convert_bytes_to_offset(
_mem.vfo.a.offset))
vfoaoffset = RadioSetting("vfo.a.offset",
"VFO A offset (0.000-99.999)", val1a)
vfoaoffset.set_doc(
"Sets the difference in MHz between VFO A's receive and "
"transmit frequencies. Shift determines whether it is "
"added or subtracted.")
vfoaoffset.set_apply_callback(apply_offset, _mem.vfo.a)
work.append(vfoaoffset)
val1b = RadioSettingValueString(0, 10, convert_bytes_to_offset(
_mem.vfo.b.offset))
vfoboffset = RadioSetting("vfo.b.offset",
"VFO B offset (0.000-99.999)", val1b)
vfoboffset.set_doc(
"Sets the difference in MHz between VFO B's receive and "
"transmit frequencies. Shift determines whether it is "
"added or subtracted.")
vfoboffset.set_apply_callback(apply_offset, _mem.vfo.b)
work.append(vfoboffset)
if not self.MODEL == "GMRS-50X1":
val = min(_mem.vfo.a.power, len(LIST_TXP) - 1)
rs = RadioSettingValueList(LIST_TXP, current_index=val)
vfoatxp = RadioSetting("vfo.a.power", "VFO A power", rs)
vfoatxp.set_doc(
"Selects VFO A's transmitter power. Use the lowest power "
"that provides reliable communication.")
work.append(vfoatxp)
val = min(_mem.vfo.b.power, len(LIST_TXP) - 1)
rs = RadioSettingValueList(LIST_TXP, current_index=val)
vfobtxp = RadioSetting("vfo.b.power", "VFO B power", rs)
vfobtxp.set_doc(
"Selects VFO B's transmitter power. Use the lowest power "
"that provides reliable communication.")
work.append(vfobtxp)
if self.COLOR_LCD:
val = min(_mem.vfo.c.power, len(LIST_TXP) - 1)
rs = RadioSettingValueList(LIST_TXP, current_index=val)
vfoctxp = RadioSetting("vfo.c.power", "VFO C power", rs)
vfoctxp.set_doc(
"Selects VFO C's transmitter power. Use the lowest power "
"that provides reliable communication.")
work.append(vfoctxp)
if not self.COLOR_LCD4:
val = min(_mem.vfo.d.power, len(LIST_TXP) - 1)
rs = RadioSettingValueList(LIST_TXP, current_index=val)
vfodtxp = RadioSetting("vfo.d.power", "VFO D power", rs)
vfodtxp.set_doc(
"Selects VFO D's transmitter power. Use the lowest "
"power that provides reliable communication.")
work.append(vfodtxp)
if not self.MODEL == "GMRS-50X1":
val = min(_mem.vfo.a.wide, len(LIST_WIDE) - 1)
rs = RadioSettingValueList(LIST_WIDE, current_index=val)
vfoawide = RadioSetting("vfo.a.wide", "VFO A bandwidth", rs)
vfoawide.set_doc(
"Selects wide or narrow FM for VFO A. Use the bandwidth "
"required by the channel plan.")
work.append(vfoawide)
val = min(_mem.vfo.b.wide, len(LIST_WIDE) - 1)
rs = RadioSettingValueList(LIST_WIDE, current_index=val)
vfobwide = RadioSetting("vfo.b.wide", "VFO B bandwidth", rs)
vfobwide.set_doc(
"Selects wide or narrow FM for VFO B. Use the bandwidth "
"required by the channel plan.")
work.append(vfobwide)
if self.COLOR_LCD:
val = min(_mem.vfo.c.wide, len(LIST_WIDE) - 1)
rs = RadioSettingValueList(LIST_WIDE, current_index=val)
vfocwide = RadioSetting("vfo.c.wide", "VFO C bandwidth", rs)
vfocwide.set_doc(
"Selects wide or narrow FM for VFO C. Use the bandwidth "
"required by the channel plan.")
work.append(vfocwide)
if not self.COLOR_LCD4:
val = min(_mem.vfo.d.wide, len(LIST_WIDE) - 1)
rs = RadioSettingValueList(LIST_WIDE, current_index=val)
vfodwide = RadioSetting("vfo.d.wide",
"VFO D bandwidth", rs)
vfodwide.set_doc(
"Selects wide or narrow FM for VFO D. Use the "
"bandwidth required by the channel plan.")
work.append(vfodwide)
val = min(_mem.vfo.a.step, len(LIST_STEP) - 1)
rs = RadioSettingValueList(LIST_STEP, current_index=val)
vfoastep = RadioSetting("vfo.a.step", "VFO A step", rs)
vfoastep.set_doc(
"Sets how far VFO A moves for each tuning step and while "
"scanning in Frequency mode.")
work.append(vfoastep)
val = min(_mem.vfo.b.step, len(LIST_STEP) - 1)
rs = RadioSettingValueList(LIST_STEP, current_index=val)
vfobstep = RadioSetting("vfo.b.step", "VFO B step", rs)
vfobstep.set_doc(
"Sets how far VFO B moves for each tuning step and while "
"scanning in Frequency mode.")
work.append(vfobstep)
if self.COLOR_LCD:
val = min(_mem.vfo.c.step, len(LIST_STEP) - 1)
rs = RadioSettingValueList(LIST_STEP, current_index=val)
vfocstep = RadioSetting("vfo.c.step", "VFO C step", rs)
vfocstep.set_doc(
"Sets how far VFO C moves for each tuning step and while "
"scanning in Frequency mode.")
work.append(vfocstep)
if not self.COLOR_LCD4:
val = min(_mem.vfo.d.step, len(LIST_STEP) - 1)
rs = RadioSettingValueList(LIST_STEP, current_index=val)
vfodstep = RadioSetting("vfo.d.step", "VFO D step", rs)
vfodstep.set_doc(
"Sets how far VFO D moves for each tuning step and while "
"scanning in Frequency mode.")
work.append(vfodstep)
val = min(_mem.vfo.a.optsig, len(OPTSIG_LIST) - 1)
rs = RadioSettingValueList(OPTSIG_LIST, current_index=val)
vfoaoptsig = RadioSetting("vfo.a.optsig", "VFO A optional signal", rs)
vfoaoptsig.set_doc(
"Selects whether VFO A requires DTMF, 2-tone, or 5-tone optional "
"signalling, or uses none.")
work.append(vfoaoptsig)
val = min(_mem.vfo.b.optsig, len(OPTSIG_LIST) - 1)
rs = RadioSettingValueList(OPTSIG_LIST, current_index=val)
vfoboptsig = RadioSetting("vfo.b.optsig", "VFO B optional signal", rs)
vfoboptsig.set_doc(
"Selects whether VFO B requires DTMF, 2-tone, or 5-tone optional "
"signalling, or uses none.")
work.append(vfoboptsig)
if self.COLOR_LCD:
val = min(_mem.vfo.c.optsig, len(OPTSIG_LIST) - 1)
rs = RadioSettingValueList(OPTSIG_LIST, current_index=val)
vfocoptsig = RadioSetting("vfo.c.optsig",
"VFO C optional signal", rs)
vfocoptsig.set_doc(
"Selects whether VFO C requires DTMF, 2-tone, or 5-tone "
"optional signalling, or uses none.")
work.append(vfocoptsig)
if not self.COLOR_LCD4:
val = min(_mem.vfo.d.optsig, len(OPTSIG_LIST) - 1)
rs = RadioSettingValueList(OPTSIG_LIST, current_index=val)
vfodoptsig = RadioSetting("vfo.d.optsig",
"VFO D optional signal", rs)
vfodoptsig.set_doc(
"Selects whether VFO D requires DTMF, 2-tone, or 5-tone "
"optional signalling, or uses none.")
work.append(vfodoptsig)
val = min(_mem.vfo.a.spmute, len(SPMUTE_LIST) - 1)
rs = RadioSettingValueList(SPMUTE_LIST, current_index=val)
vfoaspmute = RadioSetting("vfo.a.spmute", "VFO A speaker mute", rs)
vfoaspmute.set_doc(
"Controls whether VFO A's speaker opens for its CTCSS/DCS tone, "
"the optional signal, or the configured combination of both.")
work.append(vfoaspmute)
val = min(_mem.vfo.b.spmute, len(SPMUTE_LIST) - 1)
rs = RadioSettingValueList(SPMUTE_LIST, current_index=val)
vfobspmute = RadioSetting("vfo.b.spmute", "VFO B speaker mute", rs)
vfobspmute.set_doc(
"Controls whether VFO B's speaker opens for its CTCSS/DCS tone, "
"the optional signal, or the configured combination of both.")
work.append(vfobspmute)
if self.COLOR_LCD:
val = min(_mem.vfo.c.spmute, len(SPMUTE_LIST) - 1)
rs = RadioSettingValueList(SPMUTE_LIST, current_index=val)
vfocspmute = RadioSetting("vfo.c.spmute", "VFO C speaker mute", rs)
vfocspmute.set_doc(
"Controls whether VFO C's speaker opens for its CTCSS/DCS "
"tone, the optional signal, or the configured combination "
"of both.")
work.append(vfocspmute)
if not self.COLOR_LCD4:
val = min(_mem.vfo.d.spmute, len(SPMUTE_LIST) - 1)
rs = RadioSettingValueList(SPMUTE_LIST, current_index=val)
vfodspmute = RadioSetting("vfo.d.spmute",
"VFO D speaker mute", rs)
vfodspmute.set_doc(
"Controls whether VFO D's speaker opens for its "
"CTCSS/DCS tone, the optional signal, or the configured "
"combination of both.")
work.append(vfodspmute)
if not self.COLOR_LCD or \
(self.COLOR_LCD and not self.VENDOR == "BTECH"):
rs = RadioSettingValueBoolean(_mem.vfo.a.scramble)
vfoascr = RadioSetting("vfo.a.scramble", "VFO A scramble", rs)
vfoascr.set_doc(
"Enables voice inversion on VFO A. Both radios must use "
"compatible scrambling; this is not secure encryption.")
work.append(vfoascr)
rs = RadioSettingValueBoolean(_mem.vfo.b.scramble)
vfobscr = RadioSetting("vfo.b.scramble", "VFO B scramble", rs)
vfobscr.set_doc(
"Enables voice inversion on VFO B. Both radios must use "
"compatible scrambling; this is not secure encryption.")
work.append(vfobscr)
if self.COLOR_LCD and not self.VENDOR == "BTECH":
rs = RadioSettingValueBoolean(_mem.vfo.c.scramble)
vfocscr = RadioSetting("vfo.c.scramble", "VFO C scramble", rs)
vfocscr.set_doc(
"Enables voice inversion on VFO C. Both radios must use "
"compatible scrambling; this is not secure encryption.")
work.append(vfocscr)
rs = RadioSettingValueBoolean(_mem.vfo.d.scramble)
vfodscr = RadioSetting("vfo.d.scramble", "VFO D scramble", rs)
vfodscr.set_doc(
"Enables voice inversion on VFO D. Both radios must use "
"compatible scrambling; this is not secure encryption.")
work.append(vfodscr)
if not self.MODEL == "GMRS-50X1":
val = min(_mem.vfo.a.scode, len(PTTIDCODE_LIST) - 1)
rs = RadioSettingValueList(PTTIDCODE_LIST, current_index=val)
vfoascode = RadioSetting("vfo.a.scode", "VFO A PTT-ID", rs)
vfoascode.set_doc(
"Selects which of the 15 programmed signalling codes VFO A "
"uses for PTT-ID and automatic calling.")
work.append(vfoascode)
val = min(_mem.vfo.b.scode, len(PTTIDCODE_LIST) - 1)
rs = RadioSettingValueList(PTTIDCODE_LIST, current_index=val)
vfobscode = RadioSetting("vfo.b.scode", "VFO B PTT-ID", rs)
vfobscode.set_doc(
"Selects which of the 15 programmed signalling codes VFO B "
"uses for PTT-ID and automatic calling.")
work.append(vfobscode)
if self.COLOR_LCD:
val = min(_mem.vfo.c.scode, len(PTTIDCODE_LIST) - 1)
rs = RadioSettingValueList(PTTIDCODE_LIST, current_index=val)
vfocscode = RadioSetting("vfo.c.scode", "VFO C PTT-ID", rs)
vfocscode.set_doc(
"Selects which of the 15 programmed signalling codes VFO "
"C uses for PTT-ID and automatic calling.")
work.append(vfocscode)
if not self.COLOR_LCD4:
val = min(_mem.vfo.d.scode, len(PTTIDCODE_LIST) - 1)
rs = RadioSettingValueList(PTTIDCODE_LIST,
current_index=val)
vfodscode = RadioSetting("vfo.d.scode", "VFO D PTT-ID", rs)
vfodscode.set_doc(
"Selects which of the 15 programmed signalling codes "
"VFO D uses for PTT-ID and automatic calling.")
work.append(vfodscode)
if not self.MODEL == "GMRS-50X1":
val = min(_mem.settings.pttid, len(PTTID_LIST) - 1)
rs = RadioSettingValueList(PTTID_LIST, current_index=val)
pttid = RadioSetting("settings.pttid", "PTT ID", rs)
pttid.set_doc(
"Controls when the selected PTT-ID is sent: at the beginning "
"of transmission (BOT), the end (EOT), both, or never.")
work.append(pttid)
if not self.COLOR_LCD:
# FM presets
fm_presets = RadioSettingGroup("fm_presets", "FM Presets")
top.append(fm_presets)
def fm_validate(value):
if value == 0:
return chirp_common.format_freq(value)
if not (87.5 <= value and value <= 108.0): # 87.5-108 MHz
msg = ("FM-Preset-Frequency: " +
"Must be between 87.5 and 108 MHz")
raise InvalidValueError(msg)
return value
def apply_fm_preset_name(setting, obj):
valstring = str(setting.value)
for i in range(0, 6):
if valstring[i] in VALID_CHARS:
obj[i] = valstring[i]
else:
obj[i] = '0xff'
def apply_fm_freq(setting, obj):
value = chirp_common.parse_freq(str(setting.value)) / 10
for i in range(7, -1, -1):
obj.freq[i] = value % 10
value /= 10
_presets = self._memobj.fm_radio_preset
i = 1
for preset in _presets:
line = RadioSetting("fm_presets_" + str(i),
"Station name " + str(i),
RadioSettingValueString(0, 6, _filter(
preset.broadcast_station_name)))
line.set_doc(
"Sets the 6-character name shown for broadcast FM preset "
"%s." % i)
line.set_apply_callback(apply_fm_preset_name,
preset.broadcast_station_name)
try:
presetval = bfc.bcd_decode_freq(preset.freq)
except settings.InternalError:
presetval = ''
val = RadioSettingValueFloat(0, 108, presetval)
fmfreq = RadioSetting("fm_presets_" + str(i) + "_freq",
"Frequency " + str(i), val)
fmfreq.set_doc(
"Sets broadcast FM preset %s's frequency. Use "
"87.5-108.0 MHz, or 0 to leave it unused." % i)
val.set_validate_callback(fm_validate)
fmfreq.set_apply_callback(apply_fm_freq, preset)
fm_presets.append(line)
fm_presets.append(fmfreq)
i = i + 1
# DTMF-Setting
dtmf_enc_settings = RadioSettingGroup("dtmf_enc_settings",
"DTMF Encoding Settings")
dtmf_dec_settings = RadioSettingGroup("dtmf_dec_settings",
"DTMF Decoding Settings")
top.append(dtmf_enc_settings)
top.append(dtmf_dec_settings)
txdisable = RadioSetting("dtmf_settings.txdisable",
"TX-Disable",
RadioSettingValueBoolean(
_mem.dtmf_settings.txdisable))
txdisable.set_doc(
"Stores whether DTMF remote control has disabled transmission. "
"Normally leave this off; a remote stun or kill command can set "
"it.")
dtmf_enc_settings.append(txdisable)
rxdisable = RadioSetting("dtmf_settings.rxdisable",
"RX-Disable",
RadioSettingValueBoolean(
_mem.dtmf_settings.rxdisable))
rxdisable.set_doc(
"Stores whether DTMF remote control has disabled reception. "
"Normally leave this off; a remote kill command can set it.")
dtmf_enc_settings.append(rxdisable)
if _mem.dtmf_settings.dtmfspeed_on > 0x0F:
val = 0x03
else:
val = _mem.dtmf_settings.dtmfspeed_on
dtmfspeed_on = RadioSetting(
"dtmf_settings.dtmfspeed_on",
"DTMF Speed (On Time)",
RadioSettingValueList(LIST_DTMF_SPEED,
current_index=val))
dtmfspeed_on.set_doc(
"Sets how long each DTMF tone is transmitted. Increase it if the "
"receiving system does not reliably decode digits.")
dtmf_enc_settings.append(dtmfspeed_on)
if _mem.dtmf_settings.dtmfspeed_off > 0x0F:
val = 0x03
else:
val = _mem.dtmf_settings.dtmfspeed_off
dtmfspeed_off = RadioSetting(
"dtmf_settings.dtmfspeed_off",
"DTMF Speed (Off Time)",
RadioSettingValueList(LIST_DTMF_SPEED,
current_index=val))
dtmfspeed_off.set_doc(
"Sets the silent gap between transmitted DTMF tones. Increase it "
"if the receiving system cannot separate adjacent digits.")
dtmf_enc_settings.append(dtmfspeed_off)
def memory2string(dmtf_mem):
dtmf_string = ""
for digit in dmtf_mem:
if digit != 255:
index = LIST_DTMF_VALUES.index(digit)
dtmf_string = dtmf_string + LIST_DTMF_DIGITS[index]
return dtmf_string
def apply_dmtf_frame(setting, obj):
LOG.debug("Setting DTMF-Code: " + str(setting.value))
val_string = str(setting.value)
for i in range(0, 16):
obj[i] = 255
i = 0
for current_char in val_string:
current_char = current_char.upper()
index = LIST_DTMF_DIGITS.index(current_char)
obj[i] = LIST_DTMF_VALUES[index]
i = i + 1
codes = self._memobj.dtmf_codes
i = 1
for dtmfcode in codes:
val = RadioSettingValueString(0, 16, memory2string(
dtmfcode.code),
False, CHARSET_DTMF_DIGITS)
line = RadioSetting("dtmf_code_" + str(i) + "_code",
"DMTF Code " + str(i), val)
line.set_doc(
"Programs DTMF calling code %s. Select this code as a "
"channel or VFO's PTT-ID to send it automatically." % i)
line.set_apply_callback(apply_dmtf_frame, dtmfcode.code)
dtmf_enc_settings.append(line)
i = i + 1
line = RadioSetting("dtmf_settings.mastervice",
"Master and Vice ID",
RadioSettingValueBoolean(
_mem.dtmf_settings.mastervice))
line.set_doc(
"Enables the secondary (vice) controller ID in addition to the "
"master ID for DTMF remote-control commands.")
dtmf_dec_settings.append(line)
val = RadioSettingValueString(0, 16, memory2string(
_mem.dtmf_settings.masterid),
False, CHARSET_DTMF_DIGITS)
line = RadioSetting("dtmf_settings.masterid",
"Master Control ID ", val)
line.set_doc(
"Programs the DTMF ID that a master controller must send before "
"the radio accepts an enabled remote-control command.")
line.set_apply_callback(apply_dmtf_frame,
_mem.dtmf_settings.masterid)
dtmf_dec_settings.append(line)
line = RadioSetting("dtmf_settings.minspection",
"Master Inspection",
RadioSettingValueBoolean(
_mem.dtmf_settings.minspection))
line.set_doc(
"Allows the master control ID to request a radio status check "
"after it has identified itself.")
dtmf_dec_settings.append(line)
line = RadioSetting("dtmf_settings.mmonitor",
"Master Monitor",
RadioSettingValueBoolean(
_mem.dtmf_settings.mmonitor))
line.set_doc(
"Allows the master control ID to open the radio's microphone "
"remotely after it has identified itself.")
dtmf_dec_settings.append(line)
line = RadioSetting("dtmf_settings.mstun",
"Master Stun",
RadioSettingValueBoolean(
_mem.dtmf_settings.mstun))
line.set_doc(
"Allows the master control ID to disable transmitting after it "
"has identified itself.")
dtmf_dec_settings.append(line)
line = RadioSetting("dtmf_settings.mkill",
"Master Kill",
RadioSettingValueBoolean(
_mem.dtmf_settings.mkill))
line.set_doc(
"Allows the master control ID to disable transmitting and "
"receiving after it has identified itself.")
dtmf_dec_settings.append(line)
line = RadioSetting("dtmf_settings.mrevive",
"Master Revive",
RadioSettingValueBoolean(
_mem.dtmf_settings.mrevive))
line.set_doc(
"Allows the master control ID to restore a stunned or killed "
"radio after it has identified itself.")
dtmf_dec_settings.append(line)
val = RadioSettingValueString(0, 16, memory2string(
_mem.dtmf_settings.viceid),
False, CHARSET_DTMF_DIGITS)
line = RadioSetting("dtmf_settings.viceid",
"Vice Control ID ", val)
line.set_doc(
"Programs the DTMF ID for the secondary (vice) controller. It is "
"used when Master and Vice ID is enabled.")
line.set_apply_callback(apply_dmtf_frame,
_mem.dtmf_settings.viceid)
dtmf_dec_settings.append(line)
line = RadioSetting("dtmf_settings.vinspection",
"Vice Inspection",
RadioSettingValueBoolean(
_mem.dtmf_settings.vinspection))
line.set_doc(
"Allows the vice control ID to request a radio status check "
"after it has identified itself.")
dtmf_dec_settings.append(line)
line = RadioSetting("dtmf_settings.vmonitor",
"Vice Monitor",
RadioSettingValueBoolean(
_mem.dtmf_settings.vmonitor))
line.set_doc(
"Allows the vice control ID to open the radio's microphone "
"remotely after it has identified itself.")
dtmf_dec_settings.append(line)
line = RadioSetting("dtmf_settings.vstun",
"Vice Stun",
RadioSettingValueBoolean(
_mem.dtmf_settings.vstun))
line.set_doc(
"Allows the vice control ID to disable transmitting after it has "
"identified itself.")
dtmf_dec_settings.append(line)
line = RadioSetting("dtmf_settings.vkill",
"Vice Kill",
RadioSettingValueBoolean(
_mem.dtmf_settings.vkill))
line.set_doc(
"Allows the vice control ID to disable transmitting and "
"receiving after it has identified itself.")
dtmf_dec_settings.append(line)
line = RadioSetting("dtmf_settings.vrevive",
"Vice Revive",
RadioSettingValueBoolean(
_mem.dtmf_settings.vrevive))
line.set_doc(
"Allows the vice control ID to restore a stunned or killed radio "
"after it has identified itself.")
dtmf_dec_settings.append(line)
val = RadioSettingValueString(0, 16, memory2string(
_mem.dtmf_settings.inspection),
False, CHARSET_DTMF_DIGITS)
line = RadioSetting("dtmf_settings.inspection",
"Inspection", val)
line.set_doc(
"Programs the DTMF sequence for the remote inspection or "
"status-check command.")
line.set_apply_callback(apply_dmtf_frame,
_mem.dtmf_settings.inspection)
dtmf_dec_settings.append(line)
val = RadioSettingValueString(0, 16, memory2string(
_mem.dtmf_settings.alarmcode),
False, CHARSET_DTMF_DIGITS)
line = RadioSetting("dtmf_settings.alarmcode",
"Alarm", val)
line.set_doc(
"Programs the DTMF sequence used for the alarm command.")
line.set_apply_callback(apply_dmtf_frame,
_mem.dtmf_settings.alarmcode)
dtmf_dec_settings.append(line)
val = RadioSettingValueString(0, 16, memory2string(
_mem.dtmf_settings.kill),
False, CHARSET_DTMF_DIGITS)
line = RadioSetting("dtmf_settings.kill",
"Kill", val)
line.set_doc(
"Programs the DTMF remote-kill sequence. A valid controller can "
"use it to disable both transmitting and receiving.")
line.set_apply_callback(apply_dmtf_frame,
_mem.dtmf_settings.kill)
dtmf_dec_settings.append(line)
val = RadioSettingValueString(0, 16, memory2string(
_mem.dtmf_settings.monitor),
False, CHARSET_DTMF_DIGITS)
line = RadioSetting("dtmf_settings.monitor",
"Monitor", val)
line.set_doc(
"Programs the DTMF remote-monitor sequence used to open the "
"radio's microphone from a compatible controller.")
line.set_apply_callback(apply_dmtf_frame,
_mem.dtmf_settings.monitor)
dtmf_dec_settings.append(line)
val = RadioSettingValueString(0, 16, memory2string(
_mem.dtmf_settings.stun),
False, CHARSET_DTMF_DIGITS)
line = RadioSetting("dtmf_settings.stun",
"Stun", val)
line.set_doc(
"Programs the DTMF remote-stun sequence. A valid controller can "
"use it to disable transmitting.")
line.set_apply_callback(apply_dmtf_frame,
_mem.dtmf_settings.stun)
dtmf_dec_settings.append(line)
val = RadioSettingValueString(0, 16, memory2string(
_mem.dtmf_settings.revive),
False, CHARSET_DTMF_DIGITS)
line = RadioSetting("dtmf_settings.revive",
"Revive", val)
line.set_doc(
"Programs the DTMF remote-revive sequence used to restore a "
"radio after a stun or kill command.")
line.set_apply_callback(apply_dmtf_frame,
_mem.dtmf_settings.revive)
dtmf_dec_settings.append(line)
def apply_dmtf_listvalue(setting, obj):
LOG.debug("Setting value: " + str(setting.value) + " from list")
val = str(setting.value)
index = LIST_DTMF_SPECIAL_DIGITS.index(val)
val = LIST_DTMF_SPECIAL_VALUES[index]
obj.set_value(val)
if _mem.dtmf_settings.groupcode not in LIST_DTMF_SPECIAL_VALUES:
val = 0x0B
else:
val = _mem.dtmf_settings.groupcode
idx = LIST_DTMF_SPECIAL_VALUES.index(val)
line = RadioSetting(
"dtmf_settings.groupcode",
"Group Code",
RadioSettingValueList(LIST_DTMF_SPECIAL_DIGITS,
current_index=idx))
line.set_doc(
"Selects the special DTMF digit interpreted as a group-call "
"wildcard by the decoder.")
line.set_apply_callback(apply_dmtf_listvalue,
_mem.dtmf_settings.groupcode)
dtmf_dec_settings.append(line)
if _mem.dtmf_settings.spacecode not in LIST_DTMF_SPECIAL_VALUES:
val = 0x0C
else:
val = _mem.dtmf_settings.spacecode
idx = LIST_DTMF_SPECIAL_VALUES.index(val)
line = RadioSetting(
"dtmf_settings.spacecode",
"Space Code",
RadioSettingValueList(LIST_DTMF_SPECIAL_DIGITS,
current_index=idx))
line.set_doc(
"Selects the special DTMF digit interpreted as a separator by "
"the decoder.")
line.set_apply_callback(apply_dmtf_listvalue,
_mem.dtmf_settings.spacecode)
dtmf_dec_settings.append(line)
if self.COLOR_LCD:
if _mem.dtmf_settings.resettime > 0x63:
val = 0x4F
else:
val = _mem.dtmf_settings.resettime
line = RadioSetting(
"dtmf_settings.resettime",
"Reset time",
RadioSettingValueList(LIST_5TONE_RESET_COLOR,
current_index=val))
line.set_doc(
"Sets how long the DTMF decoder remains active after a valid "
"code before returning to its normal state.")
dtmf_dec_settings.append(line)
else:
if _mem.dtmf_settings.resettime > 0x4F:
val = 0x4F
else:
val = _mem.dtmf_settings.resettime
line = RadioSetting(
"dtmf_settings.resettime",
"Reset time",
RadioSettingValueList(LIST_5TONE_RESET,
current_index=val))
line.set_doc(
"Sets how long the DTMF decoder remains active after a valid "
"code before returning to its normal state.")
dtmf_dec_settings.append(line)
if _mem.dtmf_settings.delayproctime > 0x27:
val = 0x04
else:
val = _mem.dtmf_settings.delayproctime
line = RadioSetting(
"dtmf_settings.delayproctime",
"Delay processing time",
RadioSettingValueList(LIST_DTMF_DELAY,
current_index=val))
line.set_doc(
"Sets how long the radio waits after decoding a DTMF command "
"before processing its response.")
dtmf_dec_settings.append(line)
# 5 Tone Settings
stds_5tone = RadioSettingGroup("stds_5tone", "Standards")
codes_5tone = RadioSettingGroup("codes_5tone", "Codes")
group_5tone = RadioSettingGroup("group_5tone", "5 Tone Settings")
group_5tone.append(stds_5tone)
group_5tone.append(codes_5tone)
top.append(group_5tone)
def apply_list_value(setting, obj):
options = setting.value.get_options()
obj.set_value(options.index(str(setting.value)))
_5tone_standards = self._memobj._5tone_std_settings
i = 0
for standard in _5tone_standards:
std_5tone = RadioSettingGroup("std_5tone_" + str(i),
LIST_5TONE_STANDARDS[i])
stds_5tone.append(std_5tone)
period = standard.period
if period == 255:
LOG.debug("Period for " + LIST_5TONE_STANDARDS[i] +
" is not yet configured. Setting to 70ms.")
period = 5
if period <= len(LIST_5TONE_STANDARD_PERIODS):
line = RadioSetting(
"_5tone_std_settings_" + str(i) + "_period",
"Period (ms)", RadioSettingValueList
(LIST_5TONE_STANDARD_PERIODS,
current_index=period))
line.set_doc(
"Sets the duration of each tone when using the %s "
"5-tone standard." % LIST_5TONE_STANDARDS[i])
line.set_apply_callback(apply_list_value, standard.period)
std_5tone.append(line)
else:
LOG.debug("Invalid value for 5tone period! Disabling.")
group_tone = standard.group_tone
if group_tone == 255:
LOG.debug("Group-Tone for " + LIST_5TONE_STANDARDS[i] +
" is not yet configured. Setting to A.")
group_tone = 10
if group_tone <= len(LIST_5TONE_DIGITS):
line = RadioSetting(
"_5tone_std_settings_" + str(i) + "_grouptone",
"Group Tone",
RadioSettingValueList(LIST_5TONE_DIGITS,
current_index=group_tone))
line.set_doc(
"Selects the digit that represents a group call in the "
"%s 5-tone standard." % LIST_5TONE_STANDARDS[i])
line.set_apply_callback(apply_list_value,
standard.group_tone)
std_5tone.append(line)
else:
LOG.debug("Invalid value for 5tone digit! Disabling.")
repeat_tone = standard.repeat_tone
if repeat_tone == 255:
LOG.debug("Repeat-Tone for " + LIST_5TONE_STANDARDS[i] +
" is not yet configured. Setting to E.")
repeat_tone = 14
if repeat_tone <= len(LIST_5TONE_DIGITS):
line = RadioSetting(
"_5tone_std_settings_" + str(i) + "_repttone",
"Repeat Tone",
RadioSettingValueList(LIST_5TONE_DIGITS,
current_index=repeat_tone))
line.set_doc(
"Selects the substitute digit sent for consecutive "
"repeated digits in the %s 5-tone standard." %
LIST_5TONE_STANDARDS[i])
line.set_apply_callback(apply_list_value,
standard.repeat_tone)
std_5tone.append(line)
else:
LOG.debug("Invalid value for 5tone digit! Disabling.")
i = i + 1
def my_apply_5tonestdlist_value(setting, obj):
if LIST_5TONE_STANDARDS.index(str(setting.value)) == 15:
obj.set_value(0xFF)
else:
obj.set_value(LIST_5TONE_STANDARDS.
index(str(setting.value)))
def apply_5tone_frame(setting, obj):
LOG.debug("Setting 5 Tone: " + str(setting.value))
valstring = str(setting.value)
if len(valstring) == 0:
for i in range(0, 5):
obj[i] = 255
else:
validFrame = True
for i in range(0, 5):
currentChar = valstring[i].upper()
if currentChar in LIST_5TONE_DIGITS:
obj[i] = LIST_5TONE_DIGITS.index(currentChar)
else:
validFrame = False
LOG.debug("invalid char: " + str(currentChar))
if not validFrame:
LOG.debug("setting whole frame to FF")
for i in range(0, 5):
obj[i] = 255
def validate_5tone_frame(value):
if (len(str(value)) != 5) and (len(str(value)) != 0):
msg = ("5 Tone must have 5 digits or 0 digits")
raise InvalidValueError(msg)
for digit in str(value):
if digit.upper() not in LIST_5TONE_DIGITS:
msg = (str(digit) + " is not a valid digit for 5tones")
raise InvalidValueError(msg)
return value
def frame2string(frame):
frameString = ""
for digit in frame:
if digit != 255:
frameString = frameString + LIST_5TONE_DIGITS[digit]
return frameString
_5tone_codes = self._memobj._5tone_codes
i = 1
for code in _5tone_codes:
code_5tone = RadioSettingGroup("code_5tone_" + str(i),
"5 Tone code " + str(i))
codes_5tone.append(code_5tone)
if (code.standard == 255):
currentVal = 15
else:
currentVal = code.standard
line = RadioSetting(
"_5tone_code_" + str(i) + "_std", " Standard",
RadioSettingValueList(
LIST_5TONE_STANDARDS, current_index=currentVal))
line.set_doc(
"Selects the signalling standard used by 5-tone calling "
"code %s." % i)
line.set_apply_callback(my_apply_5tonestdlist_value,
code.standard)
code_5tone.append(line)
val = RadioSettingValueString(0, 6,
frame2string(code.frame1), False)
line = RadioSetting("_5tone_code_" + str(i) + "_frame1",
" Frame 1", val)
line.set_doc(
"Programs frame 1 of 5-tone calling code %s. Enter exactly "
"five digits, or leave it blank to disable this frame." % i)
val.set_validate_callback(validate_5tone_frame)
line.set_apply_callback(apply_5tone_frame, code.frame1)
code_5tone.append(line)
val = RadioSettingValueString(0, 6,
frame2string(code.frame2), False)
line = RadioSetting("_5tone_code_" + str(i) + "_frame2",
" Frame 2", val)
line.set_doc(
"Programs frame 2 of 5-tone calling code %s. Enter exactly "
"five digits, or leave it blank to disable this frame." % i)
val.set_validate_callback(validate_5tone_frame)
line.set_apply_callback(apply_5tone_frame, code.frame2)
code_5tone.append(line)
val = RadioSettingValueString(0, 6,
frame2string(code.frame3), False)
line = RadioSetting("_5tone_code_" + str(i) + "_frame3",
" Frame 3", val)
line.set_doc(
"Programs frame 3 of 5-tone calling code %s. Enter exactly "
"five digits, or leave it blank to disable this frame." % i)
val.set_validate_callback(validate_5tone_frame)
line.set_apply_callback(apply_5tone_frame, code.frame3)
code_5tone.append(line)
i = i + 1
_5_tone_decode1 = RadioSetting(
"_5tone_settings._5tone_decode_call_frame1",
"5 Tone decode call Frame 1",
RadioSettingValueBoolean(
_mem._5tone_settings._5tone_decode_call_frame1))
_5_tone_decode1.set_doc(
"Sounds a call alert after frame 1 of a received 5-tone code is "
"decoded.")
group_5tone.append(_5_tone_decode1)
_5_tone_decode2 = RadioSetting(
"_5tone_settings._5tone_decode_call_frame2",
"5 Tone decode call Frame 2",
RadioSettingValueBoolean(
_mem._5tone_settings._5tone_decode_call_frame2))
_5_tone_decode2.set_doc(
"Sounds a call alert after frame 2 of a received 5-tone code is "
"decoded.")
group_5tone.append(_5_tone_decode2)
_5_tone_decode3 = RadioSetting(
"_5tone_settings._5tone_decode_call_frame3",
"5 Tone decode call Frame 3",
RadioSettingValueBoolean(
_mem._5tone_settings._5tone_decode_call_frame3))
_5_tone_decode3.set_doc(
"Sounds a call alert after frame 3 of a received 5-tone code is "
"decoded.")
group_5tone.append(_5_tone_decode3)
_5_tone_decode_disp1 = RadioSetting(
"_5tone_settings._5tone_decode_disp_frame1",
"5 Tone decode disp Frame 1",
RadioSettingValueBoolean(
_mem._5tone_settings._5tone_decode_disp_frame1))
_5_tone_decode_disp1.set_doc(
"Shows the decoded call after frame 1 of a received 5-tone code "
"is decoded.")
group_5tone.append(_5_tone_decode_disp1)
_5_tone_decode_disp2 = RadioSetting(
"_5tone_settings._5tone_decode_disp_frame2",
"5 Tone decode disp Frame 2",
RadioSettingValueBoolean(
_mem._5tone_settings._5tone_decode_disp_frame2))
_5_tone_decode_disp2.set_doc(
"Shows the decoded call after frame 2 of a received 5-tone code "
"is decoded.")
group_5tone.append(_5_tone_decode_disp2)
_5_tone_decode_disp3 = RadioSetting(
"_5tone_settings._5tone_decode_disp_frame3",
"5 Tone decode disp Frame 3",
RadioSettingValueBoolean(
_mem._5tone_settings._5tone_decode_disp_frame3))
_5_tone_decode_disp3.set_doc(
"Shows the decoded call after frame 3 of a received 5-tone code "
"is decoded.")
group_5tone.append(_5_tone_decode_disp3)
decode_standard = _mem._5tone_settings.decode_standard
if decode_standard == 255:
decode_standard = 0
if decode_standard <= len(LIST_5TONE_STANDARDS_without_none):
line = RadioSetting("_5tone_settings.decode_standard",
"5 Tone-decode Standard",
RadioSettingValueList(
LIST_5TONE_STANDARDS_without_none,
current_index=decode_standard))
line.set_doc(
"Selects the signalling standard used to decode incoming "
"5-tone calls.")
group_5tone.append(line)
else:
LOG.debug("Invalid decode std...")
_5tone_delay1 = _mem._5tone_settings._5tone_delay1
if _5tone_delay1 == 255:
_5tone_delay1 = 20
if _5tone_delay1 <= len(LIST_5TONE_DELAY):
list = RadioSettingValueList(LIST_5TONE_DELAY,
current_index=_5tone_delay1)
line = RadioSetting("_5tone_settings._5tone_delay1",
"5 Tone Delay Frame 1", list)
line.set_doc(
"Sets the pause after frame 1 when sending a multi-frame "
"5-tone calling code.")
group_5tone.append(line)
else:
LOG.debug(
"Invalid value for 5tone delay (frame1) ! Disabling.")
_5tone_delay2 = _mem._5tone_settings._5tone_delay2
if _5tone_delay2 == 255:
_5tone_delay2 = 20
LOG.debug("5 Tone delay unconfigured! Resetting to 200ms.")
if _5tone_delay2 <= len(LIST_5TONE_DELAY):
list = RadioSettingValueList(LIST_5TONE_DELAY,
current_index=_5tone_delay2)
line = RadioSetting("_5tone_settings._5tone_delay2",
"5 Tone Delay Frame 2", list)
line.set_doc(
"Sets the pause after frame 2 when sending a multi-frame "
"5-tone calling code.")
group_5tone.append(line)
else:
LOG.debug("Invalid value for 5tone delay (frame2)! Disabling.")
_5tone_delay3 = _mem._5tone_settings._5tone_delay3
if _5tone_delay3 == 255:
_5tone_delay3 = 20
LOG.debug("5 Tone delay unconfigured! Resetting to 200ms.")
if _5tone_delay3 <= len(LIST_5TONE_DELAY):
list = RadioSettingValueList(LIST_5TONE_DELAY,
current_index=_5tone_delay3)
line = RadioSetting("_5tone_settings._5tone_delay3",
"5 Tone Delay Frame 3", list)
line.set_doc(
"Sets the pause after frame 3 when sending a multi-frame "
"5-tone calling code.")
group_5tone.append(line)
else:
LOG.debug("Invalid value for 5tone delay (frame3)! Disabling.")
ext_length = _mem._5tone_settings._5tone_first_digit_ext_length
if ext_length == 255:
ext_length = 0
LOG.debug("1st Tone ext length unconfigured! Resetting to 0")
if ext_length <= len(LIST_5TONE_DELAY):
list = RadioSettingValueList(
LIST_5TONE_DELAY,
current_index=ext_length)
line = RadioSetting(
"_5tone_settings._5tone_first_digit_ext_length",
"First digit extend length", list)
line.set_doc(
"Extends the first digit of a transmitted 5-tone code by the "
"selected time.")
group_5tone.append(line)
else:
LOG.debug("Invalid value for 5tone ext length! Disabling.")
decode_reset_time = _mem._5tone_settings.decode_reset_time
if decode_reset_time == 255:
decode_reset_time = 59
LOG.debug("Decode reset time unconfigured. resetting.")
if decode_reset_time <= len(LIST_5TONE_RESET):
list = RadioSettingValueList(
LIST_5TONE_RESET,
current_index=decode_reset_time)
line = RadioSetting("_5tone_settings.decode_reset_time",
"Decode reset time", list)
line.set_doc(
"Sets how long a successful 5-tone decode remains active "
"before the decoder resets.")
group_5tone.append(line)
else:
LOG.debug("Invalid value decode reset time! Disabling.")
# 2 Tone
encode_2tone = RadioSettingGroup("encode_2tone", "2 Tone Encode")
decode_2tone = RadioSettingGroup("decode_2tone", "2 Code Decode")
top.append(encode_2tone)
top.append(decode_2tone)
duration_1st_tone = self._memobj._2tone.duration_1st_tone
if duration_1st_tone == 255:
LOG.debug("Duration of first 2 Tone digit is not yet " +
"configured. Setting to 600ms")
duration_1st_tone = 60
if duration_1st_tone <= len(LIST_5TONE_DELAY):
val = RadioSettingValueList(LIST_5TONE_DELAY,
current_index=duration_1st_tone)
line = RadioSetting("_2tone.duration_1st_tone",
"Duration 1st Tone", val)
line.set_doc(
"Sets how long the first tone of every 2-tone calling code "
"is transmitted.")
encode_2tone.append(line)
duration_2nd_tone = self._memobj._2tone.duration_2nd_tone
if duration_2nd_tone == 255:
LOG.debug("Duration of second 2 Tone digit is not yet " +
"configured. Setting to 600ms")
duration_2nd_tone = 60
if duration_2nd_tone <= len(LIST_5TONE_DELAY):
val = RadioSettingValueList(LIST_5TONE_DELAY,
current_index=duration_2nd_tone)
line = RadioSetting("_2tone.duration_2nd_tone",
"Duration 2nd Tone", val)
line.set_doc(
"Sets how long the second tone of every 2-tone calling code "
"is transmitted.")
encode_2tone.append(line)
duration_gap = self._memobj._2tone.duration_gap
if duration_gap == 255:
LOG.debug("Duration of gap is not yet " +
"configured. Setting to 300ms")
duration_gap = 30
if duration_gap <= len(LIST_5TONE_DELAY):
line = RadioSetting(
"_2tone.duration_gap", "Duration of gap",
RadioSettingValueList(
LIST_5TONE_DELAY, current_index=duration_gap))
line.set_doc(
"Sets the silent gap between the first and second tones of a "
"2-tone calling code.")
encode_2tone.append(line)
def _2tone_validate(value):
if value == 0:
return 65535
if value == 65535:
return value
if not (300 <= value and value <= 3000):
msg = ("2 Tone Frequency: Must be between 300 and 3000 Hz")
raise InvalidValueError(msg)
return value
def apply_2tone_freq(setting, obj):
val = int(setting.value)
if (val == 0) or (val == 65535):
obj.set_value(65535)
else:
obj.set_value(val)
i = 1
for code in self._memobj._2tone._2tone_encode:
code_2tone = RadioSettingGroup("code_2tone_" + str(i),
"Encode Code " + str(i))
encode_2tone.append(code_2tone)
tmp = code.freq1
if tmp == 65535:
tmp = 0
val1 = RadioSettingValueInteger(0, 65535, tmp)
freq1 = RadioSetting("2tone_code_" + str(i) + "_freq1",
"Frequency 1", val1)
freq1.set_doc(
"Sets tone 1 of 2-tone calling code %s in hertz. Use "
"300-3000 Hz, or 0 to leave it unused." % i)
val1.set_validate_callback(_2tone_validate)
freq1.set_apply_callback(apply_2tone_freq, code.freq1)
code_2tone.append(freq1)
tmp = code.freq2
if tmp == 65535:
tmp = 0
val2 = RadioSettingValueInteger(0, 65535, tmp)
freq2 = RadioSetting("2tone_code_" + str(i) + "_freq2",
"Frequency 2", val2)
freq2.set_doc(
"Sets tone 2 of 2-tone calling code %s in hertz. Use "
"300-3000 Hz, or 0 to leave it unused." % i)
val2.set_validate_callback(_2tone_validate)
freq2.set_apply_callback(apply_2tone_freq, code.freq2)
code_2tone.append(freq2)
i = i + 1
decode_reset_time = _mem._2tone.reset_time
if decode_reset_time == 255:
decode_reset_time = 59
LOG.debug("Decode reset time unconfigured. resetting.")
if decode_reset_time <= len(LIST_5TONE_RESET):
list = RadioSettingValueList(
LIST_5TONE_RESET,
current_index=decode_reset_time)
line = RadioSetting("_2tone.reset_time",
"Decode reset time", list)
line.set_doc(
"Sets how long a successful 2-tone decode remains active "
"before the decoder resets.")
decode_2tone.append(line)
else:
LOG.debug("Invalid value decode reset time! Disabling.")
def apply_2tone_freq_pair(setting, obj):
val = int(setting.value)
derived_val = 65535
frqname = str(setting._name[-5:])
derivedname = "derived_from_" + frqname
if (val == 0):
val = 65535
derived_val = 65535
else:
derived_val = int(round(2304000.0/val))
obj[frqname].set_value(val)
obj[derivedname].set_value(derived_val)
LOG.debug("Apply " + frqname + ": " + str(val) + " | " +
derivedname + ": " + str(derived_val))
i = 1
for decode_code in self._memobj._2tone._2tone_decode:
_2tone_dec_code = RadioSettingGroup("code_2tone_" + str(i),
"Decode Code " + str(i))
decode_2tone.append(_2tone_dec_code)
j = 1
for dec in decode_code.decs:
val = dec.dec
if val == 255:
LOG.debug("Dec for Code " + str(i) + " Dec " + str(j) +
" is not yet configured. Setting to 0.")
val = 0
if val <= len(LIST_2TONE_DEC):
line = RadioSetting(
"_2tone_dec_settings_" + str(i) + "_dec_" + str(j),
"Dec " + str(j), RadioSettingValueList
(LIST_2TONE_DEC,
current_index=val))
line.set_doc(
"Selects the ordered pair of decode frequencies for "
"match rule %s of 2-tone decode code %s." % (j, i))
line.set_apply_callback(apply_list_value, dec.dec)
_2tone_dec_code.append(line)
else:
LOG.debug("Invalid value for 2tone dec! Disabling.")
val = dec.response
if val == 255:
LOG.debug("Response for Code " +
str(i) + " Dec " + str(j) +
" is not yet configured. Setting to 0.")
val = 0
if val <= len(LIST_2TONE_RESPONSE):
line = RadioSetting(
"_2tone_dec_settings_" +
str(i) + "_resp_" + str(j),
"Response " + str(j), RadioSettingValueList
(LIST_2TONE_RESPONSE,
current_index=val))
line.set_doc(
"Selects the radio's response after match rule %s of "
"2-tone decode code %s is received." % (j, i))
line.set_apply_callback(apply_list_value, dec.response)
_2tone_dec_code.append(line)
else:
LOG.debug(
"Invalid value for 2tone response! Disabling.")
val = dec.alert
if val == 255:
LOG.debug("Alert for Code " +
str(i) + " Dec " + str(j) +
" is not yet configured. Setting to 0.")
val = 0
if val <= len(PTTIDCODE_LIST):
line = RadioSetting(
"_2tone_dec_settings_" +
str(i) + "_alert_" + str(j),
"Alert " + str(j), RadioSettingValueList
(PTTIDCODE_LIST,
current_index=val))
line.set_doc(
"Selects the programmed alert entry associated with "
"match rule %s of 2-tone decode code %s." % (j, i))
line.set_apply_callback(apply_list_value, dec.alert)
_2tone_dec_code.append(line)
else:
LOG.debug("Invalid value for 2tone alert! Disabling.")
j = j + 1
freq = self._memobj._2tone.freqs[i-1]
for char in ['A', 'B', 'C', 'D']:
setting_name = "freq" + str(char)
tmp = freq[setting_name]
if tmp == 65535:
tmp = 0
if tmp != 0:
expected = int(round(2304000.0/int(tmp)))
from_mem = freq["derived_from_" + setting_name]
if expected != from_mem:
LOG.error("Expected " + str(expected) +
" but read " + str(from_mem) +
". Disabling 2Tone Decode Freqs!")
break
val = RadioSettingValueInteger(0, 65535, tmp)
frq = RadioSetting("2tone_dec_" + str(i) +
"_freq" + str(char),
("Decode Frequency " + str(char)), val)
frq.set_doc(
"Sets decode frequency %s for 2-tone decode code %s in "
"hertz. Use 300-3000 Hz, or 0 to leave it unused." %
(char, i))
val.set_validate_callback(_2tone_validate)
frq.set_apply_callback(apply_2tone_freq_pair, freq)
_2tone_dec_code.append(frq)
i = i + 1
return top
def set_settings(self, settings):
_settings = self._memobj.settings
for element in settings:
if not isinstance(element, RadioSetting):
if element.get_name() == "fm_preset":
self._set_fm_preset(element)
else:
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 == "volume" and self.MODEL == "KT-WP12":
setattr(obj, setting, int(element.value) - 1)
elif setting == "volume" and self.MODEL == "WP-9900":
setattr(obj, setting, int(element.value) - 1)
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
@classmethod
def match_model(cls, filedata, filename):
match_size = False
match_model = False
# testing the file data size
if len(filedata) == MEM_SIZE:
match_size = True
# testing the firmware model fingerprint
match_model = model_match(cls, filedata)
if match_size and match_model:
return True
else:
return False
MEM_FORMAT = """
// #seekto 0x0000;
struct {
lbcd rxfreq[4];
lbcd txfreq[4];
ul16 rxtone;
ul16 txtone;
u8 unknown0:4,
scode:4;
u8 unknown1:2,
spmute:2,
unknown2:2,
optsig:2;
u8 unknown3:3,
scramble:1,
unknown4:3,
power:1;
u8 unknown5:1,
wide:1,
unknown6:2,
bcl:1,
add:1,
pttid:2;
} memory[200];
#seekto 0x0E00;
struct {
u8 unusedE00:7,
tdr:1;
u8 unknown1;
u8 sql;
u8 unknown2[2];
u8 tot;
u8 apo; // BTech radios use this as the Auto Power Off time
// other radios use this as pre-Time Out Alert
u8 unknown3;
u8 abr;
u8 unusedE09:7,
beep:1;
u8 unknown4[4];
u8 dtmfst;
u8 unknown5[2];
u8 unusedE11:7,
prisc:1;
u8 prich;
u8 screv;
u8 unknown6[2];
u8 pttid;
u8 pttlt;
u8 unknown7;
u8 emctp;
u8 emcch;
u8 ringt;
u8 unknown8;
u8 camdf;
u8 cbmdf;
u8 unusedE1F:7,
sync:1; // BTech radios use this as the display sync setting
// other radios use this as the auto keypad lock setting
u8 ponmsg;
u8 wtled;
u8 rxled;
u8 txled;
u8 unknown9[5];
u8 anil;
u8 reps;
u8 repm;
u8 tdrab;
u8 unusedE2D:7,
ste:1;
u8 rpste;
u8 rptdl;
u8 mgain;
u8 dtmfg;
} settings;
#seekto 0x0E80;
struct {
u8 unknown1;
u8 vfomr;
u8 unusedE82:7,
keylock:1;
u8 unknown2;
u8 unknown3:4,
vfomren:1,
unknown4:1,
reseten:1,
menuen:1;
u8 unknown5[11];
u8 dispab;
u8 mrcha;
u8 mrchb;
u8 menu;
} settings2;
#seekto 0x0EC0;
struct {
char line1[6];
char line2[6];
} poweron_msg;
struct settings_vfo {
u8 freq[8];
u8 offset[6];
u8 unknown2[2];
ul16 rxtone;
ul16 txtone;
u8 scode;
u8 spmute;
u8 optsig;
u8 unused1:7,
scramble:1;
u8 wide;
u8 power;
u8 shiftd;
u8 step;
u8 unknown3[4];
};
#seekto 0x0F00;
struct {
struct settings_vfo a;
struct settings_vfo b;
} vfo;
#seekto 0x1000;
struct {
char name[6];
u8 unknown1[10];
} names[200];
#seekto 0x2400;
struct {
u8 period; // one out of LIST_5TONE_STANDARD_PERIODS
u8 group_tone;
u8 repeat_tone;
u8 unused[13];
} _5tone_std_settings[15];
#seekto 0x2500;
struct {
u8 frame1[5];
u8 frame2[5];
u8 frame3[5];
u8 standard; // one out of LIST_5TONE_STANDARDS
} _5tone_codes[15];
// #seekto 0x25F0;
struct {
u8 _5tone_delay1; // * 10ms
u8 _5tone_delay2; // * 10ms
u8 _5tone_delay3; // * 10ms
u8 _5tone_first_digit_ext_length;
u8 unknown1;
u8 unknown2;
u8 unknown3;
u8 unknown4;
u8 decode_standard;
u8 unknown5:5,
_5tone_decode_call_frame3:1,
_5tone_decode_call_frame2:1,
_5tone_decode_call_frame1:1;
u8 unknown6:5,
_5tone_decode_disp_frame3:1,
_5tone_decode_disp_frame2:1,
_5tone_decode_disp_frame1:1;
u8 decode_reset_time; // * 100 + 100ms
} _5tone_settings;
#seekto 0x2900;
struct {
u8 code[16]; // 0=x0A, A=0x0D, B=0x0E, C=0x0F, D=0x00, #=0x0C *=0x0B
} dtmf_codes[15];
// #seekto 0x29F0;
struct {
u8 dtmfspeed_on; //list with 50..2000ms in steps of 10
u8 dtmfspeed_off; //list with 50..2000ms in steps of 10
u8 unknown0[14];
u8 inspection[16];
u8 monitor[16];
u8 alarmcode[16];
u8 stun[16];
u8 kill[16];
u8 revive[16];
u8 unknown1[16];
u8 unknown2[16];
u8 unknown3[16];
u8 unknown4[16];
u8 unknown5[16];
u8 unknown6[16];
u8 unknown7[16];
u8 masterid[16];
u8 viceid[16];
u8 unused01:7,
mastervice:1;
u8 unused02:3,
mrevive:1,
mkill:1,
mstun:1,
mmonitor:1,
minspection:1;
u8 unused03:3,
vrevive:1,
vkill:1,
vstun:1,
vmonitor:1,
vinspection:1;
u8 unused04:6,
txdisable:1,
rxdisable:1;
u8 groupcode;
u8 spacecode;
u8 delayproctime; // * 100 + 100ms
u8 resettime; // * 100 + 100ms
} dtmf_settings;
#seekto 0x2D00;
struct {
struct {
ul16 freq1;
u8 unused01[6];
ul16 freq2;
u8 unused02[6];
} _2tone_encode[15];
u8 duration_1st_tone; // *10ms
u8 duration_2nd_tone; // *10ms
u8 duration_gap; // *10ms
u8 unused03[13];
struct {
struct {
u8 dec; // one out of LIST_2TONE_DEC
u8 response; // one out of LIST_2TONE_RESPONSE
u8 alert; // 1-16
} decs[4];
u8 unused04[4];
} _2tone_decode[15];
u8 unused05[16];
struct {
ul16 freqA;
ul16 freqB;
ul16 freqC;
ul16 freqD;
// unknown what those values mean, but they are
// derived from configured frequencies
ul16 derived_from_freqA; // 2304000/freqA
ul16 derived_from_freqB; // 2304000/freqB
ul16 derived_from_freqC; // 2304000/freqC
ul16 derived_from_freqD; // 2304000/freqD
}freqs[15];
u8 reset_time; // * 100 + 100ms - 100-8000ms
} _2tone;
#seekto 0x3000;
struct {
u8 freq[8];
char broadcast_station_name[6];
u8 unknown[2];
} fm_radio_preset[16];
#seekto 0x3C90;
struct {
u8 vhf_low[3];
u8 vhf_high[3];
u8 uhf_low[3];
u8 uhf_high[3];
} ranges;
// the UV-2501+220 & KT8900R has different zones for storing ranges
#seekto 0x3CD0;
struct {
u8 vhf_low[3];
u8 vhf_high[3];
u8 unknown1[4];
u8 unknown2[6];
u8 vhf2_low[3];
u8 vhf2_high[3];
u8 unknown3[4];
u8 unknown4[6];
u8 uhf_low[3];
u8 uhf_high[3];
} ranges220;
#seekto 0x3F70;
struct {
char fp[6];
} fingerprint;
"""
class BTech(BTechMobileCommon):
"""BTECH's UV-5001 and alike radios"""
BANDS = 2
COLOR_LCD = False
NAME_LENGTH = 6
def set_options(self):
"""This is to read the options from the image and set it in the
environment, for now just the limits of the freqs in the VHF/UHF
ranges"""
# setting the correct ranges for each radio type
if self.MODEL in ["UV-2501+220", "KT8900R"]:
# the model 2501+220 has a segment in 220
# and a different position in the memmap
# also the QYT KT8900R
ranges = self._memobj.ranges220
else:
ranges = self._memobj.ranges
# the normal dual bands
vhf = _decode_ranges(ranges.vhf_low, ranges.vhf_high)
uhf = _decode_ranges(ranges.uhf_low, ranges.uhf_high)
# DEBUG
LOG.info("Radio ranges: VHF %d to %d" % vhf)
LOG.info("Radio ranges: UHF %d to %d" % uhf)
# 220 MHz radios case
if self.MODEL in ["UV-2501+220", "KT8900R"]:
vhf2 = _decode_ranges(ranges.vhf2_low, ranges.vhf2_high)
LOG.info("Radio ranges: VHF(220) %d to %d" % vhf2)
self._220_range = vhf2
# set the class with the real data
self._vhf_range = vhf
self._uhf_range = uhf
def process_mmap(self):
"""Process the mem map into the mem object"""
# Get it
self._memobj = bitwise.parse(MEM_FORMAT, self._mmap)
# load specific parameters from the radio image
self.set_options()
# Declaring Aliases (Clones of the real radios)
class JT2705M(chirp_common.Alias):
VENDOR = "Jetstream"
MODEL = "JT2705M"
class JT6188Mini(chirp_common.Alias):
VENDOR = "Juentai"
MODEL = "JT-6188 Mini"
class JT6188Plus(chirp_common.Alias):
VENDOR = "Juentai"
MODEL = "JT-6188 Plus"
class SSGT890(chirp_common.Alias):
VENDOR = "Sainsonic"
MODEL = "GT-890"
class ZastoneMP300(chirp_common.Alias):
VENDOR = "Zastone"
MODEL = "MP-300"
# real radios
@directory.register
class UV2501(BTech):
"""Baofeng Tech UV2501"""
MODEL = "UV-2501"
_fileid = [UV2501G3_fp,
UV2501G2_fp,
UV2501pp2_fp,
UV2501pp_fp]
@directory.register
class UV2501_220(BTech):
"""Baofeng Tech UV2501+220"""
MODEL = "UV-2501+220"
BANDS = 3
_magic = MSTRING_220
_fileid = [UV2501_220G3_fp,
UV2501_220G2_fp,
UV2501_220_fp,
UV2501_220pp_fp]
@directory.register
class UV5001(BTech):
"""Baofeng Tech UV5001"""
MODEL = "UV-5001"
_fileid = [UV5001G3_fp,
UV5001G22_fp,
UV5001G2_fp,
UV5001alpha_fp,
UV5001pp_fp]
_power_levels = [chirp_common.PowerLevel("High", watts=50),
chirp_common.PowerLevel("Low", watts=10)]
@directory.register
class MINI8900(BTech):
"""WACCOM MINI-8900"""
VENDOR = "WACCOM"
MODEL = "MINI-8900"
_magic = MSTRING_MINI8900
_fileid = [MINI8900_fp]
# Clones
ALIASES = [JT6188Plus]
@directory.register
class KTUV980(BTech):
"""QYT KT-UV980"""
VENDOR = "QYT"
MODEL = "KT-UV980"
_vhf_range = (136000000, 175000000)
_uhf_range = (400000000, 481000000)
_magic = MSTRING_MINI8900
_fileid = [KTUV980_fp]
# Clones
ALIASES = [JT2705M]
# Please note that there is a version of this radios that is a clone of the
# Waccom Mini8900, maybe an early version?
class OTGRadioV1(chirp_common.Alias):
VENDOR = 'OTGSTUFF'
MODEL = 'OTG Radio v1'
@directory.register
class KT9800(BTech):
"""QYT KT8900"""
VENDOR = "QYT"
MODEL = "KT8900"
_vhf_range = (136000000, 175000000)
_uhf_range = (400000000, 481000000)
_magic = MSTRING_KT8900
_fileid = [KT8900_fp,
KT8900_fp1,
KT8900_fp2,
KT8900_fp3,
KT8900_fp4,
KT8900_fp5,
KT8900_fp6,
KT8900_fp7,
KT8900_fp8]
# Clones
ALIASES = [JT6188Mini, SSGT890, ZastoneMP300]
@directory.register
class KT9800R(BTech):
"""QYT KT8900R"""
VENDOR = "QYT"
MODEL = "KT8900R"
BANDS = 3
_vhf_range = (136000000, 175000000)
_220_range = (240000000, 271000000)
_uhf_range = (400000000, 481000000)
_magic = MSTRING_KT8900R
_fileid = [KT8900R_fp,
KT8900R_fp1,
KT8900R_fp2,
KT8900R_fp3,
KT8900R_fp4,
KT8900R_fp5]
@directory.register
class LT588UV(BTech):
"""LUITON LT-588UV"""
VENDOR = "LUITON"
MODEL = "LT-588UV"
_vhf_range = (136000000, 175000000)
_uhf_range = (400000000, 481000000)
_magic = MSTRING_KT8900
_fileid = [LT588UV_fp,
LT588UV_fp1]
_power_levels = [chirp_common.PowerLevel("High", watts=60),
chirp_common.PowerLevel("Low", watts=10)]
COLOR_MEM_FORMAT = """
// #seekto 0x0000;
struct {
lbcd rxfreq[4];
lbcd txfreq[4];
ul16 rxtone;
ul16 txtone;
u8 unknown0:4,
scode:4;
u8 unknown1:2,
spmute:2,
unknown2:2,
optsig:2;
u8 unknown3:3,
scramble:1,
unknown4:2,
power:2;
u8 unknown5:1,
wide:1,
unknown6:2,
bcl:1,
add:1,
pttid:2;
} memory[200];
#seekto 0x0E00;
struct {
u8 tmr;
u8 unknown1;
u8 sql;
u8 unknown2;
u8 mgain2;
u8 tot;
u8 apo;
u8 unknown3;
u8 abr;
u8 unusedE09:7,
beep:1;
u8 unknown4[4];
u8 dtmfst;
u8 unknown5[2];
u8 screv;
u8 unknown6[2];
u8 pttid;
u8 pttlt;
u8 unknown7;
u8 emctp;
u8 emcch;
u8 unusedE19:7,
sigbp:1;
u8 unknown8;
u8 camdf;
u8 cbmdf;
u8 ccmdf;
u8 cdmdf;
u8 langua; // BTech Gen2 radios have removed the Language setting
// use this as the VOX setting
u8 sync; // BTech radios use this as the display sync
// setting, other radios use this as the auto
// keypad lock setting
u8 mainfc;
u8 mainbc;
u8 menufc;
u8 menubc;
u8 stafc;
u8 stabc;
u8 sigfc;
u8 sigbc;
u8 rxfc;
u8 txfc;
u8 txdisp;
u8 unknown9[5];
u8 anil;
u8 reps;
u8 repm;
u8 tmrmr;
u8 unusedE35:7,
ste:1;
u8 rpste;
u8 rptdl;
u8 dtmfg;
u8 mgain; // used by db25-g for ponyey
u8 skiptx;
u8 scmode;
u8 tmrtx;
u8 earpho;
} settings;
#seekto 0x0E80;
struct {
u8 unknown1;
u8 vfomr;
u8 unusedE82:7,
keylock:1;
u8 unknown2;
u8 unknown3:4,
vfomren:1,
unknown4:1,
reseten:1,
menuen:1;
u8 unknown5[11];
u8 dispab;
u8 unknown6[2];
u8 menu;
u8 unknown7[7];
u8 vfomra;
u8 vfomrb;
u8 vfomrc;
u8 vfomrd;
u8 mrcha;
u8 mrchb;
u8 mrchc;
u8 mrchd;
} settings2;
struct settings_vfo {
u8 freq[8];
u8 offset[6];
u8 unknown2[2];
ul16 rxtone;
ul16 txtone;
u8 scode;
u8 spmute;
u8 optsig;
u8 unused1:7,
scramble:1;
u8 wide;
u8 power;
u8 shiftd;
u8 step;
u8 unknown3[4];
};
#seekto 0x0F00;
struct {
struct settings_vfo a;
struct settings_vfo b;
struct settings_vfo c;
struct settings_vfo d;
} vfo;
// #seekto 0x0F80;
struct {
char line1[8];
char line2[8];
char line3[8];
char line4[8];
char line5[8];
char line6[8];
char line7[8];
char line8[8];
} poweron_msg;
#seekto 0x1000;
struct {
char name[8];
u8 unknown1[8];
} names[200];
#seekto 0x2400;
struct {
u8 period; // one out of LIST_5TONE_STANDARD_PERIODS
u8 group_tone;
u8 repeat_tone;
u8 unused[13];
} _5tone_std_settings[15];
#seekto 0x2500;
struct {
u8 frame1[5];
u8 frame2[5];
u8 frame3[5];
u8 standard; // one out of LIST_5TONE_STANDARDS
} _5tone_codes[15];
// #seekto 0x25F0;
struct {
u8 _5tone_delay1; // * 10ms
u8 _5tone_delay2; // * 10ms
u8 _5tone_delay3; // * 10ms
u8 _5tone_first_digit_ext_length;
u8 unknown1;
u8 unknown2;
u8 unknown3;
u8 unknown4;
u8 decode_standard;
u8 unknown5:5,
_5tone_decode_call_frame3:1,
_5tone_decode_call_frame2:1,
_5tone_decode_call_frame1:1;
u8 unknown6:5,
_5tone_decode_disp_frame3:1,
_5tone_decode_disp_frame2:1,
_5tone_decode_disp_frame1:1;
u8 decode_reset_time; // * 100 + 100ms
} _5tone_settings;
#seekto 0x2900;
struct {
u8 code[16]; // 0=x0A, A=0x0D, B=0x0E, C=0x0F, D=0x00, #=0x0C *=0x0B
} dtmf_codes[15];
// #seekto 0x29F0;
struct {
u8 dtmfspeed_on; //list with 50..2000ms in steps of 10
u8 dtmfspeed_off; //list with 50..2000ms in steps of 10
u8 unknown0[14];
u8 inspection[16];
u8 monitor[16];
u8 alarmcode[16];
u8 stun[16];
u8 kill[16];
u8 revive[16];
u8 unknown1[16];
u8 unknown2[16];
u8 unknown3[16];
u8 unknown4[16];
u8 unknown5[16];
u8 unknown6[16];
u8 unknown7[16];
u8 masterid[16];
u8 viceid[16];
u8 unused01:7,
mastervice:1;
u8 unused02:3,
mrevive:1,
mkill:1,
mstun:1,
mmonitor:1,
minspection:1;
u8 unused03:3,
vrevive:1,
vkill:1,
vstun:1,
vmonitor:1,
vinspection:1;
u8 unused04:6,
txdisable:1,
rxdisable:1;
u8 groupcode;
u8 spacecode;
u8 delayproctime; // * 100 + 100ms
u8 resettime; // * 100 + 100ms
} dtmf_settings;
#seekto 0x2D00;
struct {
struct {
ul16 freq1;
u8 unused01[6];
ul16 freq2;
u8 unused02[6];
} _2tone_encode[15];
u8 duration_1st_tone; // *10ms
u8 duration_2nd_tone; // *10ms
u8 duration_gap; // *10ms
u8 unused03[13];
struct {
struct {
u8 dec; // one out of LIST_2TONE_DEC
u8 response; // one out of LIST_2TONE_RESPONSE
u8 alert; // 1-16
} decs[4];
u8 unused04[4];
} _2tone_decode[15];
u8 unused05[16];
struct {
ul16 freqA;
ul16 freqB;
ul16 freqC;
ul16 freqD;
// unknown what those values mean, but they are
// derived from configured frequencies
ul16 derived_from_freqA; // 2304000/freqA
ul16 derived_from_freqB; // 2304000/freqB
ul16 derived_from_freqC; // 2304000/freqC
ul16 derived_from_freqD; // 2304000/freqD
}freqs[15];
u8 reset_time; // * 100 + 100ms - 100-8000ms
} _2tone;
#seekto 0x3D80;
struct {
u8 vhf_low[3];
u8 vhf_high[3];
u8 unknown1[4];
u8 unknown2[6];
u8 vhf2_low[3];
u8 vhf2_high[3];
u8 unknown3[4];
u8 unknown4[6];
u8 uhf_low[3];
u8 uhf_high[3];
u8 unknown5[4];
u8 unknown6[6];
u8 uhf2_low[3];
u8 uhf2_high[3];
} ranges;
#seekto 0x3F70;
struct {
char fp[6];
} fingerprint;
"""
class BTechColor(BTechMobileCommon):
"""BTECH's Color LCD Mobile and alike radios"""
COLOR_LCD = True
NAME_LENGTH = 8
LIST_TMR = LIST_TMR16
def process_mmap(self):
"""Process the mem map into the mem object"""
# Get it
self._memobj = bitwise.parse(COLOR_MEM_FORMAT, self._mmap)
# load specific parameters from the radio image
self.set_options()
def set_options(self):
"""This is to read the options from the image and set it in the
environment, for now just the limits of the freqs in the VHF/UHF
ranges"""
# setting the correct ranges for each radio type
ranges = self._memobj.ranges
# the normal dual bands
vhf = _decode_ranges(ranges.vhf_low, ranges.vhf_high)
uhf = _decode_ranges(ranges.uhf_low, ranges.uhf_high)
# DEBUG
LOG.info("Radio ranges: VHF %d to %d" % vhf)
LOG.info("Radio ranges: UHF %d to %d" % uhf)
# the additional bands
if self.MODEL in ["UV-25X4", "KT7900D"]:
# 200 MHz band
vhf2 = _decode_ranges(ranges.vhf2_low, ranges.vhf2_high)
LOG.info("Radio ranges: VHF(220) %d to %d" % vhf2)
self._220_range = vhf2
# 350 MHz band
uhf2 = _decode_ranges(ranges.uhf2_low, ranges.uhf2_high)
LOG.info("Radio ranges: UHF(350) %d to %d" % uhf2)
self._350_range = uhf2
# set the class with the real data
self._vhf_range = vhf
self._uhf_range = uhf
# Declaring Aliases (Clones of the real radios)
class SKT8900D(chirp_common.Alias):
VENDOR = "Surecom"
MODEL = "S-KT8900D"
class QB25(chirp_common.Alias):
VENDOR = "Radioddity"
MODEL = "QB25"
# real radios
@directory.register
class UV25X2(BTechColor):
"""Baofeng Tech UV25X2"""
MODEL = "UV-25X2"
BANDS = 2
_vhf_range = (130000000, 180000000)
_uhf_range = (400000000, 521000000)
_magic = MSTRING_UV25X2
_fileid = [UV25X2_fp]
@directory.register
class UV25X2_G2(UV25X2):
"""Baofeng Tech UV25X2_G2"""
MODEL = "UV-25X2_G2"
_fileid = [UV25X2G2_fp]
@directory.register
class UV25X4(BTechColor):
"""Baofeng Tech UV25X4"""
MODEL = "UV-25X4"
BANDS = 4
_vhf_range = (130000000, 180000000)
_220_range = (200000000, 271000000)
_uhf_range = (400000000, 521000000)
_350_range = (350000000, 391000000)
_magic = MSTRING_UV25X4
_fileid = [UV25X4_fp]
@directory.register
class UV25X4_G2(UV25X4):
"""Baofeng Tech UV25X4_G2"""
MODEL = "UV-25X4_G2"
_fileid = [UV25X4G2_fp]
@directory.register
class UV50X2(BTechColor):
"""Baofeng Tech UV50X2"""
MODEL = "UV-50X2"
BANDS = 2
_vhf_range = (130000000, 180000000)
_uhf_range = (400000000, 521000000)
_magic = MSTRING_UV25X2
_fileid = [UV50X2_fp]
_power_levels = [chirp_common.PowerLevel("High", watts=50),
chirp_common.PowerLevel("Low", watts=10)]
@directory.register
class UV50X2_G2(BTechColor):
"""Baofeng Tech UV50X2_G2"""
MODEL = "UV-50X2_G2"
BANDS = 2
_vhf_range = (130000000, 180000000)
_uhf_range = (400000000, 521000000)
_magic = MSTRING_UV25X2
_fileid = [UV50X2_fp]
_power_levels = [chirp_common.PowerLevel("High", watts=50),
chirp_common.PowerLevel("Low", watts=10)]
@classmethod
def match_model(cls, filedata, filename):
# This model is only ever matched via metadata
return False
@directory.register
class KT7900D(BTechColor):
"""QYT KT7900D"""
VENDOR = "QYT"
MODEL = "KT7900D"
BANDS = 4
LIST_TMR = LIST_TMR15
_vhf_range = (136000000, 175000000)
_220_range = (200000000, 271000000)
_uhf_range = (400000000, 481000000)
_350_range = (350000000, 371000000)
_magic = MSTRING_KT8900D
_fileid = [KT7900D_fp, KT7900D_fp1, KT7900D_fp2, KT7900D_fp3, KT7900D_fp4,
KT7900D_fp5, KT7900D_fp6, KT7900D_fp7, KT7900D_fp8, KT7900D_fp9,
QB25_fp, KT7900D_fp10]
# Clones
ALIASES = [SKT8900D, QB25]
@directory.register
class KT8900D(BTechColor):
"""QYT KT8900D"""
VENDOR = "QYT"
MODEL = "KT8900D"
_block_size = 0x10 # Bug 11851 BLOCKSIZE fix for KT-8900D
BANDS = 2
LIST_TMR = LIST_TMR15
_vhf_range = (136000000, 175000000)
_uhf_range = (400000000, 481000000)
_magic = MSTRING_KT8900D
_fileid = [KT8900D_fp5, KT8900D_fp4, KT8900D_fp3, KT8900D_fp2, KT8900D_fp1,
KT8900D_fp]
# Clones
ALIASES = [OTGRadioV1]
@directory.register
class KT5800(BTechColor):
"""QYT KT5800"""
VENDOR = "QYT"
MODEL = "KT5800"
BANDS = 2
LIST_TMR = LIST_TMR15
_vhf_range = (136000000, 175000000)
_uhf_range = (400000000, 481000000)
_magic = MSTRING_KT8900D
_fileid = [KT5800_fp]
@directory.register
class KT980PLUS(BTechColor):
"""QYT KT980PLUS"""
VENDOR = "QYT"
MODEL = "KT980PLUS"
BANDS = 2
LIST_TMR = LIST_TMR15
_vhf_range = (136000000, 175000000)
_uhf_range = (400000000, 481000000)
_magic = MSTRING_KT8900D
_fileid = [KT980PLUS_fp1, KT980PLUS_fp]
_power_levels = [chirp_common.PowerLevel("High", watts=75),
chirp_common.PowerLevel("Low", watts=55)]
@classmethod
def match_model(cls, filedata, filename):
# This model is only ever matched via metadata
return False
@directory.register
class DB25G(BTechColor):
"""Radioddity DB25-G"""
VENDOR = "Radioddity"
MODEL = "DB25-G"
BANDS = 2
LIST_TMR = LIST_TMR15
_vhf_range = (136000000, 175000000)
_uhf_range = (400000000, 481000000)
_magic = MSTRING_KT8900D
_fileid = [DB25G_fp1, DB25G_fp]
_gmrs = True
_power_levels = [chirp_common.PowerLevel("High", watts=25),
chirp_common.PowerLevel("Mid", watts=15),
chirp_common.PowerLevel("Low", watts=5)]
def validate_memory(self, mem):
msgs = super().validate_memory(mem)
_msg_duplex = 'Duplex must be "off" for this frequency'
_msg_offset = 'Only simplex or +5 MHz offset allowed on GMRS'
if mem.freq in GMRS_FREQS3:
if mem.duplex and mem.offset != 5000000:
msgs.append(chirp_common.ValidationWarning(_msg_offset))
if mem.duplex and mem.duplex != "+":
msgs.append(chirp_common.ValidationWarning(_msg_offset))
return msgs
def check_set_memory_immutable_policy(self, existing, new):
existing.immutable = []
super().check_set_memory_immutable_policy(existing, new)
@classmethod
def match_model(cls, filedata, filename):
# This model is only ever matched via metadata
return False
GMRS_MEM_FORMAT = """
// #seekto 0x0000;
struct {
lbcd rxfreq[4];
lbcd txfreq[4];
ul16 rxtone;
ul16 txtone;
u8 unknown0:4,
scode:4;
u8 unknown1:2,
spmute:2,
unknown2:2,
optsig:2;
u8 unknown3:3,
scramble:1,
unknown4:2,
power:2;
u8 unknown5:1,
wide:1,
unknown6:2,
bcl:1,
add:1,
pttid:2;
} memory[256];
// #seekto 0x1000;
struct {
char name[7];
u8 unknown1[9];
} names[256];
#seekto 0x2400;
struct {
u8 period; // one out of LIST_5TONE_STANDARD_PERIODS
u8 group_tone;
u8 repeat_tone;
u8 unused[13];
} _5tone_std_settings[15];
#seekto 0x2500;
struct {
u8 frame1[5];
u8 frame2[5];
u8 frame3[5];
u8 standard; // one out of LIST_5TONE_STANDARDS
} _5tone_codes[15];
// #seekto 0x25F0;
struct {
u8 _5tone_delay1; // * 10ms
u8 _5tone_delay2; // * 10ms
u8 _5tone_delay3; // * 10ms
u8 _5tone_first_digit_ext_length;
u8 unknown1;
u8 unknown2;
u8 unknown3;
u8 unknown4;
u8 decode_standard;
u8 unknown5:5,
_5tone_decode_call_frame3:1,
_5tone_decode_call_frame2:1,
_5tone_decode_call_frame1:1;
u8 unknown6:5,
_5tone_decode_disp_frame3:1,
_5tone_decode_disp_frame2:1,
_5tone_decode_disp_frame1:1;
u8 decode_reset_time; // * 100 + 100ms
} _5tone_settings;
// #seekto 0x2900;
struct {
u8 code[16]; // 0=x0A, A=0x0D, B=0x0E, C=0x0F, D=0x00, #=0x0C *=0x0B
} dtmf_codes[15];
#seekto 0x29F0;
struct {
u8 dtmfspeed_on; //list with 50..2000ms in steps of 10
u8 dtmfspeed_off; //list with 50..2000ms in steps of 10
u8 unknown0[14];
u8 inspection[16];
u8 monitor[16];
u8 alarmcode[16];
u8 stun[16];
u8 kill[16];
u8 revive[16];
u8 unknown1[16];
u8 unknown2[16];
u8 unknown3[16];
u8 unknown4[16];
u8 unknown5[16];
u8 unknown6[16];
u8 unknown7[16];
u8 masterid[16];
u8 viceid[16];
u8 unused01:7,
mastervice:1;
u8 unused02:3,
mrevive:1,
mkill:1,
mstun:1,
mmonitor:1,
minspection:1;
u8 unused03:3,
vrevive:1,
vkill:1,
vstun:1,
vmonitor:1,
vinspection:1;
u8 unused04:6,
txdisable:1,
rxdisable:1;
u8 groupcode;
u8 spacecode;
u8 delayproctime; // * 100 + 100ms
u8 resettime; // * 100 + 100ms
} dtmf_settings;
#seekto 0x2D00;
struct {
struct {
ul16 freq1;
u8 unused01[6];
ul16 freq2;
u8 unused02[6];
} _2tone_encode[15];
u8 duration_1st_tone; // *10ms
u8 duration_2nd_tone; // *10ms
u8 duration_gap; // *10ms
u8 unused03[13];
struct {
struct {
u8 dec; // one out of LIST_2TONE_DEC
u8 response; // one out of LIST_2TONE_RESPONSE
u8 alert; // 1-16
} decs[4];
u8 unused04[4];
} _2tone_decode[15];
u8 unused05[16];
struct {
ul16 freqA;
ul16 freqB;
ul16 freqC;
ul16 freqD;
// unknown what those values mean, but they are
// derived from configured frequencies
ul16 derived_from_freqA; // 2304000/freqA
ul16 derived_from_freqB; // 2304000/freqB
ul16 derived_from_freqC; // 2304000/freqC
ul16 derived_from_freqD; // 2304000/freqD
}freqs[15];
u8 reset_time; // * 100 + 100ms - 100-8000ms
} _2tone;
#seekto 0x3000;
struct {
u8 freq[8];
char broadcast_station_name[6];
u8 unknown[2];
} fm_radio_preset[16];
"""
GMRS_MEM_FORMAT_PT2 = """
#seekto 0x3280;
struct {
u8 unknown1;
u8 vfomr;
u8 unused3281:7,
keylock:1;
u8 unknown2;
u8 unknown3:4,
vfomren:1,
unknown4:1,
reseten:1,
menuen:1;
u8 unknown5[11];
u8 dispab;
u8 unknown6[2];
u8 smenu;
u8 unknown7[7];
u8 vfomra;
u8 vfomrb;
u8 vfomrc;
u8 vfomrd;
u8 mrcha;
u8 mrchb;
u8 mrchc;
u8 mrchd;
} settings2;
struct settings_vfo {
u8 freq[8];
u8 offset[6];
u8 unknown2[2];
ul16 rxtone;
ul16 txtone;
u8 scode;
u8 spmute;
u8 optsig;
u8 unused1:7,
scramble:1;
u8 wide;
u8 power;
u8 shiftd;
u8 step;
u8 unknown3[4];
};
#seekto 0x3300;
struct {
struct settings_vfo a;
struct settings_vfo b;
struct settings_vfo c;
struct settings_vfo d;
} vfo;
#seekto 0x33B0;
struct {
char line[16];
} static_msg;
#seekto 0x3D80;
struct {
u8 vhf_low[3];
u8 vhf_high[3];
u8 unknown1[4];
u8 unknown2[6];
u8 vhf2_low[3];
u8 vhf2_high[3];
u8 unknown3[4];
u8 unknown4[6];
u8 uhf_low[3];
u8 uhf_high[3];
u8 unknown5[4];
u8 unknown6[6];
u8 uhf2_low[3];
u8 uhf2_high[3];
} ranges;
#seekto 0x3F70;
struct {
char fp[6];
} fingerprint;
"""
GMRS_ORIG_MEM_FORMAT = """
#seekto 0x3200;
struct {
u8 tmr;
u8 unknown1;
u8 sql;
u8 unknown2;
u8 unused3204:7,
autolk:1;
u8 tot;
u8 apo;
u8 unknown3;
u8 abr;
u8 unused3209:7,
beep:1;
u8 unknown4[4];
u8 dtmfst;
u8 unknown5[2];
u8 screv;
u8 unknown6[2];
u8 pttid;
u8 pttlt;
u8 unknown7;
u8 emctp;
u8 emcch;
u8 unusedE19:7,
sigbp:1;
u8 vox;
u8 camdf;
u8 cbmdf;
u8 ccmdf;
u8 cdmdf;
u8 langua;
u8 sync;
u8 stfc;
u8 mffc;
u8 sfafc;
u8 sfbfc;
u8 sfcfc;
u8 sfdfc;
u8 subfc;
u8 fmfc;
u8 sigfc;
u8 modfc;
u8 menufc;
u8 txfc;
u8 txdisp;
u8 unknown9[5];
u8 anil;
u8 reps;
u8 repm;
u8 tmrmr;
u8 unusedE37:7,
ste:1;
u8 rpste;
u8 rptdl;
u8 dtmfg;
u8 mgain;
u8 skiptx;
u8 scmode;
u8 tmrtx;
u8 unknown10;
u8 earpho;
} settings;
"""
GMRS_V2_MEM_FORMAT = """
#seekto 0x3200;
struct {
u8 tmr;
u8 unknown1;
u8 sql;
u8 unknown2;
u8 unused3204:7,
autolk:1;
u8 tot;
u8 apo;
u8 unknown3;
u8 abr;
u8 unused3209:7,
beep:1;
u8 unknown4[4];
u8 dtmfst;
u8 unknown5[2];
u8 screv;
u8 unknown6[2];
u8 pttid;
u8 pttlt;
u8 unknown7;
u8 emctp;
u8 emcch;
u8 unusedE19:7,
sigbp:1;
u8 unknown8; // vox
u8 camdf;
u8 cbmdf;
u8 ccmdf;
u8 cdmdf;
u8 vox; // langua
u8 sync;
u8 stfc;
u8 mffc;
u8 sfafc;
u8 sfbfc;
u8 sfcfc;
u8 sfdfc;
u8 subfc;
u8 fmfc;
u8 sigfc;
u8 modfc;
u8 menufc;
u8 txfc;
u8 unknown9[5];
u8 anil;
u8 reps;
u8 repm;
u8 tmrmr;
u8 unusedE37:7,
ste:1;
u8 rpste;
u8 rptdl;
u8 dtmfg;
u8 mgain;
u8 skiptx;
u8 scmode;
u8 tmrtx;
u8 unknown10[2];
u8 earpho;
} settings;
"""
class BTechGMRS(BTechMobileCommon):
"""BTECH's GMRS Mobile"""
COLOR_LCD = True
COLOR_LCD2 = True
NAME_LENGTH = 7
UPLOAD_MEM_SIZE = 0X3400
def process_mmap(self):
"""Process the mem map into the mem object"""
# Get it
mem_format = GMRS_MEM_FORMAT + GMRS_ORIG_MEM_FORMAT + \
GMRS_MEM_FORMAT_PT2
self._memobj = bitwise.parse(mem_format, self._mmap)
# load specific parameters from the radio image
self.set_options()
def set_options(self):
"""This is to read the options from the image and set it in the
environment, for now just the limits of the freqs in the VHF/UHF
ranges"""
# setting the correct ranges for each radio type
ranges = self._memobj.ranges
# the normal dual bands
vhf = _decode_ranges(ranges.vhf_low, ranges.vhf_high)
uhf = _decode_ranges(ranges.uhf_low, ranges.uhf_high)
# DEBUG
LOG.info("Radio ranges: VHF %d to %d" % vhf)
LOG.info("Radio ranges: UHF %d to %d" % uhf)
# set the class with the real data
self._vhf_range = vhf
self._uhf_range = uhf
# real radios
@directory.register
class GMRS50X1(BTechGMRS):
"""Baofeng Tech GMRS50X1"""
MODEL = "GMRS-50X1"
BANDS = 2
LIST_TMR = LIST_TMR16
_power_levels = [chirp_common.PowerLevel("High", watts=50),
chirp_common.PowerLevel("Mid", watts=10),
chirp_common.PowerLevel("Low", watts=5)]
_vhf_range = (136000000, 175000000)
_uhf_range = (400000000, 521000000)
_upper = 255
_magic = MSTRING_GMRS50X1
_fileid = [GMRS50X1_fp1, GMRS50X1_fp]
_gmrs = True
def validate_memory(self, mem):
msgs = super().validate_memory(mem)
_msg_duplex = 'Duplex must be "off" for this frequency'
_msg_offset = 'Only simplex or +5 MHz offset allowed on GMRS'
if not (mem.number >= 1 and mem.number <= 30):
if mem.duplex != "off":
msgs.append(chirp_common.ValidationWarning(_msg_duplex))
return msgs
def check_set_memory_immutable_policy(self, existing, new):
if not (new.number >= 1 and new.number <= 30):
existing.immutable = []
super().check_set_memory_immutable_policy(existing, new)
@directory.register
class GMRS50V2(BTechGMRS):
"""Baofeng Tech GMRS50V2"""
MODEL = "GMRS-50V2"
BANDS = 2
LIST_TMR = LIST_TMR16
_power_levels = [chirp_common.PowerLevel("High", watts=50),
chirp_common.PowerLevel("Mid", watts=10),
chirp_common.PowerLevel("Low", watts=5)]
_vhf_range = (136000000, 175000000)
_uhf_range = (400000000, 521000000)
_upper = 255
_magic = MSTRING_GMRS50X1
_fileid = [GMRS50X1_fp1, GMRS50X1_fp]
_gmrs = True
def process_mmap(self):
"""Process the mem map into the mem object"""
# Get it
mem_format = GMRS_MEM_FORMAT + GMRS_V2_MEM_FORMAT + \
GMRS_MEM_FORMAT_PT2
self._memobj = bitwise.parse(mem_format, self._mmap)
# load specific parameters from the radio image
self.set_options()
def validate_memory(self, mem):
msgs = super().validate_memory(mem)
_msg_duplex = 'Duplex must be "off" for this frequency'
_msg_offset = 'Only simplex or +5 MHz offset allowed on GMRS'
if mem.freq not in GMRS_FREQS:
if mem.duplex != "off":
msgs.append(chirp_common.ValidationWarning(_msg_duplex))
elif mem.duplex:
if mem.duplex and mem.offset != 5000000:
msgs.append(chirp_common.ValidationWarning(_msg_offset))
return msgs
def check_set_memory_immutable_policy(self, existing, new):
existing.immutable = []
super().check_set_memory_immutable_policy(existing, new)
@classmethod
def match_model(cls, filedata, filename):
# This model is only ever matched via metadata
return False
COLORHT_MEM_FORMAT = """
// #seekto 0x0000;
struct {
lbcd rxfreq[4];
lbcd txfreq[4];
ul16 rxtone;
ul16 txtone;
u8 unknown0:4,
scode:4;
u8 unknown1:2,
spmute:2,
unknown2:2,
optsig:2;
u8 unknown3:3,
scramble:1,
unknown4:3,
power:1;
u8 unknown5:1,
wide:1,
unknown6:2,
bcl:1,
add:1,
pttid:2;
} memory[200];
#seekto 0x0E00;
struct {
u8 tmr;
u8 unknownE01;
u8 sql;
u8 unknownE03[2];
u8 tot;
u8 unusedE06:7,
save:1;
u8 unknownE07;
u8 abr;
u8 unusedE09:7,
beep:1;
u8 unknownE0A[4];
u8 unusedE0E:7,
dsub:1;
u8 unusedE0F:7,
dtmfst:1;
u8 screv;
u8 unknownE11[3];
u8 pttid;
u8 unknownE15;
u8 pttlt;
u8 unknownE17;
u8 emctp;
u8 emcch;
u8 unusedE1A:7,
sigbp:1;
u8 unknownE1B;
u8 camdf;
u8 cbmdf;
u8 ccmdf;
u8 cdmdf;
u8 langua;
u8 voice;
u8 vox;
u8 voxt;
u8 sync; // BTech radios use this as the display sync setting
// other radios use this as the auto keypad lock setting
u8 stfc;
u8 mffc;
u8 sfafc;
u8 sfbfc;
u8 sfcfc;
u8 sfdfc;
u8 subfc;
u8 fmfc;
u8 sigfc;
u8 menufc;
u8 txfc;
u8 rxfc;
u8 unknownE31[5];
u8 anil;
u8 reps;
u8 tmrmr;
u8 unusedE39:7,
ste:1;
u8 rpste;
u8 rptdl;
u8 dtmfg;
u8 tmrtx;
} settings;
#seekto 0x0E80;
struct {
u8 unknown1;
u8 vfomr;
u8 unusedE82:7,
keylock:1;
u8 unknown2;
u8 unknown3:4,
vfomren:1,
unknown4:1,
reseten:1,
menuen:1;
u8 unknown5[11];
u8 dispab;
u8 unknown6[2];
u8 menu;
u8 unknown7[7];
u8 vfomra;
u8 vfomrb;
u8 vfomrc;
u8 vfomrd;
u8 mrcha;
u8 mrchb;
u8 mrchc;
u8 mrchd;
} settings2;
struct settings_vfo {
u8 freq[8];
u8 offset[6];
u8 unknown2[2];
ul16 rxtone;
ul16 txtone;
u8 scode;
u8 spmute;
u8 optsig;
u8 unused1:7,
scramble:1;
u8 wide;
u8 power;
u8 shiftd;
u8 step;
u8 unknown3[4];
};
#seekto 0x0F00;
struct {
struct settings_vfo a;
struct settings_vfo b;
struct settings_vfo c;
struct settings_vfo d;
} vfo;
#seekto 0x0FE0;
struct {
char line[16];
} static_msg;
#seekto 0x1000;
struct {
char name[8];
u8 unknown1[8];
} names[200];
#seekto 0x2400;
struct {
u8 period; // one out of LIST_5TONE_STANDARD_PERIODS
u8 group_tone;
u8 repeat_tone;
u8 unused[13];
} _5tone_std_settings[15];
#seekto 0x2500;
struct {
u8 frame1[5];
u8 frame2[5];
u8 frame3[5];
u8 standard; // one out of LIST_5TONE_STANDARDS
} _5tone_codes[15];
// #seekto 0x25F0;
struct {
u8 _5tone_delay1; // * 10ms
u8 _5tone_delay2; // * 10ms
u8 _5tone_delay3; // * 10ms
u8 _5tone_first_digit_ext_length;
u8 unknown1;
u8 unknown2;
u8 unknown3;
u8 unknown4;
u8 decode_standard;
u8 unknown5:5,
_5tone_decode_call_frame3:1,
_5tone_decode_call_frame2:1,
_5tone_decode_call_frame1:1;
u8 unknown6:5,
_5tone_decode_disp_frame3:1,
_5tone_decode_disp_frame2:1,
_5tone_decode_disp_frame1:1;
u8 decode_reset_time; // * 100 + 100ms
} _5tone_settings;
#seekto 0x2900;
struct {
u8 code[16]; // 0=x0A, A=0x0D, B=0x0E, C=0x0F, D=0x00, #=0x0C *=0x0B
} dtmf_codes[15];
// #seekto 0x29F0;
struct {
u8 dtmfspeed_on; //list with 50..2000ms in steps of 10
u8 dtmfspeed_off; //list with 50..2000ms in steps of 10
u8 unknown0[14];
u8 inspection[16];
u8 monitor[16];
u8 alarmcode[16];
u8 stun[16];
u8 kill[16];
u8 revive[16];
u8 unknown1[16];
u8 unknown2[16];
u8 unknown3[16];
u8 unknown4[16];
u8 unknown5[16];
u8 unknown6[16];
u8 unknown7[16];
u8 masterid[16];
u8 viceid[16];
u8 unused01:7,
mastervice:1;
u8 unused02:3,
mrevive:1,
mkill:1,
mstun:1,
mmonitor:1,
minspection:1;
u8 unused03:3,
vrevive:1,
vkill:1,
vstun:1,
vmonitor:1,
vinspection:1;
u8 unused04:6,
txdisable:1,
rxdisable:1;
u8 groupcode;
u8 spacecode;
u8 delayproctime; // * 100 + 100ms
u8 resettime; // * 100 + 100ms
} dtmf_settings;
#seekto 0x2D00;
struct {
struct {
ul16 freq1;
u8 unused01[6];
ul16 freq2;
u8 unused02[6];
} _2tone_encode[15];
u8 duration_1st_tone; // *10ms
u8 duration_2nd_tone; // *10ms
u8 duration_gap; // *10ms
u8 unused03[13];
struct {
struct {
u8 dec; // one out of LIST_2TONE_DEC
u8 response; // one out of LIST_2TONE_RESPONSE
u8 alert; // 1-16
} decs[4];
u8 unused04[4];
} _2tone_decode[15];
u8 unused05[16];
struct {
ul16 freqA;
ul16 freqB;
ul16 freqC;
ul16 freqD;
// unknown what those values mean, but they are
// derived from configured frequencies
ul16 derived_from_freqA; // 2304000/freqA
ul16 derived_from_freqB; // 2304000/freqB
ul16 derived_from_freqC; // 2304000/freqC
ul16 derived_from_freqD; // 2304000/freqD
}freqs[15];
u8 reset_time; // * 100 + 100ms - 100-8000ms
} _2tone;
#seekto 0x3D80;
struct {
u8 vhf_low[3];
u8 vhf_high[3];
u8 unknown1[4];
u8 unknown2[6];
u8 vhf2_low[3];
u8 vhf2_high[3];
u8 unknown3[4];
u8 unknown4[6];
u8 uhf_low[3];
u8 uhf_high[3];
u8 unknown5[4];
u8 unknown6[6];
u8 uhf2_low[3];
u8 uhf2_high[3];
} ranges;
#seekto 0x3F70;
struct {
char fp[6];
} fingerprint;
"""
class QYTColorHT(BTechMobileCommon):
"""QTY's Color LCD Handheld and alike radios"""
COLOR_LCD = True
COLOR_LCD3 = True
NAME_LENGTH = 8
LIST_TMR = LIST_TMR15
def process_mmap(self):
"""Process the mem map into the mem object"""
# Get it
self._memobj = bitwise.parse(COLORHT_MEM_FORMAT, self._mmap)
# load specific parameters from the radio image
self.set_options()
def set_options(self):
"""This is to read the options from the image and set it in the
environment, for now just the limits of the freqs in the VHF/UHF
ranges"""
# setting the correct ranges for each radio type
ranges = self._memobj.ranges
# the normal dual bands
vhf = _decode_ranges(ranges.vhf_low, ranges.vhf_high)
uhf = _decode_ranges(ranges.uhf_low, ranges.uhf_high)
# DEBUG
LOG.info("Radio ranges: VHF %d to %d" % vhf)
LOG.info("Radio ranges: UHF %d to %d" % uhf)
# the additional bands
if self.MODEL in ["KT-8R"]:
# 200 MHz band
vhf2 = _decode_ranges(ranges.vhf2_low, ranges.vhf2_high)
LOG.info("Radio ranges: VHF(220) %d to %d" % vhf2)
self._220_range = vhf2
# 350 MHz band
uhf2 = _decode_ranges(ranges.uhf2_low, ranges.uhf2_high)
LOG.info("Radio ranges: UHF(350) %d to %d" % uhf2)
self._350_range = uhf2
# set the class with the real data
self._vhf_range = vhf
self._uhf_range = uhf
# real radios
@directory.register
class KT8R(QYTColorHT):
"""QYT KT8R"""
VENDOR = "QYT"
MODEL = "KT-8R"
BANDS = 4
LIST_TMR = LIST_TMR16
_vhf_range = (136000000, 175000000)
_220_range = (200000000, 261000000)
_uhf_range = (400000000, 481000000)
_350_range = (350000000, 391000000)
_magic = MSTRING_KT8R
_fileid = [KT8R_fp2, KT8R_fp1, KT8R_fp, KT8R_fp3]
_power_levels = [chirp_common.PowerLevel("High", watts=5),
chirp_common.PowerLevel("Low", watts=1)]
COLOR9900_MEM_FORMAT = """
// #seekto 0x0000;
struct {
lbcd rxfreq[4];
lbcd txfreq[4];
ul16 rxtone;
ul16 txtone;
u8 unknown0:4,
scode:4;
u8 unknown1:2,
spmute:2,
unknown2:2,
optsig:2;
u8 unknown3:3,
scramble:1,
unknown4:2,
power:2;
u8 unknown5:1,
wide:1,
unknown6:2,
bcl:1,
add:1,
pttid:2;
} memory[200];
#seekto 0x0E00;
struct {
u8 tmr;
u8 unknown1;
u8 sql;
u8 unknown2[2];
u8 tot;
u8 volume;
u8 unknown3;
u8 abr;
u8 unusedE09:7,
beep:1;
u8 unknown4[4];
u8 unusedE0E:7,
dsub:1;
u8 unusedE0F:7,
dtmfst:1;
u8 unknown_e10;
u8 unknown_e11;
u8 screv;
u8 unknown_e13;
u8 unknown_e14;
u8 pttid;
u8 pttlt;
u8 unknown7;
u8 emctp;
u8 emcch;
u8 unusedE1A:7,
sigbp:1;
u8 unknown8;
u8 camdf;
u8 cbmdf;
u8 ccmdf;
u8 language;
u8 tmrtx;
u8 vox;
u8 voxt;
u8 unusedE23:7,
autolock:1;
u8 asfc;
u8 mainfc;
u8 a_fc;
u8 b_fc;
u8 c_fc;
u8 subfc;
u8 battfc;
u8 sigfc;
u8 menufc;
u8 txfc;
u8 rxfc;
u8 unknown_e2f;
u8 unknown_e30;
u8 unknown9[3];
u8 anil;
u8 reps;
u8 tmrmr;
u8 unusedE37:7,
ste:1;
u8 rpste;
u8 rptdl;
u8 dtmfg;
} settings;
#seekto 0x0E80;
struct {
u8 unknown1;
u8 vfomr;
u8 unusedE82:7,
keylock:1;
u8 unknown2;
u8 unknown3:4,
vfomren:1,
unknown4:1,
reseten:1,
menuen:1;
u8 unknown5[11];
u8 dispab;
u8 unknown6[2];
u8 menu;
u8 unknown7[7];
u8 vfomra;
u8 vfomrb;
u8 vfomrc;
u8 vfomrd;
u8 mrcha;
u8 mrchb;
u8 mrchc;
u8 mrchd;
} settings2;
struct settings_vfo {
u8 freq[8];
u8 offset[6];
u8 unknown2[2];
ul16 rxtone;
ul16 txtone;
u8 scode;
u8 spmute;
u8 optsig;
u8 unused1:7,
scramble:1;
u8 wide;
u8 power;
u8 shiftd;
u8 step;
u8 unknown3[4];
};
#seekto 0x0F00;
struct {
struct settings_vfo a;
struct settings_vfo b;
struct settings_vfo c;
struct settings_vfo d;
} vfo;
// #seekto 0x0F80;
struct {
char line1[8];
char line2[8];
char line3[8];
char line4[8];
char line5[8];
char line6[8];
char line7[8];
char line8[8];
} poweron_msg;
#seekto 0x0FE0;
struct {
char line[16];
} static_msg;
#seekto 0x1000;
struct {
char name[7];
u8 unknown1[9];
} names[200];
#seekto 0x2400;
struct {
u8 period; // one out of LIST_5TONE_STANDARD_PERIODS
u8 group_tone;
u8 repeat_tone;
u8 unused[13];
} _5tone_std_settings[15];
#seekto 0x2500;
struct {
u8 frame1[5];
u8 frame2[5];
u8 frame3[5];
u8 standard; // one out of LIST_5TONE_STANDARDS
} _5tone_codes[15];
// #seekto 0x25F0;
struct {
u8 _5tone_delay1; // * 10ms
u8 _5tone_delay2; // * 10ms
u8 _5tone_delay3; // * 10ms
u8 _5tone_first_digit_ext_length;
u8 unknown1;
u8 unknown2;
u8 unknown3;
u8 unknown4;
u8 decode_standard;
u8 unknown5:5,
_5tone_decode_call_frame3:1,
_5tone_decode_call_frame2:1,
_5tone_decode_call_frame1:1;
u8 unknown6:5,
_5tone_decode_disp_frame3:1,
_5tone_decode_disp_frame2:1,
_5tone_decode_disp_frame1:1;
u8 decode_reset_time; // * 100 + 100ms
} _5tone_settings;
#seekto 0x2900;
struct {
u8 code[16]; // 0=x0A, A=0x0D, B=0x0E, C=0x0F, D=0x00, #=0x0C *=0x0B
} dtmf_codes[15];
// #seekto 0x29F0;
struct {
u8 dtmfspeed_on; //list with 50..2000ms in steps of 10 // 9f0
u8 dtmfspeed_off; //list with 50..2000ms in steps of 10 // 9f1
u8 unknown0[14]; // 9f2-9ff
u8 inspection[16]; // a00-a0f
u8 monitor[16]; // a10-a1f
u8 alarmcode[16]; // a20-a2f
u8 stun[16]; // a30-a3f
u8 kill[16]; // a40-a4f
u8 revive[16]; // a50-a5f
u8 unknown1[16]; // a60-a6f
u8 unknown2[16]; // a70-a7f
u8 unknown3[16]; // a80-a8f
u8 unknown4[16]; // a90-a9f
u8 unknown5[16]; // aa0-aaf
u8 unknown6[16]; // ab0-abf
u8 unknown7[16]; // ac0-acf
u8 masterid[16]; // ad0-adf
u8 viceid[16]; // ae0-aef
u8 unused01:7, // af0
mastervice:1;
u8 unused02:3, // af1
mrevive:1,
mkill:1,
mstun:1,
mmonitor:1,
minspection:1;
u8 unused03:3, // af2
vrevive:1,
vkill:1,
vstun:1,
vmonitor:1,
vinspection:1;
u8 unused04:6, // af3
txdisable:1,
rxdisable:1;
u8 groupcode; // af4
u8 spacecode; // af5
u8 delayproctime; // * 100 + 100ms // af6
u8 resettime; // * 100 + 100ms // af7
} dtmf_settings;
#seekto 0x2D00;
struct {
struct {
ul16 freq1;
u8 unused01[6];
ul16 freq2;
u8 unused02[6];
} _2tone_encode[15];
u8 duration_1st_tone; // *10ms
u8 duration_2nd_tone; // *10ms
u8 duration_gap; // *10ms
u8 unused03[13];
struct {
struct {
u8 dec; // one out of LIST_2TONE_DEC
u8 response; // one out of LIST_2TONE_RESPONSE
u8 alert; // 1-16
} decs[4];
u8 unused04[4];
} _2tone_decode[15];
u8 unused05[16];
struct {
ul16 freqA;
ul16 freqB;
ul16 freqC;
ul16 freqD;
// unknown what those values mean, but they are
// derived from configured frequencies
ul16 derived_from_freqA; // 2304000/freqA
ul16 derived_from_freqB; // 2304000/freqB
ul16 derived_from_freqC; // 2304000/freqC
ul16 derived_from_freqD; // 2304000/freqD
}freqs[15];
u8 reset_time; // * 100 + 100ms - 100-8000ms
} _2tone;
#seekto 0x3D80;
struct {
u8 vhf_low[3];
u8 vhf_high[3];
u8 unknown1[4];
u8 unknown2[6];
u8 vhf2_low[3];
u8 vhf2_high[3];
u8 unknown3[4];
u8 unknown4[6];
u8 uhf_low[3];
u8 uhf_high[3];
u8 unknown5[4];
u8 unknown6[6];
u8 uhf2_low[3];
u8 uhf2_high[3];
} ranges;
#seekto 0x3F70;
struct {
char fp[6];
} fingerprint;
"""
COLOR20V2_MEM_FORMAT = """
// #seekto 0x0000;
struct {
lbcd rxfreq[4];
lbcd txfreq[4];
ul16 rxtone;
ul16 txtone;
u8 unknown0:4,
scode:4;
u8 unknown1:2,
spmute:2,
unknown2:2,
optsig:2;
u8 unknown3:3,
scramble:1,
unknown4:2,
power:2;
u8 unknown5:1,
wide:1,
unknown6:2,
bcl:1,
add:1,
pttid:2;
} memory[200];
#seekto 0x0E00;
struct {
u8 tmr;
u8 unknown1;
u8 sql;
u8 unknown2;
u8 unusedE04:7,
autolock:1;
u8 tot;
u8 apo;
u8 unknown3;
u8 abr;
u8 unusedE09:7,
beep:1;
u8 unknown4[4];
u8 unusedE0E:7,
dtmfst:1;
u8 unknown5[2];
u8 screv;
u8 unknown6[2];
u8 pttid;
u8 pttlt;
u8 unknown7;
u8 emctp;
u8 emcch;
u8 unusedE19:7,
sigbp:1;
u8 unknown8;
u8 camdf;
u8 cbmdf;
u8 ccmdf;
u8 vox;
u8 voxt;
u8 sync;
u8 asfc;
u8 mainfc;
u8 a_fc;
u8 b_fc;
u8 c_fc;
u8 subfc;
u8 battfc;
u8 sigfc;
u8 menufc;
u8 txfc;
u8 rxfc;
u8 repsw;
u8 unusedE2D:7,
dsub:1;
u8 unknown9[5];
u8 anil;
u8 reps;
u8 repm;
u8 tmrmr;
u8 unusedE37:7,
ste:1;
u8 rpste;
u8 rptdl;
u8 dtmfg;
u8 mgain;
u8 skiptx;
u8 scmode;
u8 tmrtx;
u8 volume;
u8 unknown_10;
u8 unusedE41:7,
save:1;
} settings;
#seekto 0x0E80;
struct {
u8 unknown1;
u8 vfomr;
u8 unusedE82:7,
keylock:1;
u8 unknown2;
u8 unknown3:4,
vfomren:1,
unknown4:1,
reseten:1,
menuen:1;
u8 unknown5[11];
u8 dispab;
u8 unknown6[2];
u8 menu;
u8 unknown7[7];
u8 vfomra;
u8 vfomrb;
u8 vfomrc;
u8 vfomrd;
u8 mrcha;
u8 mrchb;
u8 mrchc;
u8 mrchd;
} settings2;
struct settings_vfo {
u8 freq[8];
u8 offset[6];
u8 unknown2[2];
ul16 rxtone;
ul16 txtone;
u8 scode;
u8 spmute;
u8 optsig;
u8 unused1:7,
scramble:1;
u8 wide;
u8 power;
u8 shiftd;
u8 step;
u8 unknown3[4];
};
#seekto 0x0F00;
struct {
struct settings_vfo a;
struct settings_vfo b;
struct settings_vfo c;
struct settings_vfo d;
} vfo;
// #seekto 0x0F80;
struct {
char line1[8];
char line2[8];
char line3[8];
char line4[8];
char line5[8];
char line6[8];
char line7[8];
char line8[8];
} poweron_msg;
#seekto 0x0FE0;
struct {
char line[16];
} static_msg;
#seekto 0x1000;
struct {
char name[7];
u8 unknown1[9];
} names[200];
#seekto 0x2400;
struct {
u8 period; // one out of LIST_5TONE_STANDARD_PERIODS
u8 group_tone;
u8 repeat_tone;
u8 unused[13];
} _5tone_std_settings[15];
#seekto 0x2500;
struct {
u8 frame1[5];
u8 frame2[5];
u8 frame3[5];
u8 standard; // one out of LIST_5TONE_STANDARDS
} _5tone_codes[15];
// #seekto 0x25F0;
struct {
u8 _5tone_delay1; // * 10ms
u8 _5tone_delay2; // * 10ms
u8 _5tone_delay3; // * 10ms
u8 _5tone_first_digit_ext_length;
u8 unknown1;
u8 unknown2;
u8 unknown3;
u8 unknown4;
u8 decode_standard;
u8 unknown5:5,
_5tone_decode_call_frame3:1,
_5tone_decode_call_frame2:1,
_5tone_decode_call_frame1:1;
u8 unknown6:5,
_5tone_decode_disp_frame3:1,
_5tone_decode_disp_frame2:1,
_5tone_decode_disp_frame1:1;
u8 decode_reset_time; // * 100 + 100ms
} _5tone_settings;
#seekto 0x2900;
struct {
u8 code[16]; // 0=x0A, A=0x0D, B=0x0E, C=0x0F, D=0x00, #=0x0C *=0x0B
} dtmf_codes[15];
// #seekto 0x29F0;
struct {
u8 dtmfspeed_on; //list with 50..2000ms in steps of 10 // 9f0
u8 dtmfspeed_off; //list with 50..2000ms in steps of 10 // 9f1
u8 unknown0[14]; // 9f2-9ff
u8 inspection[16]; // a00-a0f
u8 monitor[16]; // a10-a1f
u8 alarmcode[16]; // a20-a2f
u8 stun[16]; // a30-a3f
u8 kill[16]; // a40-a4f
u8 revive[16]; // a50-a5f
u8 unknown1[16]; // a60-a6f
u8 unknown2[16]; // a70-a7f
u8 unknown3[16]; // a80-a8f
u8 unknown4[16]; // a90-a9f
u8 unknown5[16]; // aa0-aaf
u8 unknown6[16]; // ab0-abf
u8 unknown7[16]; // ac0-acf
u8 masterid[16]; // ad0-adf
u8 viceid[16]; // ae0-aef
u8 unused01:7, // af0
mastervice:1;
u8 unused02:3, // af1
mrevive:1,
mkill:1,
mstun:1,
mmonitor:1,
minspection:1;
u8 unused03:3, // af2
vrevive:1,
vkill:1,
vstun:1,
vmonitor:1,
vinspection:1;
u8 unused04:6, // af3
txdisable:1,
rxdisable:1;
u8 groupcode; // af4
u8 spacecode; // af5
u8 delayproctime; // * 100 + 100ms // af6
u8 resettime; // * 100 + 100ms // af7
} dtmf_settings;
#seekto 0x2D00;
struct {
struct {
ul16 freq1;
u8 unused01[6];
ul16 freq2;
u8 unused02[6];
} _2tone_encode[15];
u8 duration_1st_tone; // *10ms
u8 duration_2nd_tone; // *10ms
u8 duration_gap; // *10ms
u8 unused03[13];
struct {
struct {
u8 dec; // one out of LIST_2TONE_DEC
u8 response; // one out of LIST_2TONE_RESPONSE
u8 alert; // 1-16
} decs[4];
u8 unused04[4];
} _2tone_decode[15];
u8 unused05[16];
struct {
ul16 freqA;
ul16 freqB;
ul16 freqC;
ul16 freqD;
// unknown what those values mean, but they are
// derived from configured frequencies
ul16 derived_from_freqA; // 2304000/freqA
ul16 derived_from_freqB; // 2304000/freqB
ul16 derived_from_freqC; // 2304000/freqC
ul16 derived_from_freqD; // 2304000/freqD
}freqs[15];
u8 reset_time; // * 100 + 100ms - 100-8000ms
} _2tone;
#seekto 0x3D80;
struct {
u8 vhf_low[3];
u8 vhf_high[3];
u8 unknown1[4];
u8 unknown2[6];
u8 vhf2_low[3];
u8 vhf2_high[3];
u8 unknown3[4];
u8 unknown4[6];
u8 uhf_low[3];
u8 uhf_high[3];
u8 unknown5[4];
u8 unknown6[6];
u8 uhf2_low[3];
u8 uhf2_high[3];
} ranges;
#seekto 0x3F70;
struct {
char fp[6];
} fingerprint;
"""
class BTechColorWP(BTechMobileCommon):
"""BTECH's Color WP Mobile and alike radios"""
COLOR_LCD = True
COLOR_LCD4 = True
NAME_LENGTH = 7
LIST_TMR = LIST_TMR7
def set_options(self):
"""This is to read the options from the image and set it in the
environment, for now just the limits of the freqs in the VHF/UHF
ranges"""
# setting the correct ranges for each radio type
ranges = self._memobj.ranges
# the normal dual bands
vhf = _decode_ranges(ranges.vhf_low, ranges.vhf_high)
uhf = _decode_ranges(ranges.uhf_low, ranges.uhf_high)
# DEBUG
LOG.info("Radio ranges: VHF %d to %d" % vhf)
LOG.info("Radio ranges: UHF %d to %d" % uhf)
# set the class with the real data
self._vhf_range = vhf
self._uhf_range = uhf
# real radios
@directory.register
class KTWP12(BTechColorWP):
"""QYT KT-WP12"""
VENDOR = "QYT"
MODEL = "KT-WP12"
BANDS = 2
UPLOAD_MEM_SIZE = 0X3100
_power_levels = [chirp_common.PowerLevel("High", watts=25),
chirp_common.PowerLevel("Low", watts=5)]
_upper = 199
_magic = MSTRING_KTWP12
_fileid = [KTWP12_fp]
_gmrs = False
def process_mmap(self):
"""Process the mem map into the mem object"""
# Get it
self._memobj = bitwise.parse(COLOR9900_MEM_FORMAT, self._mmap)
# load specific parameters from the radio image
self.set_options()
@directory.register
class WP9900(BTechColorWP):
"""Anysecu WP-9900"""
VENDOR = "Anysecu"
MODEL = "WP-9900"
BANDS = 2
UPLOAD_MEM_SIZE = 0X3100
_power_levels = [chirp_common.PowerLevel("High", watts=25),
chirp_common.PowerLevel("Low", watts=5)]
_upper = 199
_magic = MSTRING_KTWP12
_fileid = [WP9900_fp]
_gmrs = False
def process_mmap(self):
"""Process the mem map into the mem object"""
# Get it
self._memobj = bitwise.parse(COLOR9900_MEM_FORMAT, self._mmap)
# load specific parameters from the radio image
self.set_options()
@directory.register
class KT5000(BTechColorWP):
"""QYT KT-5000"""
# TODO: memory map is not EXACTLY the same as in WP12. E.g. setting
# "C display mode" overwrites VOX setting
VENDOR = "QYT"
MODEL = "KT-5000"
BANDS = 2
UPLOAD_MEM_SIZE = 0X3100
_power_levels = [chirp_common.PowerLevel("High", watts=25),
chirp_common.PowerLevel("Low", watts=5)]
_upper = 199
_magic = MSTRING_KT5000
_fileid = [KT5000_fp]
_gmrs = False
def process_mmap(self):
"""Process the mem map into the mem object"""
# Get it
self._memobj = bitwise.parse(COLOR9900_MEM_FORMAT, self._mmap)
# load specific parameters from the radio image
self.set_options()
def get_features(self):
rf = super().get_features()
rf.has_settings = False
return rf
@directory.register
class GMRS20V2(BTechColorWP):
"""Baofeng Tech GMRS-20V2"""
MODEL = "GMRS-20V2"
BANDS = 2
UPLOAD_MEM_SIZE = 0X3100
_power_levels = [chirp_common.PowerLevel("High", watts=20),
chirp_common.PowerLevel("", watts=6),
chirp_common.PowerLevel("Low", watts=5)]
_upper = 199
_magic = MSTRING_GMRS20V2
_fileid = [GMRS20V2_fp2, GMRS20V2_fp1, GMRS20V2_fp]
_gmrs = True
def validate_memory(self, mem):
msgs = super().validate_memory(mem)
_msg_duplex = 'Duplex must be "off" for this frequency'
_msg_offset = 'Only simplex or +5 MHz offset allowed on GMRS'
if mem.freq not in GMRS_FREQS:
if mem.duplex != "off":
msgs.append(chirp_common.ValidationWarning(_msg_duplex))
elif mem.duplex:
if mem.duplex and mem.offset != 5000000:
msgs.append(chirp_common.ValidationWarning(_msg_offset))
return msgs
def check_set_memory_immutable_policy(self, existing, new):
existing.immutable = []
super().check_set_memory_immutable_policy(existing, new)
def process_mmap(self):
"""Process the mem map into the mem object"""
# Get it
self._memobj = bitwise.parse(COLOR20V2_MEM_FORMAT, self._mmap)
# load specific parameters from the radio image
self.set_options()
chirp-20261030/chirp/drivers/fake.py 0000644 0001750 0001750 00000015220 14665027664 016115 0 ustar hibby hibby import importlib
import logging
import os
import struct
import time
from chirp import chirp_common
from chirp import directory
from chirp import errors
from chirp import settings
from chirp import util
LOG = logging.getLogger(__name__)
class FakeLiveRadio(chirp_common.LiveRadio):
VENDOR = 'CHIRP'
MODEL = 'Fake Live Radio'
def __init__(self, *a, **k):
super().__init__(*a, **k)
self.settings = {'knob': 5}
self.memories = []
for i in range(1, 12):
m = chirp_common.Memory(i, empty=i > 5, name='channel %i' % i)
m.freq = 146520000
self.memories.append(m)
self.memories[-1].extd_number = 'Special'
def get_features(self):
rf = chirp_common.RadioFeatures()
rf.memory_bounds = (1, 10)
rf.has_settings = True
# We don't even implement this interface, but the point of this
# is to make sure the GUI doesn't choke on live radios.
rf.has_bank = True
rf.valid_name_length = 8
rf.valid_characters = chirp_common.CHARSET_ASCII
rf.valid_special_chans = ['Special']
return rf
def get_memory(self, number):
if number == 'Special':
number = len(self.memories)
m = self.memories[number - 1]
if isinstance(m, chirp_common.Memory) and m.number != number:
LOG.error('fake driver found %i instead of %i',
m.number, number)
return m
def set_memory(self, mem):
LOG.info('Set memory %s' % mem)
self.memories[mem.number - 1] = mem.dupe()
def get_settings(self):
g = settings.RadioSettingGroup('top', 'Some Settings')
g.append(
settings.RadioSetting(
'knob', 'A knob',
settings.RadioSettingValueInteger(0, 10,
self.settings['knob'])))
return settings.RadioSettings(g)
def set_settings(self, rs):
for e in rs:
if isinstance(e, settings.RadioSetting):
self.settings[e.name] = e.value
else:
self.set_settings(e)
class FakeLiveSlowRadio(FakeLiveRadio):
VARIANT = 'Slow'
def get_memory(self, number):
time.sleep(0.5)
return super().get_memory(number)
def set_memory(self, mem):
time.sleep(5)
return super().set_memory(mem)
def get_settings(self):
time.sleep(1)
return super().get_settings()
def set_settings(self, settings):
time.sleep(2)
return super().set_settings(settings)
class FakeLiveRadioWithErrors(FakeLiveRadio):
VARIANT = 'Errors'
def __init__(self, *a, **k):
super().__init__(*a, **k)
self.memories[2] = errors.RadioError
self.memories[3] = Exception
def get_memory(self, number):
m = super().get_memory(number)
if isinstance(m, type):
raise m('Error getting %i' % number)
else:
return m
def set_memory(self, mem):
if not mem.empty and mem.freq < 145000000:
raise errors.RadioError('Out of range')
else:
return super().set_memory(mem)
class FakeCloneFail(chirp_common.CloneModeRadio):
VENDOR = 'CHIRP'
MODEL = 'Fake Clone Radio'
VARIANT = 'Errors'
def sync_in(self):
s = chirp_common.Status()
s.max = 100
s.cur = 10
s.msg = 'Gonna fail...'
self.status_fn(s)
raise errors.RadioError('This always fails')
class FakeKenwoodSerial:
def __init__(self, *a, **k):
self._rbuf = b''
def write(self, buffer):
LOG.debug('Write: %r' % buffer)
if buffer.startswith(b'ID'):
self._rbuf += b'ID TH-F7\r'
elif buffer.startswith(b'MR 0,001'):
self._rbuf += \
b'MR 0,001,00146520000,0,0,0,0,0,0,00,00,000,000000000,0,0\r'
elif buffer.startswith(b'MNA 001\r'):
self._rbuf += b'MNA 001,Foo\r'
elif buffer.startswith(b'MNA 0'):
self._rbuf += buffer
elif buffer.startswith(b'MW 0'):
self._rbuf += b'MW\r'
else:
self._rbuf += b'N\r'
def read(self, n):
ret = self._rbuf[:n]
self._rbuf = self._rbuf[n:]
return ret
# NOTE: This is not complete, it's just enough to do the ident dance with
# these radios
class FakeUV17Serial:
def get_radio(self):
baofeng_uv17 = importlib.import_module('chirp.drivers.baofeng_uv17')
self.rclass = baofeng_uv17.UV17
def __init__(self, *a, **k):
self._sbuf = b''
self.get_radio()
imgfn = os.path.join(os.path.dirname(__file__), '..', '..',
'tests', 'images',
'%s_%s.img' % (self.rclass.VENDOR,
self.rclass.MODEL))
LOG.debug('Opening %s' % imgfn)
try:
with open(imgfn, 'rb') as f:
self._img = f.read()
LOG.debug('Loaded image size 0x%x', len(self._img))
except FileNotFoundError:
LOG.error('Unable to open image, fixture will not work')
self._img = b''
def write(self, buffer):
baofeng_uv17Pro = importlib.import_module(
'chirp.drivers.baofeng_uv17Pro')
if buffer == self.rclass._magic:
LOG.debug('Sent first magic')
self._sbuf += self.rclass._fingerprint
elif buffer.startswith(b'R'):
LOG.debug('Got: %s' % util.hexprint(buffer))
cmd, addr, blen = struct.unpack('>cHb', buffer)
resp = struct.pack('>cHb', b'W', addr, blen)
block = self._img[addr:addr + blen] or (b'\x00' * blen)
LOG.debug('Sending block length 0x%x', len(block))
self._sbuf += resp + baofeng_uv17Pro._crypt(1, block)
else:
for magic, rlen in self.rclass._magics:
if buffer == magic:
LOG.debug('Sent magic %r' % magic)
self._sbuf += b' ' * rlen
return
LOG.debug('Unrecognized ident string %r' % buffer)
self._sbuf += b'\x15' * 32
def read(self, length):
chunk = self._sbuf[:length]
self._sbuf = self._sbuf[length:]
return chunk
def close(self):
pass
class FakeUV17ProSerial(FakeUV17Serial):
def get_radio(self):
baofeng_uv17Pro = importlib.import_module(
'chirp.drivers.baofeng_uv17Pro')
self.rclass = baofeng_uv17Pro.UV17Pro
def register_fakes():
directory.register(FakeLiveRadio)
directory.register(FakeLiveSlowRadio)
directory.register(FakeLiveRadioWithErrors)
directory.register(FakeCloneFail)
chirp-20261030/chirp/drivers/fd268.py 0000644 0001750 0001750 00000070735 14747120722 016042 0 ustar hibby hibby # Copyright 2015 Pavel Milanes CO7WT
#
# 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 .
import struct
import logging
from chirp import chirp_common, directory, memmap, errors, util, bitwise
from chirp.settings import RadioSettingGroup, RadioSetting, \
RadioSettingValueBoolean, RadioSettingValueList, \
RadioSettingValueString, RadioSettings
LOG = logging.getLogger(__name__)
MEM_FORMAT = """
#seekto 0x0010;
struct {
lbcd rx_freq[4];
lbcd tx_freq[4];
lbcd rx_tone[2];
lbcd tx_tone[2];
u8 unknown:4,
scrambler:1,
unknown1:1,
unknown2:1,
busy_lock:1;
u8 unknown3[3];
} memory[99];
#seekto 0x0640;
struct {
lbcd vrx_freq[4];
lbcd vtx_freq[4];
lbcd vrx_tone[2];
lbcd vtx_tone[2];
u8 shift_plus:1,
shift_minus:1,
unknown11:2,
scramble:1,
unknown12:1,
unknown13:1,
busy_lock:1;
u8 unknown14[3];
} vfo;
#seekto 0x07B0;
struct {
u8 ani_mode;
char ani[3];
u8 unknown21[12];
u8 unknown22:5,
bw1:1, // twin setting of bw (LCD "romb")
bs1:1, // twin setting of bs (LCD "S")
warning1:1; // twin setting of warning (LCD "Tune")
u8 sql;
u8 monitorval;
u8 tot;
u8 powerrank;
u8 unknown23[3];
u8 unknown24[8];
char model[8];
u8 unknown26[8];
u8 step;
u8 unknown27:2,
power:1,
lamp:1,
lamp_auto:1,
key:1,
monitor:1,
bw:1;
u8 unknown28:3,
warning:1,
bs:1,
unknown29:1,
wmem:1,
wvfo:1;
u8 active_ch;
u8 unknown30[4];
u8 unknown31[4];
bbcd vfo_shift[4];
} settings;
"""
MEM_SIZE = 0x0800
CMD_ACK = "\x06"
BLOCK_SIZE = 0x08
POWER_LEVELS = ["Low", "High"]
LIST_SQL = ["Off"] + ["%s" % x for x in range(1, 10)]
LIST_TOT = ["Off"] + ["%s" % x for x in range(10, 100, 10)]
ONOFF = ["Off", "On"]
STEPF = ["5", "10", "6.25", "12.5", "25"]
ACTIVE_CH = ["%s" % x for x in range(1, 100)]
KEY_LOCK = ["Automatic", "Manual"]
BW = ["Narrow", "Wide"]
W_MODE = ["VFO", "Memory"]
VSHIFT = ["None", "-", "+"]
POWER_RANK = ["%s" % x for x in range(0, 28)]
ANI = ["Off", "BOT", "EOT", "Both"]
def raw_recv(radio, amount):
"""Raw read from the radio device"""
data = ""
try:
data = radio.pipe.read(amount)
except:
raise errors.RadioError("Error reading data from radio")
return data
def raw_send(radio, data):
"""Raw write to the radio device"""
try:
data = radio.pipe.write(data)
except:
raise errors.RadioError("Error writing data to radio")
def make_frame(cmd, addr, length=BLOCK_SIZE):
"""Pack the info in the format it likes"""
return struct.pack(">BHB", ord(cmd), addr, length)
def check_ack(r, text):
"""Check for a correct ACK from radio, raising error 'Text'
if something was wrong"""
ack = raw_recv(r, 1)
if ack != CMD_ACK:
raise errors.RadioError(text)
else:
return True
def send(radio, frame, data=""):
"""Generic send data to the radio"""
raw_send(radio, frame)
if data != "":
raw_send(radio, data)
check_ack(radio, "Radio didn't ack the last block of data")
def recv(radio):
"""Generic receive data from the radio, return just data"""
# you must get it all 12 at once (4 header + 8 data)
rxdata = raw_recv(radio, 12)
if (len(rxdata) != 12):
raise errors.RadioError(
"Radio sent %i bytes, we expected 12" % (len(rxdata)))
else:
data = rxdata[4:]
send(radio, CMD_ACK)
check_ack(radio, "Radio didn't ack the sended data")
return data
def do_magic(radio):
"""Try to get the radio in program mode, the factory software
(FDX-288) tries up to ~16 times to get the correct response,
we will do the same, but with a lower count."""
tries = 8
# UI information
status = chirp_common.Status()
status.cur = 0
status.max = tries
status.msg = "Linking to radio, please wait."
radio.status_fn(status)
# every byte of this magic chain must be send separately
magic = "\x02PROGRA"
# start the fun, finger crossed please...
for a in range(0, tries):
# UI update
status.cur = a
radio.status_fn(status)
for i in range(0, len(magic)):
send(radio, magic[i])
# Now you get a x06 of ACK
ack = raw_recv(radio, 1)
if ack == CMD_ACK:
return True
return False
def do_program(radio):
"""Feidaxin program mode and identification dance"""
# try to get the radio in program mode
ack = do_magic(radio)
if not ack:
erc = "Radio did not accept program mode, "
erc += "check your cable and radio; then try again."
raise errors.RadioError(erc)
# now we request identification
send(radio, "M")
send(radio, "\x02")
ident = raw_recv(radio, 8)
# ################ WARNING ##########################################
# Feidaxin radios has a "send id" procedure in the initial handshake
# but it's worthless, once you do a hardware reset the ident area
# get all 0xFF.
#
# Even FDX-288 software appears to match the model by any other
# mean, so I detected on a few images that the 3 first bytes are
# unique to each radio model, so for now we use that method until
# proven otherwise
# ###################################################################
LOG.debug("Radio's ID string:")
LOG.debug(util.hexprint(ident))
# final ACK
send(radio, CMD_ACK)
check_ack(radio, "Radio refused to enter programming mode")
def do_download(radio):
""" The download function """
do_program(radio)
# UI progress
status = chirp_common.Status()
status.cur = 0
status.max = MEM_SIZE
status.msg = "Cloning from radio..."
radio.status_fn(status)
data = ""
for addr in range(0x0000, MEM_SIZE, BLOCK_SIZE):
send(radio, make_frame("R", addr))
d = recv(radio)
data += d
# UI Update
status.cur = addr
radio.status_fn(status)
return memmap.MemoryMap(data)
def do_upload(radio):
"""The upload function"""
do_program(radio)
# UI progress
status = chirp_common.Status()
status.cur = 0
status.max = MEM_SIZE
status.msg = "Cloning to radio..."
radio.status_fn(status)
for addr in range(0x0000, MEM_SIZE, BLOCK_SIZE):
send(radio, make_frame("W", addr),
radio.get_mmap()[addr:addr+BLOCK_SIZE])
# UI Update
status.cur = addr
radio.status_fn(status)
def model_match(cls, data):
"""Use a experimental guess to determine if the radio you just
downloaded or the img opened you is for this model"""
# Using a few imgs of some FD radio I found that the four first
# bytes it's like the model fingerprint, so we have to testing the
# model type with this experimental method so far.
fp = data[0:4]
return fp == cls._IDENT
class FeidaxinFD2x8yRadio(chirp_common.CloneModeRadio):
"""Feidaxin FD-268 & alike Radios"""
VENDOR = "Feidaxin"
MODEL = "FD-268 & alike Radios"
BAUD_RATE = 9600
NEEDS_COMPAT_SERIAL = True
_memsize = MEM_SIZE
_upper = 99
_VFO_DEFAULT = 0
_IDENT = b""
_active_ch = ACTIVE_CH
@classmethod
def get_prompts(cls):
rp = chirp_common.RadioPrompts()
rp.experimental = \
('The program mode of this radio has his tricks, '
'so this driver is *completely experimental*.')
rp.pre_download = _(
"This radio has a tricky way of enter into program mode,\n"
"even the original software has a few tries to get inside.\n"
"I will try 8 times (most of the time ~3 will doit) and this\n"
"can take a few seconds, if don't work, try again a few times.\n"
"If you can get into it, please check the radio and cable.\n")
rp.pre_upload = _(
"This radio has a tricky way of enter into program mode,\n"
"even the original software has a few tries to get inside.\n"
"I will try 8 times (most of the time ~3 will doit) and this\n"
"can take a few seconds, if don't work, try again a few times.\n"
"If you can get into it, please check the radio and cable.\n")
return rp
def get_features(self):
"""Return information about this radio's features"""
rf = chirp_common.RadioFeatures()
# this feature is READ ONLY by now.
rf.has_settings = True
rf.has_bank = False
rf.has_tuning_step = False
rf.has_name = False
rf.has_offset = True
rf.has_mode = False
rf.has_dtcs = True
rf.has_rx_dtcs = True
rf.has_dtcs_polarity = True
rf.has_ctone = True
rf.has_cross = True
rf.valid_duplexes = ["", "-", "+", "off"]
rf.valid_tmodes = ['', 'Tone', 'TSQL', 'DTCS', 'Cross']
# we have to remove "Tone->" because this is the same to "TQSL"
# I get a few days hitting the wall with my head about this...
rf.valid_cross_modes = [
"Tone->Tone",
"DTCS->",
"->DTCS",
"Tone->DTCS",
"DTCS->Tone",
"->Tone",
"DTCS->DTCS"]
# Power levels are global and no per channel, so disabled
# rf.valid_power_levels = POWER_LEVELS
# this radio has no skips
rf.valid_skips = []
# this radio modes are global and not per channel, so just FM
rf.valid_modes = ["FM"]
rf.valid_bands = [self._range]
rf.memory_bounds = (1, self._upper)
return rf
def sync_in(self):
"""Do a download of the radio eeprom"""
data = do_download(self)
# as the radio comm protocol's identification is useless
# we test the model after having the img
if not model_match(self, data):
# ok, wrong model, fire an error
erc = "EEPROM fingerprint don't match, check if you "
erc += "selected the right radio model."
raise errors.RadioError(erc)
# all ok
self._mmap = data
self.process_mmap()
def sync_out(self):
"""Do an upload to the radio eeprom"""
do_upload(self)
def process_mmap(self):
"""Process the memory object"""
self._memobj = bitwise.parse(MEM_FORMAT, self._mmap)
def get_raw_memory(self, number):
"""Return a raw representation of the memory object"""
return repr(self._memobj.memory[number])
def _decode_tone(self, val):
"""Parse the tone data to decode from mem, it returns"""
if val.get_raw(asbytes=False) == "\xFF\xFF":
return '', None, None
val = int(val)
if val >= 12000:
a = val - 12000
return 'DTCS', a, 'R'
elif val >= 8000:
a = val - 8000
return 'DTCS', a, 'N'
else:
a = val / 10.0
return 'Tone', a, None
def _encode_tone(self, memval, mode, value, pol):
"""Parse the tone data to encode from UI to mem"""
if mode == '':
memval[0].set_raw(0xFF)
memval[1].set_raw(0xFF)
elif mode == 'Tone':
memval.set_value(int(value * 10))
elif mode == 'DTCS':
flag = 0x80 if pol == 'N' else 0xC0
memval.set_value(value)
memval[1].set_bits(flag)
else:
raise Exception("Internal error: invalid mode `%s'" % mode)
def get_memory(self, number):
"""Extract a high-level memory object from the low-level
memory map, This is called to populate a memory in the UI"""
# Get a low-level memory object mapped to the image
_mem = self._memobj.memory[number - 1]
# Create a high-level memory object to return to the UI
mem = chirp_common.Memory()
# number
mem.number = number
# empty
if _mem.get_raw(asbytes=False)[0] == "\xFF":
mem.empty = True
return mem
# rx freq
mem.freq = int(_mem.rx_freq) * 10
# checking if tx freq is empty, this is "possible" on the
# original soft after a warning, and radio is happy with it
if _mem.tx_freq.get_raw(asbytes=False) == "\xFF\xFF\xFF\xFF":
mem.duplex = "off"
mem.offset = 0
else:
rx = int(_mem.rx_freq) * 10
tx = int(_mem.tx_freq) * 10
if tx == rx:
mem.offset = 0
mem.duplex = ""
else:
mem.duplex = rx > tx and "-" or "+"
mem.offset = abs(tx - rx)
# tone data
txtone = self._decode_tone(_mem.tx_tone)
rxtone = self._decode_tone(_mem.rx_tone)
chirp_common.split_tone_decode(mem, txtone, rxtone)
# Extra setting group, FD-268 don't uset it at all
# FD-288's & others do it?
mem.extra = RadioSettingGroup("extra", "Extra")
busy = RadioSetting("busy_lock", "Busy Channel Lockout",
RadioSettingValueBoolean(
bool(_mem.busy_lock)))
mem.extra.append(busy)
scramble = RadioSetting("scrambler", "Scrambler Option",
RadioSettingValueBoolean(
bool(_mem.scrambler)))
mem.extra.append(scramble)
# return mem
return mem
def set_memory(self, mem):
"""Store details about a high-level memory to the memory map
This is called when a user edits a memory in the UI"""
# Get a low-level memory object mapped to the image
_mem = self._memobj.memory[mem.number - 1]
# Empty memory
if mem.empty:
_mem.set_raw("\xFF" * 16)
return
# freq rx
_mem.rx_freq = mem.freq / 10
# freq tx
if mem.duplex == "+":
_mem.tx_freq = (mem.freq + mem.offset) / 10
elif mem.duplex == "-":
_mem.tx_freq = (mem.freq - mem.offset) / 10
elif mem.duplex == "off":
for i in range(0, 4):
_mem.tx_freq[i].set_raw("\xFF")
else:
_mem.tx_freq = mem.freq / 10
# tone data
((txmode, txtone, txpol), (rxmode, rxtone, rxpol)) = \
chirp_common.split_tone_encode(mem)
self._encode_tone(_mem.tx_tone, txmode, txtone, txpol)
self._encode_tone(_mem.rx_tone, rxmode, rxtone, rxpol)
# extra settings
for setting in mem.extra:
setattr(_mem, setting.get_name(), setting.value)
return mem
def get_settings(self):
"""Translate the bit in the mem_struct into settings in the UI"""
_mem = self._memobj
basic = RadioSettingGroup("basic", "Basic")
work = RadioSettingGroup("work", "Work Mode Settings")
top = RadioSettings(basic, work)
# Basic
sql = RadioSetting("settings.sql", "Squelch Level",
RadioSettingValueList(LIST_SQL, current_index=_mem.settings.sql))
basic.append(sql)
tot = RadioSetting("settings.tot", "Time out timer",
RadioSettingValueList(LIST_TOT, current_index=_mem.settings.tot))
basic.append(tot)
key_lock = RadioSetting("settings.key", "Keyboard Lock",
RadioSettingValueList(KEY_LOCK, current_index=_mem.settings.key))
basic.append(key_lock)
bw = RadioSetting("settings.bw", "Bandwidth",
RadioSettingValueList(BW, current_index=_mem.settings.bw))
basic.append(bw)
powerrank = RadioSetting("settings.powerrank", "Power output adjust",
RadioSettingValueList(POWER_RANK, current_index=_mem.settings.powerrank))
basic.append(powerrank)
lamp = RadioSetting("settings.lamp", "LCD Lamp",
RadioSettingValueBoolean(_mem.settings.lamp))
basic.append(lamp)
lamp_auto = RadioSetting("settings.lamp_auto", "LCD Lamp auto on/off",
RadioSettingValueBoolean(
_mem.settings.lamp_auto))
basic.append(lamp_auto)
bs = RadioSetting("settings.bs", "Battery Save",
RadioSettingValueBoolean(_mem.settings.bs))
basic.append(bs)
warning = RadioSetting("settings.warning", "Warning Alerts",
RadioSettingValueBoolean(_mem.settings.warning))
basic.append(warning)
monitor = RadioSetting("settings.monitor", "Monitor key",
RadioSettingValueBoolean(_mem.settings.monitor))
basic.append(monitor)
# Work mode settings
wmset = RadioSetting("settings.wmem", "VFO/MR Mode",
RadioSettingValueList(
W_MODE, current_index=_mem.settings.wmem))
work.append(wmset)
power = RadioSetting("settings.power", "Actual Power",
RadioSettingValueList(POWER_LEVELS, current_index=_mem.settings.power))
work.append(power)
active_ch = RadioSetting("settings.active_ch", "Work Channel",
RadioSettingValueList(ACTIVE_CH, current_index=_mem.settings.active_ch))
work.append(active_ch)
# vfo rx validation
if _mem.vfo.vrx_freq.get_raw(asbytes=False)[0] == "\xFF":
# if the vfo is not set, the UI cares about the
# length of the field, so set a default
LOG.debug("Setting VFO to default %s" % self._VFO_DEFAULT)
vfo = self._VFO_DEFAULT
else:
vfo = int(_mem.vfo.vrx_freq) * 10
# frequency apply_callback
def apply_freq(setting, obj):
"""Setting is the UI returned value, obj is the memmap object"""
value = chirp_common.parse_freq(str(setting.value))
obj.set_value(value / 10)
# preparing for callback on vrxfreq (handled also in a special )
vf_freq = RadioSetting("none.vrx_freq", "VFO frequency",
RadioSettingValueString(0, 10, chirp_common.
format_freq(vfo)))
vf_freq.set_apply_callback(apply_freq, _mem.vfo.vrx_freq)
work.append(vf_freq)
# shift works
# VSHIFT = ["None", "-", "+"]
sset = 0
if bool(_mem.vfo.shift_minus) is True:
sset = 1
elif bool(_mem.vfo.shift_plus) is True:
sset = 2
shift = RadioSetting("none.shift", "VFO Shift",
RadioSettingValueList(VSHIFT, current_index=sset))
work.append(shift)
# vfo shift validation if none set it to ZERO
if _mem.settings.vfo_shift.get_raw(asbytes=False)[0] == "\xFF":
# if the shift is not set, the UI cares about the
# length of the field, so set to zero
LOG.debug("VFO shift not set, setting it to zero")
vfo_shift = 0
else:
vfo_shift = int(_mem.settings.vfo_shift) * 10
offset = RadioSetting("none.vfo_shift", "VFO Offset",
RadioSettingValueString(0, 9, chirp_common.
format_freq(vfo_shift)))
work.append(offset)
step = RadioSetting("settings", "VFO step",
RadioSettingValueList(STEPF, current_index=_mem.settings.step))
work.append(step)
# FD-288 Family ANI settings
if "FD-288" in self.MODEL:
ani_mode = RadioSetting("ani_mode", "ANI ID",
RadioSettingValueList(ANI, current_index=_mem.settings.ani_mode))
work.append(ani_mode)
# it can't be \xFF
ani_value = str(_mem.settings.ani)
if ani_value == "\xFF\xFF\xFF":
ani_value = "200"
ani_value = "".join(x for x in ani_value if (int(x) >= 2 and
int(x) <= 9))
ani = RadioSetting("ani", "ANI (200-999)",
RadioSettingValueString(0, 3, ani_value))
work.append(ani)
return top
def set_settings(self, settings):
"""Set settings in the Chirp way."""
# special case: shift handling
def handle_shift(_vfo, settings):
"""_vfo is mmap obj for vfo, settings is for all UI settings"""
# reset the shift in the memmap
_vfo.shift_minus = 0
_vfo.shift_plus = 0
# parse and set if needed
rx = chirp_common.parse_freq(
str(settings["none.vrx_freq"]).split(":")[1])
offset = chirp_common.parse_freq(
str(settings["none.vfo_shift"]).split(":")[1])
shift = str(settings["none.shift"]).split(":")[1]
if shift == "None" or shift == "":
# no shift
_vfo.vtx_freq = rx / 10
if shift == "-":
# minus shift
_vfo.vtx_freq = (rx - offset) / 10
if shift == "+":
# plus shift
_vfo.vtx_freq = (rx + offset) / 10
# special case: narrow/wide at radio level and display icon
def handle_width(_settings, settings):
"""_settings is mmap obj for settings,
settings is all the UI settings"""
value = str(settings["settings.bw"]).split(":")[1]
# set bw in settings
if value == "Wide":
_settings.bw.set_value(1)
_settings.bw1.set_value(0)
else:
_settings.bw.set_value(0)
_settings.bw1.set_value(1)
# special case: battery save and display icon
def handle_bsave(_settings, settings):
"""_settings is mmap obj for settings,
settings is all the UI settings"""
value = str(settings["settings.bs"]).split(":")[1]
# set bs in settings
if value == "True":
_settings.bs.set_value(1)
_settings.bs1.set_value(0)
else:
_settings.bs.set_value(0)
_settings.bs1.set_value(1)
# special case: warning tones and display icon
def handle_warning(_settings, settings):
"""_settings is mmap obj for settings,
settings is all the UI settings"""
value = str(settings["settings.warning"]).split(":")[1]
# set warning in settings
if value == "True":
_settings.warning.set_value(1)
_settings.warning1.set_value(0)
else:
_settings.warning.set_value(0)
_settings.warning1.set_value(1)
_mem = self._memobj
for element in settings:
name = element.get_name()
if not isinstance(element, RadioSetting):
self.set_settings(element)
continue
if not element.changed():
continue
sett, name = name.split(".")
try:
# special cases
if name in ["vrx_freq", "vfo_shift", "shift"]:
# call local callback
print("Calling local callback for shift handling")
handle_shift(self._memobj.vfo, settings)
continue
if name == "bw":
# call local callback
print("Calling local callback for bw handling")
handle_width(self._memobj.settings, settings)
continue
if name == "bs":
# call local callback
print("Calling local callback for bs handling")
handle_bsave(self._memobj.settings, settings)
continue
if name == "warning":
# call local callback
print("Calling local callback for warning handling")
handle_warning(self._memobj.settings, settings)
continue
if element.has_apply_callback():
try:
element.run_apply_callback()
continue
except NotImplementedError:
raise
elif sett == "none":
LOG.debug("Setting %sett.%s is ignored" % (sett, name))
continue
elif element.value.get_mutable():
# value is mutable, find it on the mem space
LOG.debug("Setting %s.%s = %s" % (sett, name, str(
element.value)))
# process it
try:
obj = getattr(_mem, sett)
setattr(obj, name, element.value)
except AttributeError:
m = "Setting %s is not in this setting block" % name
LOG.debug(m)
except Exception:
LOG.debug(element.get_name())
raise
@classmethod
def match_model(cls, filedata, filename):
match_size = False
match_model = False
# testing the file data size
if len(filedata) == MEM_SIZE:
match_size = True
# testing the firmware fingerprint, this experimental
match_model = model_match(cls, filedata)
if match_size and match_model:
return True
else:
return False
# ##########################################################################3
# FD-268 family: this are the original tested models, FD-268B UHF
# was tested "remotely" with images thanks to AG5M
# I just have the 268A in hand to test
@directory.register
class FD268ARadio(FeidaxinFD2x8yRadio):
"""Feidaxin FD-268A Radio"""
MODEL = "FD-268A"
_range = (136000000, 174000000)
_VFO_DEFAULT = 145000000
_IDENT = b"\xFF\xEE\x46\xFF"
@directory.register
class FD268BRadio(FeidaxinFD2x8yRadio):
"""Feidaxin FD-268B Radio"""
MODEL = "FD-268B"
_range = (400000000, 470000000)
_VFO_DEFAULT = 439000000
_IDENT = b"\xFF\xEE\x47\xFF"
# #####################################################################
# FD-288 Family: the only difference from this family to the FD-268's
# are the ANI settings
# Tested hacking the FD-268A memory
@directory.register
class FD288ARadio(FeidaxinFD2x8yRadio):
"""Feidaxin FD-288A Radio"""
MODEL = "FD-288A"
_range = (136000000, 174000000)
_VFO_DEFAULT = 145000000
_IDENT = b"\xFF\xEE\x4B\xFF"
@directory.register
class FD288BRadio(FeidaxinFD2x8yRadio):
"""Feidaxin FD-288B Radio"""
MODEL = "FD-288B"
_range = (400000000, 470000000)
_VFO_DEFAULT = 439000000
_IDENT = b"\xFF\xEE\x4C\xFF"
# #####################################################################
# The following radios was tested hacking the FD-268A memory with
# the software and found to be clones of FD-268 ones
# please report any problems to driver author (see head of this file)
@directory.register
class FD150ARadio(FeidaxinFD2x8yRadio):
"""Feidaxin FD-150A Radio"""
MODEL = "FD-150A"
_range = (136000000, 174000000)
_VFO_DEFAULT = 145000000
_IDENT = b"\xFF\xEE\x45\xFF"
@directory.register
class FD160ARadio(FeidaxinFD2x8yRadio):
"""Feidaxin FD-160A Radio"""
MODEL = "FD-160A"
_range = (136000000, 174000000)
_VFO_DEFAULT = 145000000
_IDENT = b"\xFF\xEE\x48\xFF"
@directory.register
class FD450ARadio(FeidaxinFD2x8yRadio):
"""Feidaxin FD-450A Radio"""
MODEL = "FD-450A"
_range = (400000000, 470000000)
_VFO_DEFAULT = 439000000
_IDENT = b"\xFF\xEE\x44\xFF"
@directory.register
class FD460ARadio(FeidaxinFD2x8yRadio):
"""Feidaxin FD-460A Radio"""
MODEL = "FD-460A"
_range = (400000000, 470000000)
_VFO_DEFAULT = 439000000
_IDENT = b"\xFF\xEE\x4A\xFF"
@directory.register
class FD460UHRadio(FeidaxinFD2x8yRadio):
"""Feidaxin FD-460UH Radio"""
MODEL = "FD-460UH"
_range = (400000000, 480000000)
_VFO_DEFAULT = 439000000
_IDENT = b"\xFF\xF4\x44\xFF"
chirp-20261030/chirp/drivers/ft1500m.py 0000644 0001750 0001750 00000016262 14665027664 016312 0 ustar hibby hibby # Copyright 2019 Josh Small VK2HFF
# - Derived from ./ft1802.py 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 .
# FT-1500M Clone Procedure
# 1. Turn radio off.
# 2. Connect cable to mic jack.
# 3. Press and hold in the [MHz],[LOW] and [D/MR] keys
# while turning the radio on.
# 4. In Chirp, choose Download from Radio.
# 5. Press the [MHz(SET)] key to send image.
# or
# 4. Press the [D/MR(MW)] key ("--WAIT--" will appear on the LCD).
# 5. In Chirp, choose Upload to Radio.
from chirp.drivers import yaesu_clone
from chirp import chirp_common, bitwise, directory
from chirp.settings import RadioSetting, RadioSettingGroup, \
RadioSettingValueBoolean
MEM_FORMAT = """
#seekto 0x002a;
struct {
u8 unknown_f:4,
pskip:1,
skip:1,
visible:1,
valid:1;
} flags[130];
#seekto 0x00ca;
struct {
u8 unknown1a:2,
narrow:1,
clk_shift:1,
unknown1b:4;
u8 unknown2a:3,
unknown2b:2,
tune_step:3;
bbcd freq[3];
u8 tone;
u8 name[6];
u8 unknown3;
u8 offset;
u8 unknown4a:1,
unknown4b:1,
unknown4c:2,
unknown4d:4;
u8 unknown5a:2,
tmode:2,
power:2,
duplex:2;
} memory[130];
"""
MODES = ["FM", "NFM"]
TMODES = ["", "Tone", "TSQL"]
DUPLEX = ["", "-", "+"]
STEPS = [5.0, 10.0, 12.5, 15.0, 20.0, 25.0, 50.0, 100.0]
POWER_LEVELS = [chirp_common.PowerLevel("LOW1", watts=5),
chirp_common.PowerLevel("LOW2", watts=10),
chirp_common.PowerLevel("LOW3", watts=25),
chirp_common.PowerLevel("HIGH", watts=50),
]
CHARSET = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ +-/?()?_"
@directory.register
class FT1500Radio(yaesu_clone.YaesuCloneModeRadio):
"""Yaesu FT-1500M"""
VENDOR = "Yaesu"
MODEL = "FT-1500M"
BAUD_RATE = 9600
_model = "AH4N0"
_block_lengths = [10, 16, 3953]
_memsize = 3979
@classmethod
def get_prompts(cls):
rp = chirp_common.RadioPrompts()
rp.pre_download = _(
"1. Turn radio off.\n"
"2. Connect cable to mic jack.\n"
"3. Press and hold in the [MHz], [LOW], and [D/MR] keys\n"
" while turning the radio on.\n"
"4. After clicking OK , press the [MHz(SET)] key to send"
" image.\n")
rp.pre_upload = _(
"1. Turn radio off.\n"
"2. Connect cable to mic jack.\n"
"3. Press and hold in the [MHz], [LOW], and [D/MR] keys\n"
" while turning the radio on.\n"
"4. Press the [D/MR(MW)] key (\"--WAIT--\" will appear on the"
" LCD).\n")
return rp
def get_features(self):
rf = chirp_common.RadioFeatures()
rf.memory_bounds = (0, 129)
rf.can_odd_split = False
rf.has_ctone = False
rf.has_tuning_step = True
rf.has_dtcs_polarity = False
rf.has_bank = False
rf.valid_tuning_steps = STEPS
rf.valid_modes = MODES
rf.valid_tmodes = TMODES
rf.valid_bands = [(137000000, 174000000)]
rf.valid_power_levels = POWER_LEVELS
rf.valid_duplexes = DUPLEX
rf.valid_skips = ["", "S", "P"]
rf.valid_name_length = 6
rf.valid_characters = CHARSET
rf.has_cross = False
return rf
def _checksums(self):
return [yaesu_clone.YaesuChecksum(0, self._memsize-2)]
def process_mmap(self):
self._memobj = bitwise.parse(MEM_FORMAT, self._mmap)
def get_raw_memory(self, number):
return repr(self._memobj.memory[number]) + \
repr(self._memobj.flags[number])
def get_memory(self, number):
_mem = self._memobj.memory[number]
_flag = self._memobj.flags[number]
visible = _flag["visible"]
valid = _flag["valid"]
skip = _flag["skip"]
pskip = _flag["pskip"]
mem = chirp_common.Memory()
mem.number = number
if not visible:
mem.empty = True
if not valid:
mem.empty = True
return mem
mem.freq = chirp_common.fix_rounded_step(int(_mem.freq) * 1000)
mem.offset = chirp_common.fix_rounded_step(int(_mem.offset) * 100000)
mem.duplex = DUPLEX[_mem.duplex]
mem.tuning_step = STEPS[_mem.tune_step] or STEPS[0]
mem.tmode = TMODES[_mem.tmode]
mem.rtone = chirp_common.TONES[_mem.tone]
for i in _mem.name:
if i == 0xFF:
break
if i & 0x80 == 0x80:
mem.name += CHARSET[0x80 ^ int(i)]
else:
mem.name += CHARSET[i]
mem.name = mem.name.rstrip()
mem.mode = _mem.narrow and "NFM" or "FM"
mem.skip = pskip and "P" or skip and "S" or ""
mem.power = POWER_LEVELS[_mem.power]
mem.extra = RadioSettingGroup("extra", "Extra Settings")
rs = RadioSetting("clk_shift", "Clock Shift",
RadioSettingValueBoolean(_mem.clk_shift))
mem.extra.append(rs)
return mem
def set_memory(self, mem):
_mem = self._memobj.memory[mem.number]
_flag = self._memobj.flags[mem.number]
valid = _flag["valid"]
visible = _flag["visible"]
if not mem.empty and not valid:
_flag["valid"] = True
_mem.unknown1a = 0x00
_mem.clk_shift = 0x00
_mem.unknown1b = 0x00
_mem.unknown2a = 0x00
_mem.unknown2b = 0x00
_mem.unknown3 = 0x00
_mem.unknown4a = 0x00
_mem.unknown4b = 0x00
_mem.unknown4c = 0x00
_mem.unknown4d = 0x00
_mem.unknown5a = 0x00
if mem.empty and valid and not visible:
_flag["valid"] = False
return
_flag["visible"] = not mem.empty
if mem.empty:
return
_flag["valid"] = True
_mem.freq = mem.freq / 1000
_mem.offset = mem.offset / 100000
_mem.duplex = DUPLEX.index(mem.duplex)
_mem.tune_step = STEPS.index(mem.tuning_step)
_mem.tmode = TMODES.index(mem.tmode)
_mem.tone = chirp_common.TONES.index(mem.rtone)
_mem.name = [0xFF] * 6
for i in range(0, len(mem.name)):
try:
_mem.name[i] = CHARSET.index(mem.name[i])
except IndexError:
raise Exception("Character `%s' not supported")
_mem.narrow = MODES.index(mem.mode)
_mem.power = 3 if mem.power is None else POWER_LEVELS.index(mem.power)
_flag["pskip"] = mem.skip == "P"
_flag["skip"] = mem.skip == "S"
for element in mem.extra:
setattr(_mem, element.get_name(), element.value)
chirp-20261030/chirp/drivers/ft1802.py 0000644 0001750 0001750 00000020154 14665027664 016135 0 ustar hibby hibby # 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 .
# FT-1802 Clone Procedure
# 1. Turn radio off.
# 2. Connect cable to mic jack.
# 3. Press and hold in the [LOW(A/N)] key while turning the radio on.
# 4. In Chirp, choose Download from Radio.
# 5. Press the [MHz(SET)] key to send image.
# or
# 4. Press the [D/MR(MW)] key ("--WAIT--" will appear on the LCD).
# 5. In Chirp, choose Upload to Radio.
from chirp.drivers import yaesu_clone
from chirp import chirp_common, bitwise, directory
from chirp.settings import RadioSetting, RadioSettingGroup, \
RadioSettingValueBoolean
MEM_FORMAT = """
#seekto 0x06ea;
struct {
u8 odd_pskip:1,
odd_skip:1,
odd_visible:1,
odd_valid:1,
even_pskip:1,
even_skip:1,
even_visible:1,
even_valid:1;
} flags[100];
#seekto 0x076a;
struct {
u8 unknown1a:1,
step_changed:1,
narrow:1,
clk_shift:1,
unknown1b:4;
u8 unknown2a:2,
duplex:2,
unknown2b:1,
tune_step:3;
bbcd freq[3];
u8 power:2,
unknown3:3,
tmode:3;
u8 name[6];
bbcd offset[3];
u8 tone;
u8 dtcs;
u8 unknown4;
} memory[200];
"""
MODES = ["FM", "NFM"]
TMODES = ["", "Tone", "TSQL", "DTCS", "TSQL-R", "Cross"]
CROSS_MODES = ["DTCS->", "Tone->DTCS", "DTCS->Tone"]
DUPLEX = ["", "-", "+", "split"]
STEPS = [5.0, 10.0, 12.5, 15.0, 20.0, 25.0, 50.0, 100.0]
POWER_LEVELS = [chirp_common.PowerLevel("LOW1", watts=5),
chirp_common.PowerLevel("LOW2", watts=10),
chirp_common.PowerLevel("LOW3", watts=25),
chirp_common.PowerLevel("HIGH", watts=50),
]
CHARSET = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ +-/?()?_"
@directory.register
class FT1802Radio(yaesu_clone.YaesuCloneModeRadio):
"""Yaesu FT-1802"""
VENDOR = "Yaesu"
MODEL = "FT-1802M"
BAUD_RATE = 19200
_model = b"AH023"
_block_lengths = [10, 8001]
_memsize = 8011
@classmethod
def get_prompts(cls):
rp = chirp_common.RadioPrompts()
rp.pre_download = _(
"1. Turn radio off.\n"
"2. Connect cable to mic jack.\n"
"3. Press and hold in the [LOW(A/N)] key while turning the radio"
" on.\n"
"4. After clicking OK , press the [MHz(SET)] key to send"
" image.\n")
rp.pre_upload = _(
"1. Turn radio off.\n"
"2. Connect cable to mic jack.\n"
"3. Press and hold in the [LOW(A/N)] key while turning the radio"
" on.\n"
"4. Press the [D/MR(MW)] key (\"--WAIT--\" will appear on the"
" LCD).\n")
return rp
def get_features(self):
rf = chirp_common.RadioFeatures()
rf.memory_bounds = (0, 199)
rf.can_odd_split = True
rf.has_ctone = False
rf.has_tuning_step = True
rf.has_dtcs_polarity = False # in radio settings, not per memory
rf.has_bank = False # has banks, but not implemented
rf.valid_tuning_steps = STEPS
rf.valid_modes = MODES
rf.valid_tmodes = TMODES
rf.valid_bands = [(137000000, 174000000)]
rf.valid_power_levels = POWER_LEVELS
rf.valid_duplexes = DUPLEX
rf.valid_skips = ["", "S", "P"]
rf.valid_name_length = 6
rf.valid_characters = CHARSET
rf.has_cross = True
rf.valid_cross_modes = list(CROSS_MODES)
return rf
def _checksums(self):
return [yaesu_clone.YaesuChecksum(0, self._memsize-2)]
def process_mmap(self):
self._memobj = bitwise.parse(MEM_FORMAT, self._mmap)
def get_raw_memory(self, number):
return repr(self._memobj.memory[number]) + \
repr(self._memobj.flags[number/2])
def get_memory(self, number):
_mem = self._memobj.memory[number]
_flag = self._memobj.flags[number/2]
nibble = (number % 2) and "odd" or "even"
visible = _flag["%s_visible" % nibble]
valid = _flag["%s_valid" % nibble]
pskip = _flag["%s_pskip" % nibble]
skip = _flag["%s_skip" % nibble]
mem = chirp_common.Memory()
mem.number = number
if not visible:
mem.empty = True
if not valid:
mem.empty = True
return mem
mem.freq = chirp_common.fix_rounded_step(int(_mem.freq) * 1000)
mem.offset = chirp_common.fix_rounded_step(int(_mem.offset) * 1000)
mem.duplex = DUPLEX[_mem.duplex]
mem.tuning_step = _mem.step_changed and \
STEPS[_mem.tune_step] or STEPS[0]
if _mem.tmode < TMODES.index("Cross"):
mem.tmode = TMODES[_mem.tmode]
mem.cross_mode = CROSS_MODES[0]
else:
mem.tmode = "Cross"
mem.cross_mode = CROSS_MODES[_mem.tmode - TMODES.index("Cross")]
mem.rtone = chirp_common.TONES[_mem.tone]
mem.dtcs = chirp_common.DTCS_CODES[_mem.dtcs]
for i in _mem.name:
if i == 0xFF:
break
if i & 0x80 == 0x80:
# first bit in name is "show name"
mem.name += CHARSET[0x80 ^ int(i)]
else:
mem.name += CHARSET[i]
mem.name = mem.name.rstrip()
mem.mode = _mem.narrow and "NFM" or "FM"
mem.skip = pskip and "P" or skip and "S" or ""
mem.power = POWER_LEVELS[_mem.power]
mem.extra = RadioSettingGroup("extra", "Extra Settings")
rs = RadioSetting("clk_shift", "Clock Shift",
RadioSettingValueBoolean(_mem.clk_shift))
mem.extra.append(rs)
return mem
def set_memory(self, mem):
_mem = self._memobj.memory[mem.number]
_flag = self._memobj.flags[mem.number/2]
nibble = (mem.number % 2) and "odd" or "even"
valid = _flag["%s_valid" % nibble]
visible = _flag["%s_visible" % nibble]
if not mem.empty and not valid:
_flag["%s_valid" % nibble] = True
_mem.unknown1a = 0x00
_mem.clk_shift = 0x00
_mem.unknown1b = 0x00
_mem.unknown2a = 0x00
_mem.unknown2b = 0x00
_mem.unknown3 = 0x00
_mem.unknown4 = 0x00
if mem.empty and valid and not visible:
_flag["%s_valid" % nibble] = False
return
_flag["%s_visible" % nibble] = not mem.empty
if mem.empty:
return
_flag["%s_valid" % nibble] = True
_mem.freq = mem.freq / 1000
_mem.offset = mem.offset / 1000
_mem.duplex = DUPLEX.index(mem.duplex)
_mem.tune_step = STEPS.index(mem.tuning_step)
_mem.step_changed = mem.tuning_step != STEPS[0]
if mem.tmode != "Cross":
_mem.tmode = TMODES.index(mem.tmode)
else:
_mem.tmode = TMODES.index("Cross") + \
CROSS_MODES.index(mem.cross_mode)
_mem.tone = chirp_common.TONES.index(mem.rtone)
_mem.dtcs = chirp_common.DTCS_CODES.index(mem.dtcs)
_mem.name = [0xFF] * 6
for i in range(0, len(mem.name)):
try:
_mem.name[i] = CHARSET.index(mem.name[i])
except IndexError:
raise Exception("Character `%s' not supported")
if _mem.name[0] != 0xFF:
_mem.name[0] += 0x80 # show name instead of frequency
_mem.narrow = MODES.index(mem.mode)
_mem.power = 3 if mem.power is None else POWER_LEVELS.index(mem.power)
_flag["%s_pskip" % nibble] = mem.skip == "P"
_flag["%s_skip" % nibble] = mem.skip == "S"
for element in mem.extra:
setattr(_mem, element.get_name(), element.value)
chirp-20261030/chirp/drivers/ft1d.py 0000644 0001750 0001750 00000264635 15250742640 016052 0 ustar hibby hibby # Copyright 2010 Dan Smith
# Copyright 2014 Angus Ainslie
# Copyright 2023,2025 Declan Rieb
# Sections of digital settings applied from ft70.py, thus
# Copyright 2017 Nicolas Pike
#
# 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
import string
import logging
from chirp.drivers import yaesu_clone
from chirp import chirp_common, directory, bitwise
from chirp import memmap
from chirp.settings import RadioSettingGroup, RadioSetting, RadioSettings, \
RadioSettingValueInteger, RadioSettingValueString, \
RadioSettingValueList, RadioSettingValueBoolean, \
InvalidValueError, RadioSettingSubGroup
from chirp import util
LOG = logging.getLogger(__name__)
SETTINGS_FORMAT = """
// Settings
#seekto 0x047E;
struct {
u8 unknown1;
u8 flag;
u16 unknown2;
struct {
u8 padded_yaesu[16];
} message;
} opening_message;
// Settings
#seekto 0x049A;
struct {
u8 vfo_a;
u8 vfo_b;
} squelch;
// GM / Digital
#seekto 0x04BA;
struct {
u8 unknown:3,
scan_resume:5; // 52 SCN.RSM
u8 unknown1:3,
dw_resume_interval:5; // 22 DW RSM
u8 unknown2;
u8 unknown3:3,
apo:5; // 02 APO
u8 unknown4:6,
gm_ring:2; // 24 GM RNG
u8 temp_cf; // Placeholder as not found
} first_settings;
// GM / Digital
// already at 0x04c0;
// Caution: overloaded at 04c1! only location in FORMATs.
// Old = beep_select.beep, new is beep_settings.beep_select.
struct {
u8 unknown1:5,
beep_level:3; // 05 BEP.LVL
u8 unknown2:6,
beep_select:2; // 04 BEEP
} beep_settings;
// Settings
#seekto 0x04CE;
struct {
u8 lcd_dimmer; // 14 DIMMER
u8 dtmf_delay; // 18 DT DLY
u8 unknown0[3];
u8 unknown1:4,
lcd_contrast:4;
u8 lamp; // 28 LAMP
u8 lock; // 30 LOCK
u8 unknown2;
u8 mic_gain; // 31 MCGAIN
u8 unknown2_3;
u8 dw_interval; // 21 DW INT
u8 ptt_delay; // 42 PTT.DLY
u8 rx_save; // 48 RX.SAVE
u8 scan_restart; // 53 SCN.STR
u8 unknown3;
u8 scan_resume;
u8 unknown4[5];
u8 tot;
u8 unknown5[3];
u8 vfo_mode:1, // 60 VFO.MOD
unknown6:1,
scan_lamp:1, // 51 SCN.LMP
unknown7:1,
ars:1, // 45 RPT.ARS
dtmf_speed:1, // 20 DT SPD
unknown8:1,
dtmf_mode:1;
u8 busy_led:1,
unknown9:2,
bclo:1, // 03 BCLO
beep_edge:1, // 06 BEP.EDG
unknown9_1:3;
u8 unknown10:5,
password:1,
home_rev:1, // 26 HOME/REV
moni:1; // 32 MON/T-Call
u8 gm_interval:4, // 25 GM INT
unknown11:4;
u8 vol_mode:1,
unknown11_1:7;
u8 unknown12:4,
home_vfo:1, // 27 HOME->VFO
unknown12_1:2,
dw_rt:1; // 23 DW RVT;
} scan_settings;
// Settings
#seekto 0x054A;
struct {
u16 in_use;
} bank_used[24];
// Settings
#seekto 0x064A;
struct {
u8 unknown0[4];
u8 frequency_band;
u8 unknown1:6,
manual_or_mr:2;
u8 unknown2:7,
mr_banks:1;
u8 unknown3;
u16 mr_index;
u16 bank_index;
u16 bank_enable;
u8 unknown4[5];
u8 unknown5:6,
power:2;
u8 unknown6:4,
tune_step:4;
u8 unknown7:6,
duplex:2;
u8 unknown8:6,
tone_mode:2;
u8 unknown9:2,
tone:6;
u8 unknown10;
u8 unknown11:6,
mode:2;
bbcd freq0[4];
bbcd offset_freq[4];
u8 unknown12[2];
char label[16];
u8 unknown13[6];
bbcd band_lower[4];
bbcd band_upper[4];
bbcd rx_freq[4];
u8 unknown14[22];
bbcd freq1[4];
u8 unknown15[11];
u8 unknown16:3,
volume:5;
u8 unknown17[18];
u8 active_menu_item;
u8 checksum;
} vfo_info[6];
// Settings
#seekto 0x%(dtmadd)04X; // FT-1D:0e4a, FT2D:094a
struct {
u8 memory[16];
} dtmf[10];
// Settings
#seekto 0x0EFE;
struct {
u8 unknown[2];
u8 name[16];
} bank_info[24];
"""
MEM_FORMAT = """
// This FORMAT is used in FT-xD, FTM-3200D and FTM-7250D
// It relies on defined integer for memnum (number of memories)
// Memory: memory-flags structure used in Yaesu FT-xD, et. al.
struct flagslot {
u8 nosubvfo:1,
unknown:3,
pskip:1,
skip:1,
used:1,
valid:1;
};
// Memory: memory structure used in FT-xD
struct memslot {
u8 unknown0:2,
mode_alt:1, // mode for FTM-3200D
clock_shift:1,
unknown1:4;
u8 mode:2,
duplex:2,
tune_step:4;
bbcd freq[3];
u8 power:2,
digmode:2, // 0=Analog, 1=AMS, 2=DN, 3=VW
tone_mode:4;
u16 charsetbits;
char label[16];
bbcd offset[3];
u8 unknown5:2,
tone:6;
u8 unknown6:1,
dcs:7;
u16 unknown7;
u8 unknown8:2,
att:1,
autostep:1,
automode:1,
unknown9:3;
};
// Memory n.b., Unneeded for FTM-3200 and FTM-7250, but needs memslot
#seekto 0x10CA;
struct memslot Home[11];
// Settings n.b., these are unused in FTM-3200 and FTM-7250,
// But are needed for spacing!
#seekto 0x154A;
// These "channels" seem to actually be a structure:
// first five bits are flags
// 0 Unused (1=entry is unused)
// 1 SW Broadcast
// 2 VHF Marine
// 3 WX (weather)
// 4 ? a mode? ?
// 11 bits of index into frequency tables
//
struct {
u16 channel[100];
} bank_members[24];
// Memory: had unneeded 0x280A
struct flagslot flag[%(memnum)d];
struct flagslot flagskp[99];
struct flagslot flagPMS[100];
// Memory
#seekto 0x2D4A;
struct memslot memory[%(memnum)d];
struct memslot Skip[99];
struct memslot PMS[100];
"""
DIGITAL_FORMAT = """
// APRS
#seekto 0xBECA;
struct {
u8 rx_baud;
u8 custom_symbol;
struct {
char callsign[6];
u8 ssid;
} my_callsign;
u8 unknown3:4,
selected_position_comment:4;
u8 unknown4;
u8 set_time_manually:1,
tx_interval_beacon:1,
ring_beacon:1,
ring_msg:1,
aprs_mute:1,
unknown6:1,
tx_smartbeacon:1,
af_dual:1;
u8 unknown7:1,
aprs_units_wind_mph:1,
aprs_units_rain_inch:1,
aprs_units_temperature_f:1,
aprs_units_altitude_ft:1,
unknown8:1,
aprs_units_distance_m:1,
aprs_units_position_mmss:1;
u8 unknown9:6,
aprs_units_speed:2;
u8 unknown11:1,
filter_other:1,
filter_status:1,
filter_item:1,
filter_object:1,
filter_weather:1,
filter_position:1,
filter_mic_e:1;
u8 unknown12;
u8 unknown13;
u8 unknown14;
u8 unknown15:7,
latitude_sign:1;
u8 latitude_degree;
u8 latitude_minute;
u8 latitude_second;
u8 unknown16:7,
longitude_sign:1;
u8 longitude_degree;
u8 longitude_minute;
u8 longitude_second;
u8 unknown17:4,
selected_position:4;
u8 unknown18:5,
selected_beacon_status_txt:3;
u8 unknown19:4,
beacon_interval:4;
u8 unknowni21:4,
tx_delay:4;
u8 unknown21b:6,
gps_units_altitude_ft:1,
gps_units_position_sss:1;
u8 unknown20:6,
gps_units_speed:2;
u8 unknown21c[4];
struct {
struct {
char callsign[6];
u8 ssid;
} entry[8];
} digi_path_7;
u8 unknown22[18];
struct {
char padded_string[16];
} message_macro[7];
u8 unknown23:5,
selected_msg_group:3;
u8 unknown24;
struct {
char padded_string[9];
} msg_group[8];
u8 unknown25;
u8 unknown25a:2,
timezone:6;
u8 unknown25b[2];
u8 active_smartbeaconing;
struct {
u8 low_speed_mph;
u8 high_speed_mph;
u8 slow_rate_min;
u8 fast_rate_sec;
u8 turn_angle;
u8 turn_slop;
u8 turn_time_sec;
} smartbeaconing_profile[3];
u8 unknown26:2,
flash_msg:6;
u8 unknown27:2,
flash_grp:6;
u8 unknown28:2,
flash_bln:6;
u8 selected_digi_path;
struct {
struct {
char callsign[6];
u8 ssid;
} entry[2];
} digi_path_3_6[4];
u8 unknown30:6,
selected_my_symbol:2;
u8 unknown31[3];
u8 unknown32:2,
vibrate_msg:6;
u8 unknown33:2,
vibrate_grp:6;
u8 unknown34:2,
vibrate_bln:6;
} aprs;
// APRS
#seekto 0xC26A;
struct {
char padded_string[60];
} aprs_beacon_status_txt[5];
// GM / Digital
#seekto 0xCED0;
struct {
char callsign[10]; // 63 MYCALL
u16 charset; // character set ID
} my_call;
// GM / Digital
#seekto 0xCF30;
struct {
u8 unknown0;
u8 unknown1;
u8 unknown2;
u8 unknown3;
u8 unknown4;
u8 unknown5;
u8 unknown6;
u8 digital_popup; // 15 DIG.POP
} digital_settings_more;
// GM / Digital
#seekto 0xCF7C;
struct {
u8 unknown0:6,
ams_tx_mode:2; // AMS TX Mode
u8 unknown1;
u8 unknown2:7,
standby_beep:1; // 07 BEP.STB
u8 unknown3;
u8 unknown4:6,
gm_ring:2; // 24 GM RNG
u8 unknown5;
u8 rx_dg_id; // RX DG-ID
u8 tx_dg_id; // TX DG-ID
u8 unknown6:7,
vw_mode:1; // 16 DIG VW
u8 unknown7;
} digital_settings;
// Backtrack
#seekto 0xDF06;
struct {
u8 status; // 01 full 08 empty
u8 reserved0; // 00
bbcd year; // 17
bbcd mon; // 06
bbcd day; // 01
u8 reserved1; // 06
bbcd hour; // 21
bbcd min; // xx
u8 reserved2; // 00
u8 reserved3; // 00
char NShemi[1];
char lat[3];
char lat_min[2];
char lat_dec_sec[4];
char WEhemi[1];
char lon[3];
char lon_min[2];
char lon_dec_sec[4];
} backtrack[3];
// APRS
#seekto 0xFECA;
struct {
bbcd date[3];
bbcd time[2];
u8 sequence;
u8 unknown1;
u8 unknown2;
char sender_callsign[9];
u8 data_type;
u8 yeasu_data_type;
u8 unknown4:1,
callsign_is_ascii:1,
unknown5:6;
u8 unknown6;
u16 pkt_len;
u8 unknown7;
u16 in_use;
u16 unknown8;
u16 unknown9;
u16 unknown10;
} aprs_beacon_meta[60];
// APRS Had unneeded 0x10641
struct {
char dst_callsign[9];
char path[30];
u16 flags;
u8 separator;
char body[134];
} aprs_beacon_pkt[60];
// APRS
#seekto 0x137C4;
struct {
u8 flag;
char dst_callsign[6];
u8 dst_callsign_ssid;
char path_and_body[66];
u8 unknown[70];
} aprs_message_pkt[60];
// GM / Digital
#seekto 0x1D6D3;
struct {
char message[32];
} GM[10];
// GM / Digital
#seekto 0x1DDCA;
struct {
struct {
char name[16];
} Category[5];
struct {
struct {
char ID[5]; // ASCII numerals
char name[16]; // blank-fill
u8 unknown[3];
} Rooms[20];
} RoomsPerCategory[5];
} WiresX_settings;
"""
CHECKSUM_FORMAT = """
// Checksum
#seekto 0x1FDC9;
u8 checksum;
"""
TMODES = ["", "Tone", "TSQL", "DTCS"]
DUPLEX = ["", "-", "+", "split"]
MODES = ["FM", "AM", "WFM"]
STEPS = [5.0, 6.25, 8.33, 10.0, 12.5, 15.0, 20.0, 25.0, 50.0, 100.0, 9.0]
SKIPS = ["", "S", "P"]
FT1_DTMF_CHARS = list("0123456789ABCD*#-")
CHARSET = ["%i" % int(x) for x in range(0, 10)] + \
[chr(x) for x in range(ord("A"), ord("Z") + 1)] + \
[" ", ] + \
[chr(x) for x in range(ord("a"), ord("z") + 1)] + \
list(".,:;*#_-/&()@!?^ ") + list("\x00" * 100)
POWER_LEVELS = [chirp_common.PowerLevel("Hi", watts=5.00),
chirp_common.PowerLevel("L3", watts=2.50),
chirp_common.PowerLevel("L2", watts=1.00),
chirp_common.PowerLevel("L1", watts=0.05)]
SKIPNAMES = ["Skip%i" % i for i in range(901, 1000)]
PMSNAMES = ["%s%i" % (c, i) for i in range(1, 51) for c in ['L', 'U']]
HOMENAMES = ["Home%i" % i for i in range(1, 12)]
ALLNAMES = SKIPNAMES + PMSNAMES + HOMENAMES
# list of (array name, (list of memories in that array))
# array names must match names of memories defined for radio
SPECIALS = [
("Skip", SKIPNAMES),
("PMS", PMSNAMES),
("Home", HOMENAMES)
]
# Band edges are integer Hz.
VALID_BANDS = [
(510000, 1790000),
(1800000, 50490000),
(50500000, 75990000),
(76000000, 107990000),
(108000000, 136990000),
(145000000, 169920000),
(174000000, 221950000),
(222000000, 419990000),
(420000000, 469990000),
(470000000, 773990000),
(810010000, 999000000)
]
class FT1Bank(chirp_common.NamedBank):
"""A FT1D bank"""
def get_name(self):
_bank = self._model._radio._memobj.bank_info[self.index]
name = ""
for i in _bank.name:
if i == 0xFF:
break
name += CHARSET[i & 0x7F]
return name.rstrip()
def set_name(self, name):
_bank = self._model._radio._memobj.bank_info[self.index]
_bank.name = [CHARSET.index(x) for x in name.ljust(16)[:16]]
class FT1BankModel(chirp_common.BankModel):
"""A FT1D bank model"""
def __init__(self, radio, name='Banks'):
super(FT1BankModel, self).__init__(radio, name)
_banks = self._radio._memobj.bank_info
self._bank_mappings = []
for index, _bank in enumerate(_banks):
bank = FT1Bank(self, "%i" % index, "BANK-%i" % index)
bank.index = index
self._bank_mappings.append(bank)
def get_num_mappings(self):
return len(self._bank_mappings)
def get_mappings(self):
return self._bank_mappings
def _channel_numbers_in_bank(self, bank):
_bank_used = self._radio._memobj.bank_used[bank.index]
if _bank_used.in_use == 0xFFFF:
return set()
_members = self._radio._memobj.bank_members[bank.index]
return set([int(ch) + 1 for ch in _members.channel if ch != 0xFFFF])
def update_vfo(self):
chosen_bank = [None, None]
chosen_mr = [None, None]
flags = self._radio._memobj.flag
# Find a suitable bank and MR for VFO A and B.
for bank in self.get_mappings():
for channel in self._channel_numbers_in_bank(bank):
chosen_bank[0] = bank.index
chosen_mr[0] = channel
if channel & 0x7000 != 0:
# Ignore preset channels without comment DAR
break
if not flags[channel].nosubvfo:
chosen_bank[1] = bank.index
chosen_mr[1] = channel
break
if chosen_bank[1]:
break
for vfo_index in (0, 1):
# 3 VFO info structs are stored as 3 pairs of (master, backup)
vfo = self._radio._memobj.vfo_info[vfo_index * 2]
vfo_bak = self._radio._memobj.vfo_info[(vfo_index * 2) + 1]
if vfo.checksum != vfo_bak.checksum:
LOG.warn("VFO settings are inconsistent with backup")
else:
if ((chosen_bank[vfo_index] is None) and (vfo.bank_index !=
0xFFFF)):
LOG.info("Disabling banks for VFO %d" % vfo_index)
vfo.bank_index = 0xFFFF
vfo.mr_index = 0xFFFF
vfo.bank_enable = 0xFFFF
elif ((chosen_bank[vfo_index] is not None) and
(vfo.bank_index == 0xFFFF)):
LOG.info("Enabling banks for VFO %d" % vfo_index)
vfo.bank_index = chosen_bank[vfo_index]
vfo.mr_index = chosen_mr[vfo_index]
vfo.bank_enable = 0x0000
vfo_bak.bank_index = vfo.bank_index
vfo_bak.mr_index = vfo.mr_index
vfo_bak.bank_enable = vfo.bank_enable
def _update_bank_with_channel_numbers(self, bank, channels_in_bank):
_members = self._radio._memobj.bank_members[bank.index]
if len(channels_in_bank) > len(_members.channel):
raise Exception("Too many entries in bank %d" % bank.index)
empty = 0
for index, channel_number in enumerate(sorted(channels_in_bank)):
_members.channel[index] = channel_number - 1
if channel_number & 0x7000 != 0:
LOG.warn("Bank %d uses Yaesu preset frequency id=%04X. "
"Chirp cannot see or change that entry." % (
bank.index, channel_number))
empty = index + 1
for index in range(empty, len(_members.channel)):
_members.channel[index] = 0xFFFF
def add_memory_to_mapping(self, memory, bank):
channels_in_bank = self._channel_numbers_in_bank(bank)
channels_in_bank.add(memory.number)
self._update_bank_with_channel_numbers(bank, channels_in_bank)
_bank_used = self._radio._memobj.bank_used[bank.index]
_bank_used.in_use = 0x06
self.update_vfo()
def remove_memory_from_mapping(self, memory, bank):
channels_in_bank = self._channel_numbers_in_bank(bank)
try:
channels_in_bank.remove(memory.number)
except KeyError:
raise Exception("Memory %i is not in bank %s. Cannot remove" %
(memory.number, bank))
self._update_bank_with_channel_numbers(bank, channels_in_bank)
if not channels_in_bank:
_bank_used = self._radio._memobj.bank_used[bank.index]
_bank_used.in_use = 0xFFFF
self.update_vfo()
def get_mapping_memories(self, bank):
memories = []
for channel in self._channel_numbers_in_bank(bank):
memories.append(self._radio.get_memory(channel))
return memories
def get_memory_mappings(self, memory):
banks = []
for bank in self.get_mappings():
if memory.number in self._channel_numbers_in_bank(bank):
banks.append(bank)
return banks
# Note: other radios like FTM3200Radio subclass this radio
@directory.register
class FT1Radio(yaesu_clone.YaesuCloneModeRadio):
"""Yaesu FT1DR"""
BAUD_RATE = 38400
VENDOR = "Yaesu"
MODEL = "FT-1D"
VARIANT = "R"
FORMATS = [directory.register_format('FT1D ADMS-6', '*.ft1d')]
class_specials = SPECIALS
_model = b"AH44M"
_memsize = 130507
_block_lengths = [10, 130497]
_block_size = 32
MAX_MEM_SLOT = 900
_mem_params = {
"memnum": 900, # size of memories array
"flgnum": 900, # size of flags array
"dtmadd": 0xe4a, # location of DTMF storage
}
_has_vibrate = False
_has_af_dual = True
_adms_ext = '.ft1d'
_SG_RE = re.compile(r"(?P[-+NESW]?)(?P[\d]+)[\s\.,]*"
r"(?P[\d]*)[\s\']*(?P[\d]*)")
_RX_BAUD = ("off", "1200 baud", "9600 baud")
_TX_DELAY = ("100ms", "150ms", "200ms", "250ms", "300ms",
"400ms", "500ms", "750ms", "1000ms")
_WIND_UNITS = ("m/s", "mph")
_RAIN_UNITS = ("mm", "inch")
_TEMP_UNITS = ("C", "F")
_ALT_UNITS = ("m", "ft")
_DIST_UNITS = ("km", "mile")
_POS_UNITS = ("dd.mmmm'", "dd mm'ss\"")
_SPEED_UNITS = ("km/h", "knot", "mph")
_TIME_SOURCE = ("manual", "GPS")
_TZ = ("-13:00", "-13:30", "-12:00", "-12:30", "-11:00", "-11:30",
"-10:00", "-10:30", "-09:00", "-09:30", "-08:00", "-08:30",
"-07:00", "-07:30", "-06:00", "-06:30", "-05:00", "-05:30",
"-04:00", "-04:30", "-03:00", "-03:30", "-02:00", "-02:30",
"-01:00", "-01:30", "-00:00", "-00:30", "+01:00", "+01:30",
"+02:00", "+02:30", "+03:00", "+03:30", "+04:00", "+04:30",
"+05:00", "+05:30", "+06:00", "+06:30", "+07:00", "+07:30",
"+08:00", "+08:30", "+09:00", "+09:30", "+10:00", "+10:30",
"+11:00", "+11:30")
_BEACON_TYPE = ("Off", "Interval", "SmartBeaconing")
_SMARTBEACON_PROFILE = ("Off", "Type 1", "Type 2", "Type 3")
_BEACON_INT = ("30s", "1m", "2m", "3m", "5m", "10m", "15m",
"20m", "30m", "60m")
_DIGI_PATHS = ("OFF", "WIDE1-1", "WIDE1-1, WIDE2-1", "Digi Path 4",
"Digi Path 5", "Digi Path 6", "Digi Path 7", "Digi Path 8")
_MSG_GROUP_NAMES = ("Message Group 1", "Message Group 2",
"Message Group 3", "Message Group 4",
"Message Group 5", "Message Group 6",
"Message Group 7", "Message Group 8")
_POSITIONS = ("GPS", "Manual Latitude/Longitude",
"Manual Latitude/Longitude", "P1", "P2", "P3", "P4",
"P5", "P6", "P7", "P8", "P9")
_FLASH = ("OFF", "2 seconds", "4 seconds", "6 seconds", "8 seconds",
"10 seconds", "20 seconds", "30 seconds", "60 seconds",
"CONTINUOUS", "every 2 seconds", "every 3 seconds",
"every 4 seconds", "every 5 seconds", "every 6 seconds",
"every 7 seconds", "every 8 seconds", "every 9 seconds",
"every 10 seconds", "every 20 seconds", "every 30 seconds",
"every 40 seconds", "every 50 seconds", "every minute",
"every 2 minutes", "every 3 minutes", "every 4 minutes",
"every 5 minutes", "every 6 minutes", "every 7 minutes",
"every 8 minutes", "every 9 minutes", "every 10 minutes")
_BEEP_SELECT = ("Off", "Key+Scan", "Key")
_SQUELCH = ["%d" % x for x in range(0, 16)]
_VOLUME = ["%d" % x for x in range(0, 33)]
_OPENING_MESSAGE = ("Off", "DC", "Message", "Normal")
_SCAN_RESUME = ["%.1fs" % (0.5 * x) for x in range(4, 21)] + \
["Busy", "Hold"]
_SCAN_RESTART = ["%.1fs" % (0.1 * x) for x in range(1, 10)] + \
["%.1fs" % (0.5 * x) for x in range(2, 21)]
_LAMP_KEY = ["Key %d sec" % x
for x in range(2, 11)] + ["Continuous", "OFF"]
_LCD_CONTRAST = ["Level %d" % x for x in range(1, 16)]
_LCD_DIMMER = ["Level %d" % x for x in range(1, 7)]
_TOT_TIME = ["Off"] + ["%.1f min" % (0.5 * x) for x in range(1, 21)]
_OFF_ON = ("Off", "On")
_VOL_MODE = ("Normal", "Auto Back")
_DTMF_MODE = ("Manual", "Auto")
_DTMF_SPEED = ("50ms", "100ms")
_DTMF_DELAY = ("50ms", "250ms", "450ms", "750ms", "1000ms")
_MY_SYMBOL = ("/[ Person", "/b Bike", "/> Car", "User selected")
_BACKTRACK_STATUS = ("Valid", "Invalid")
_NS_HEMI = ("N", "S")
_WE_HEMI = ("W", "E")
_APRS_HIGH_SPEED_MAX = 70
_MIC_GAIN = ("Level %d" % i for i in range(1, 10))
_AMS_TX_MODE = ("TX Auto", "TX DIGITAL", "TX FM")
_VW_MODE = ("On", "Off")
_DIG_POP_UP = ("Off", "2sec", "4sec", "6sec", "8sec", "10sec",
"20sec", "30sec", "60sec", "Continuous")
_STANDBY_BEEP = ("On", "Off")
_ON_OFF = ("On", "Off")
_TEMP_CF = ("Centigrade", "Fahrenheit")
_APO_SELECT = ("Off", "0.5H", "1.0H", "1.5H", "2.0H", "2.5H",
"3.0H", "3.5H", "4.0H", "4.5H", "5.0H",
"5.5H", "6.0H", "6.5H", "7.0H", "7.5H", "8.0H",
"8.5H", "9.0H", "9.5H", "10.0H", "10.5H",
"11.0H", "11.5H", "12.0H")
_MONI_TCALL = ("Monitor", "Tone-CALL")
_HOME_REV = ("Home", "Reverse")
_LOCK = ("KEY", "DIAL", "Key+Dial", "PTT", "Key+PTT", "Dial+PTT", "ALL")
_PTT_DELAY = ("Off", "20 ms", "50 ms", "100 ms", "200 ms")
_BEEP_LEVEL = ("Level %i" % i for i in range(1, 7))
_SET_MODE = ("Level %i" % i for i in range(1, 8))
_RX_SAVE = ("OFF", "0.2s", ".3s", ".4s", ".5s", ".6s",
".7s", ".8s", ".9s", "1.0s", "1.5s",
"2.0s", "2.5s", "3.0s", "3.5s", "4.0s", "4.5s",
"5.0s", "5.5s", "6.0s", "6.5s", "7.0s",
"7.5s", "8.0s", "8.5s", "9.0s", "10.0s", "15s",
"20s", "25s", "30s", "35s", "40s", "45s", "50s", "55s",
"60s")
_VFO_MODE = ("ALL", "BAND")
_VFO_SCAN_MODE = ("BAND", "ALL")
_MEMORY_SCAN_MODE = ("BAND", "ALL")
_RX_BAUD = ("off", "1200 baud", "9600 baud")
_TX_DELAY = ("100ms", "150ms", "200ms", "250ms", "300ms",
"400ms", "500ms", "750ms", "1000ms")
_WIND_UNITS = ("m/s", "mph")
_RAIN_UNITS = ("mm", "inch")
_TEMP_UNITS = ("C", "F")
_ALT_UNITS = ("m", "ft")
_DIST_UNITS = ("km", "mile")
_POS_UNITS = ("dd.mmmm'", "dd mm'ss\"")
_SPEED_UNITS = ("km/h", "knot", "mph")
_TIME_SOURCE = ("manual", "GPS")
_TZ = ("-13:00", "-13:30", "-12:00", "-12:30", "-11:00", "-11:30",
"-10:00", "-10:30", "-09:00", "-09:30", "-08:00", "-08:30",
"-07:00", "-07:30", "-06:00", "-06:30", "-05:00", "-05:30",
"-04:00", "-04:30", "-03:00", "-03:30", "-02:00", "-02:30",
"-01:00", "-01:30", "-00:00", "-00:30", "+01:00", "+01:30",
"+02:00", "+02:30", "+03:00", "+03:30", "+04:00", "+04:30",
"+05:00", "+05:30", "+06:00", "+06:30", "+07:00", "+07:30",
"+08:00", "+08:30", "+09:00", "+09:30", "+10:00", "+10:30",
"+11:00", "+11:30")
_BEACON_TYPE = ("Off", "Interval", "SmartBeaconing")
_SMARTBEACON_PROFILE = ("Off", "Type 1", "Type 2", "Type 3")
_BEACON_INT = ("30s", "1m", "2m", "3m", "5m", "10m", "15m",
"20m", "30m", "60m")
_DIGI_PATHS = ("OFF", "WIDE1-1", "WIDE1-1, WIDE2-1", "Digi Path 4",
"Digi Path 5", "Digi Path 6", "Digi Path 7", "Digi Path 8")
_MSG_GROUP_NAMES = ("Message Group 1", "Message Group 2",
"Message Group 3", "Message Group 4",
"Message Group 5", "Message Group 6",
"Message Group 7", "Message Group 8")
_POSITIONS = ("GPS", "Manual Latitude/Longitude",
"Manual Latitude/Longitude", "P1", "P2", "P3", "P4",
"P5", "P6", "P7", "P8", "P9")
_FLASH = ("OFF", "2 seconds", "4 seconds", "6 seconds", "8 seconds",
"10 seconds", "20 seconds", "30 seconds", "60 seconds",
"CONTINUOUS", "every 2 seconds", "every 3 seconds",
"every 4 seconds", "every 5 seconds", "every 6 seconds",
"every 7 seconds", "every 8 seconds", "every 9 seconds",
"every 10 seconds", "every 20 seconds", "every 30 seconds",
"every 40 seconds", "every 50 seconds", "every minute",
"every 2 minutes", "every 3 minutes", "every 4 minutes",
"every 5 minutes", "every 6 minutes", "every 7 minutes",
"every 8 minutes", "every 9 minutes", "every 10 minutes")
_BEEP_SELECT = ("Off", "Key+Scan", "Key")
_SQUELCH = ["%d" % x for x in range(0, 16)]
_VOLUME = ["%d" % x for x in range(0, 33)]
_DG_ID = ["%d" % x for x in range(0, 100)]
_GM_RING = ("OFF", "IN RING", "ALWAYS")
_GM_INTERVAL = ("LONG", "NORMAL", "OFF")
_MYCALL_CHR_SET = list(string.ascii_uppercase) + \
list(string.digits) + ['-', '/']
@classmethod
def match_model(cls, filedata, filename):
if filename.endswith(cls._adms_ext):
return True
else:
return super().match_model(filedata, filename)
@classmethod
def get_prompts(cls):
rp = chirp_common.RadioPrompts()
rp.pre_download = _(
"1. Turn radio off.\n"
"2. Connect cable to DATA terminal.\n"
"3. Press and hold in the [F] key while turning the radio on\n"
" (\"CLONE\" will appear on the display).\n"
"4. After clicking OK , press the [BAND] key to send"
" image.\n")
rp.pre_upload = _(
"1. Turn radio off.\n"
"2. Connect cable to DATA terminal.\n"
"3. Press and hold in the [F] key while turning the radio on\n"
" (\"CLONE\" will appear on the display).\n"
"4. Press the [Dx] key (\"-WAIT-\" will appear on the LCD).\n")
return rp
def process_mmap(self):
_mf = SETTINGS_FORMAT + MEM_FORMAT + DIGITAL_FORMAT + CHECKSUM_FORMAT
self._memobj = bitwise.parse(_mf % self._mem_params, self._mmap)
def get_features(self):
rf = chirp_common.RadioFeatures()
rf.has_dtcs_polarity = False
rf.valid_modes = list(MODES) + ['NFM', 'DN']
rf.valid_tmodes = list(TMODES)
rf.valid_duplexes = list(DUPLEX)
rf.valid_tuning_steps = [x for x in STEPS if x]
rf.valid_bands = [(500000, 999900000)]
rf.valid_skips = SKIPS
rf.valid_power_levels = POWER_LEVELS
rf.valid_characters = "".join(CHARSET)
rf.valid_name_length = 16
rf.memory_bounds = (1, 900)
rf.can_odd_split = True
rf.has_ctone = False
rf.has_bank_names = True
rf.has_settings = True
rf.valid_special_chans = [name for s in SPECIALS for name in s[1]]
return rf
def get_raw_memory(self, number):
return "\n".join([repr(self._memobj.memory[number - 1]),
repr(self._memobj.flag[number - 1])])
def _checksums(self):
return [yaesu_clone.YaesuChecksum(0x064A, 0x06C8),
yaesu_clone.YaesuChecksum(0x06CA, 0x0748),
yaesu_clone.YaesuChecksum(0x074A, 0x07C8),
yaesu_clone.YaesuChecksum(0x07CA, 0x0848),
yaesu_clone.YaesuChecksum(0x0000, 0x1FDC9)]
@staticmethod
def _add_ff_pad(val: str, length: int) -> str:
_fill = b'\xff' if isinstance(val, bytes | bytearray) else '\xff'
return val.ljust(length, _fill)[:length]
@classmethod
def _strip_ff_pads(cls, messages):
result = []
for msg_text in messages:
result.append(str(msg_text).rstrip("\xFF"))
return result
# Called with a "memref" index to CHIRP memory (int or str)
# and optionally with a "extref" extended name.
# Find and return the corresponding memobj
# Returns:
# Corresponding radio memory object
# Corresponding radio alag structure (if any)
# index into the specific memory object structure (int) ndx
# overall index into memory & specials (int) num
# an indicator of the specific radio object structure (str)
def slotloc(self, memref, extref=None):
array = None
num = memref
ename = ""
mstr = isinstance(memref, str)
specials = ALLNAMES
extr = False
if extref is not None:
extr = extref in specials
if mstr or extr: # named special?
ename = memref
num = self.MAX_MEM_SLOT + 1
# num = -1
sname = memref if mstr else extref
# Find name of appropriate memory and index into that memory
for x in self.class_specials:
try:
ndx = x[1].index(sname)
array = x[0]
break
except Exception:
num += len(x[1])
# num -= len(x[1])
if array is None:
raise IndexError("Unknown special %s" % memref)
num += ndx
# num -= ndx
elif memref > self.MAX_MEM_SLOT: # numbered special
ename = extref
ndx = memref - (self.MAX_MEM_SLOT + 1)
# Find name of appropriate memory and index into that memory
# But assumes specials are in reverse-quantity order
for x in self.class_specials:
if ndx < len(x[1]):
array = x[0]
break
ndx -= len(x[1])
if array is None:
raise IndexError("Unknown memref number %s" % memref)
else:
array = "memory"
ndx = memref - 1
_flag = self._memobj.flag[ndx]
if array == "Skip":
_flag = self._memobj.flagskp[ndx]
elif array == "PMS":
_flag = self._memobj.flagPMS[ndx]
elif array == "Home":
_flag = None
_mem = getattr(self._memobj, array)[ndx]
return (_mem, _flag, ndx, num, array, ename)
# Build CHIRP version (mem) of radio's memory (_mem)
def get_memory(self, number):
_mem, _flag, ndx, num, array, ename = self.slotloc(number)
mem = chirp_common.Memory()
mem.number = num
if array == "Home":
mem.empty = False
mem.extd_number = ename
mem.name = self._decode_label(_mem)
mem.immutable += ["empty", "number", "extd_number", "skip"]
elif array != "memory":
mem.extd_number = ename
mem.immutable += ["name", "extd_number"]
else:
mem.name = self._decode_label(_mem)
if _flag is not None:
mem.skip = _flag.pskip and "P" or _flag.skip and "S" or ""
mem.empty = False
if not _flag.used:
mem.empty = True
if not _flag.valid:
mem.empty = True
if mem.empty:
mem.freq = 0
mem.offset = 0
mem.duplex = ""
mem.power = POWER_LEVELS[0]
mem.mode = "FM"
mem.tuning_step = STEPS[0]
else:
mem.freq = chirp_common.fix_rounded_step(int(_mem.freq) * 1000)
mem.offset = int(_mem.offset) * 1000
mem.rtone = mem.ctone = chirp_common.TONES[_mem.tone]
self._get_tmode(mem, _mem)
mem.duplex = DUPLEX[_mem.duplex] if _mem.duplex else ""
if mem.duplex == "split":
mem.offset = chirp_common.fix_rounded_step(mem.offset)
mem.mode = self._decode_mode(_mem)
mem.dtcs = chirp_common.DTCS_CODES[_mem.dcs]
mem.tuning_step = STEPS[_mem.tune_step]
mem.power = self._decode_power_level(_mem)
self._get_mem_extra(mem, _mem)
return mem
def _get_mem_extra(self, mem, _mem):
mem.extra = RadioSettingGroup('Extra', 'extra')
ams = _mem.digmode == 1
rs = RadioSetting('ysf_ams', 'AMS mode',
RadioSettingValueBoolean(ams))
mem.extra.append(rs)
def _set_mem_extra(self, mem, _mem):
if 'ysf_ams' in mem.extra:
# We only set AMS if the memory mode is DN. If it is FM,
# then that takes precedence as "analog-only".
if mem.mode == 'DN':
orig = int(_mem.digmode)
_mem.digmode = int(mem.extra['ysf_ams'].value) and 1 or 2
LOG.debug('Changed digmode from %i to %i' % (
orig, _mem.digmode))
def _decode_label(self, mem):
charset = ''.join(CHARSET).ljust(256, '.')
return str(mem.label).rstrip("\xFF").translate(charset)
def _encode_label(self, mem):
label = "".join([chr(CHARSET.index(x))
for x in mem.name.rstrip()]).encode()
return self._add_ff_pad(label, 16)
def _encode_charsetbits(self, mem):
# We only speak English here in chirpville
return 0x0000
def _decode_power_level(self, mem):
return POWER_LEVELS[3 - mem.power]
def _encode_power_level(self, mem):
if mem.power is None:
return 3 # Choose lowest power
else:
return 3 - POWER_LEVELS.index(mem.power)
def _decode_mode(self, mem):
mode = MODES[mem.mode]
if mode == 'FM' and int(mem.digmode):
# DN mode is FM with a digital flag. Since digmode can be AMS or
# DN, either will be called 'DN' in chirp
return 'DN'
if mode == 'FM' and int(mem.mode_alt):
return 'NFM'
else:
return mode
def _encode_mode(self, mem):
mode = mem.mode
if mode == 'NFM':
# Narrow is handled by a separate flag
mode = 'FM'
elif mode == 'DN':
# DN mode is FM with a digital flag
mode = 'FM'
return MODES.index(mode)
def _get_tmode(self, mem, _mem):
mem.tmode = TMODES[_mem.tone_mode]
def _set_tmode(self, _mem, mem):
_mem.tone_mode = TMODES.index(mem.tmode)
def _set_mode(self, _mem, mem):
_mem.mode_alt = mem.mode == 'NFM'
if mem.mode == 'DN' and int(_mem.digmode) == 0:
# If we are going to DN mode, default to AMS
LOG.debug('New mode DN, setting AMS')
if 'ysf_ams' not in mem.extra:
_mem.digmode = 1
else:
# If we have the extra setting, use that, since it will be
# applied at the end of set_memory() and we want it to apply
# the right value.
mem.extra['ysf_ams'].value = True
elif mem.mode == 'FM' and int(_mem.digmode):
# If we are going back to FM, that means analog-only, so AMS
# is disabled
LOG.debug('New mode FM, disabling AMS')
_mem.digmode = 0
_mem.mode = self._encode_mode(mem)
bm = self.get_bank_model()
for bank in bm.get_memory_mappings(mem):
bm.remove_memory_from_mapping(mem, bank)
def _debank(self, mem):
bm = self.get_bank_model()
for bank in bm.get_memory_mappings(mem):
bm.remove_memory_from_mapping(mem, bank)
def validate_memory(self, mem):
# Only check the home registers for appropriate bands
msgs = super().validate_memory(mem)
ndx = mem.number - ALLNAMES.index("Home1") - self.MAX_MEM_SLOT - 1
if 10 >= ndx >= 0:
f = VALID_BANDS[ndx]
if not(f[0] < mem.freq < f[1]):
msgs.append(chirp_common.ValidationError(
"Frequency outside of band for Home%2d" %
(ndx + 1)))
return msgs
# Modify radio's memory (_mem) corresponding to CHIRP version at 'mem'
def set_memory(self, mem):
_mem, flag, ndx, num, regtype, ename = self.slotloc(mem.number,
mem.extd_number)
if mem.empty:
self._wipe_memory(_mem)
if flag is not None:
flag.used = False
return
_mem.power = self._encode_power_level(mem)
_mem.tone = chirp_common.TONES.index(mem.rtone)
self._set_tmode(_mem, mem)
_mem.dcs = chirp_common.DTCS_CODES.index(mem.dtcs)
_mem.tune_step = STEPS.index(mem.tuning_step)
# duplex "off" is equivalent to "" and may show up in tox test.
if mem.duplex is None:
_mem.duplex = DUPLEX.index("")
else:
_mem.duplex = DUPLEX.index(mem.duplex)
self._set_mode(_mem, mem)
if flag is not None:
if mem.freq < 30000000 or \
(mem.freq > 88000000 and mem.freq < 108000000) or \
mem.freq > 580000000:
flag.nosubvfo = True # Masked from VFO B
else:
flag.nosubvfo = False # Available in both VFOs
if regtype != "Home":
self._debank(mem)
ndx = num - 1
flag.used = not mem.empty
flag.valid = True
flag.skip = mem.skip == "S"
flag.pskip = mem.skip == "P"
freq = mem.freq
_mem.freq = int(freq / 1000)
_mem.offset = int(mem.offset / 1000)
_mem.label = self._encode_label(mem)
_mem.charsetbits = self._encode_charsetbits(mem)
self._set_mem_extra(mem, _mem)
return
@classmethod
def _wipe_memory(cls, mem):
mem.set_raw("\x00" * (mem.size() // 8))
mem.unknown1 = 0x05
def get_bank_model(self):
return FT1BankModel(self)
@classmethod
def _digi_path_to_str(cls, path):
path_cmp = []
for entry in path.entry:
callsign = str(entry.callsign).rstrip("\xFF")
if not callsign:
break
path_cmp.append("%s-%d" % (callsign, entry.ssid))
return ",".join(path_cmp)
@staticmethod
def _latlong_sanity(sign, l_d, l_m, l_s, is_lat):
if sign not in (0, 1):
sign = 0
if is_lat:
d_max = 90
else:
d_max = 180
if l_d < 0 or l_d > d_max:
l_d = 0
l_m = 0
l_s = 0
if l_m < 0 or l_m > 60:
l_m = 0
l_s = 0
if l_s < 0 or l_s > 60:
l_s = 0
return sign, l_d, l_m, l_s
@classmethod
def _latlong_to_str(cls, sign, l_d, l_m, l_s, is_lat, to_sexigesimal=True):
sign, l_d, l_m, l_s = cls._latlong_sanity(sign, l_d, l_m, l_s, is_lat)
mult = sign and -1 or 1
if to_sexigesimal:
return "%d,%d'%d\"" % (mult * l_d, l_m, l_s)
return "%0.5f" % (mult * l_d + (l_m / 60.0) + (l_s / (60.0 * 60.0)))
@classmethod
def _str_to_latlong(cls, lat_long, is_lat):
sign = 0
result = [0, 0, 0]
lat_long = lat_long.strip()
if not lat_long:
return 1, 0, 0, 0
try:
# DD.MMMMM is the simple case, try that first.
val = float(lat_long)
if val < 0:
sign = 1
val = abs(val)
result[0] = int(val)
result[1] = int(val * 60) % 60
result[2] = int(val * 3600) % 60
except ValueError:
# Try DD MM'SS" if DD.MMMMM failed.
match = cls._SG_RE.match(lat_long.strip())
if match:
if match.group("sign") and (match.group("sign") in "SE-"):
sign = 1
else:
sign = 0
if match.group("d"):
result[0] = int(match.group("d"))
if match.group("m"):
result[1] = int(match.group("m"))
if match.group("s"):
result[2] = int(match.group("s"))
elif len(lat_long) > 4:
raise Exception("Lat/Long should be DD MM'SS\" or DD.MMMMM")
return cls._latlong_sanity(sign, result[0], result[1], result[2],
is_lat)
def _get_aprs_settings(self):
menu = RadioSettingGroup("aprs_top", "APRS")
menu.append(self._get_aprs_general_settings())
menu.append(self._get_aprs_rx_settings())
menu.append(self._get_aprs_tx_settings())
menu.append(self._get_aprs_smartbeacon())
menu.append(self._get_aprs_msgs())
menu.append(self._get_aprs_beacons())
return menu
def _get_aprs_general_settings(self):
menu = RadioSettingGroup("aprs_general", "APRS General")
aprs = self._memobj.aprs
val = RadioSettingValueString(
0, 6, str(aprs.my_callsign.callsign).rstrip("\xFF"))
rs = RadioSetting("aprs.my_callsign.callsign", "My Callsign", val)
rs.set_apply_callback(self.apply_callsign, aprs.my_callsign)
menu.append(rs)
val = RadioSettingValueList(
chirp_common.APRS_SSID,
current_index=aprs.my_callsign.ssid)
rs = RadioSetting("aprs.my_callsign.ssid", "My SSID", val)
menu.append(rs)
val = RadioSettingValueList(self._MY_SYMBOL,
current_index=aprs.selected_my_symbol)
rs = RadioSetting("aprs.selected_my_symbol", "My Symbol", val)
menu.append(rs)
symbols = list(chirp_common.APRS_SYMBOLS)
selected = aprs.custom_symbol
if aprs.custom_symbol >= len(chirp_common.APRS_SYMBOLS):
symbols.append("%d" % aprs.custom_symbol)
selected = len(symbols) - 1
val = RadioSettingValueList(symbols, current_index=selected)
rs = RadioSetting("aprs.custom_symbol_text", "User Selected Symbol",
val)
rs.set_apply_callback(self.apply_custom_symbol, aprs)
menu.append(rs)
val = RadioSettingValueList(
chirp_common.APRS_POSITION_COMMENT,
current_index=aprs.selected_position_comment)
rs = RadioSetting("aprs.selected_position_comment", "Position Comment",
val)
menu.append(rs)
latitude = self._latlong_to_str(aprs.latitude_sign,
aprs.latitude_degree,
aprs.latitude_minute,
aprs.latitude_second,
True, aprs.aprs_units_position_mmss)
longitude = self._latlong_to_str(aprs.longitude_sign,
aprs.longitude_degree,
aprs.longitude_minute,
aprs.longitude_second,
False, aprs.aprs_units_position_mmss)
# TODO: Rebuild this when aprs_units_position_mmss changes.
# TODO: Rebuild this when latitude/longitude change.
# TODO: Add saved positions p1 - p10 to memory map.
position_str = list(self._POSITIONS)
# position_str[1] = "%s %s" % (latitude, longitude)
# position_str[2] = "%s %s" % (latitude, longitude)
val = RadioSettingValueList(position_str,
current_index=aprs.selected_position)
rs = RadioSetting("aprs.selected_position", "My Position", val)
menu.append(rs)
val = RadioSettingValueString(0, 10, latitude)
rs = RadioSetting("latitude", "Manual Latitude", val)
rs.set_apply_callback(self.apply_lat_long, aprs)
menu.append(rs)
val = RadioSettingValueString(0, 11, longitude)
rs = RadioSetting("longitude", "Manual Longitude", val)
rs.set_apply_callback(self.apply_lat_long, aprs)
menu.append(rs)
val = RadioSettingValueList(
self._TIME_SOURCE, current_index=aprs.set_time_manually)
rs = RadioSetting("aprs.set_time_manually", "Time Source", val)
menu.append(rs)
val = RadioSettingValueList(self._TZ, current_index=aprs.timezone)
rs = RadioSetting("aprs.timezone", "Timezone", val)
menu.append(rs)
val = RadioSettingValueList(self._SPEED_UNITS,
current_index=aprs.aprs_units_speed)
rs = RadioSetting("aprs.aprs_units_speed", "APRS Speed Units", val)
menu.append(rs)
val = RadioSettingValueList(self._SPEED_UNITS,
current_index=aprs.gps_units_speed)
rs = RadioSetting("aprs.gps_units_speed", "GPS Speed Units", val)
menu.append(rs)
val = RadioSettingValueList(
self._ALT_UNITS, current_index=aprs.aprs_units_altitude_ft)
rs = RadioSetting("aprs.aprs_units_altitude_ft", "APRS Altitude Units",
val)
menu.append(rs)
val = RadioSettingValueList(
self._ALT_UNITS, current_index=aprs.gps_units_altitude_ft)
rs = RadioSetting("aprs.gps_units_altitude_ft", "GPS Altitude Units",
val)
menu.append(rs)
val = RadioSettingValueList(
self._POS_UNITS, current_index=aprs.aprs_units_position_mmss)
rs = RadioSetting("aprs.aprs_units_position_mmss",
"APRS Position Format", val)
menu.append(rs)
val = RadioSettingValueList(
self._POS_UNITS, current_index=aprs.gps_units_position_sss)
rs = RadioSetting("aprs.gps_units_position_sss",
"GPS Position Format", val)
menu.append(rs)
val = RadioSettingValueList(
self._DIST_UNITS, current_index=aprs.aprs_units_distance_m)
rs = RadioSetting("aprs.aprs_units_distance_m", "APRS Distance Units",
val)
menu.append(rs)
val = RadioSettingValueList(self._WIND_UNITS,
current_index=aprs.aprs_units_wind_mph)
rs = RadioSetting("aprs.aprs_units_wind_mph", "APRS Wind Speed Units",
val)
menu.append(rs)
val = RadioSettingValueList(
self._RAIN_UNITS, current_index=aprs.aprs_units_rain_inch)
rs = RadioSetting("aprs.aprs_units_rain_inch", "APRS Rain Units", val)
menu.append(rs)
val = RadioSettingValueList(
self._TEMP_UNITS, current_index=aprs.aprs_units_temperature_f)
rs = RadioSetting("aprs.aprs_units_temperature_f",
"APRS Temperature Units", val)
menu.append(rs)
return menu
def _get_aprs_msgs(self):
menu = RadioSettingGroup("aprs_msg", "APRS Messages")
aprs_msg = self._memobj.aprs_message_pkt
for index in range(0, 60):
if aprs_msg[index].flag != 255:
astring = bitwise.get_string(aprs_msg[index].dst_callsign)\
.partition("\xFF")[0]
val = RadioSettingValueString(
0, 9, chirp_common.sanitize_string(astring) +
"-%d" % aprs_msg[index].dst_callsign_ssid)
val.set_mutable(False)
rs = RadioSetting(
"aprs_msg.dst_callsign%d" % index,
"Dst Callsign %d" % index, val)
menu.append(rs)
astring = bitwise.get_string(aprs_msg[index].path_and_body)\
.partition("\xFF")[0]
val = RadioSettingValueString(
0, 66, chirp_common.sanitize_string(astring))
val.set_mutable(False)
rs = RadioSetting(
"aprs_msg.path_and_body%d" % index, "Body", val)
menu.append(rs)
return menu
def _get_aprs_beacons(self):
menu = RadioSettingGroup("aprs_beacons", "APRS Beacons")
aprs_beacon = self._memobj.aprs_beacon_pkt
aprs_meta = self._memobj.aprs_beacon_meta
for index in range(0, 60):
# There is probably a more pythonesque way to do this
scl = int(aprs_meta[index].sender_callsign[0])
dcl = int(aprs_beacon[index].dst_callsign[0])
if scl not in (255, 0): # ignore if empty send call
_c = chirp_common.sanitize_string(
bitwise.get_string(aprs_meta[index].sender_callsign))
val = RadioSettingValueString(0, 9, _c)
val.set_mutable(False)
rs = RadioSetting(
"aprs_beacon.src_callsign%d" % index,
"SRC Callsign %d" % index, val)
menu.append(rs)
if dcl not in (255, 0): # ignore if empty dest call
val = chirp_common.sanitize_string(
bitwise.get_string(aprs_beacon[index].dst_callsign))
val = RadioSettingValueString(0, 9, val)
val.set_mutable(False)
rs = RadioSetting(
"aprs_beacon.dst_callsign%d" % index,
"DST Callsign %d" % index, val)
menu.append(rs)
date = "%02d/%02d/%02d" % (
aprs_meta[index].date[0],
aprs_meta[index].date[1],
aprs_meta[index].date[2])
val = RadioSettingValueString(0, 8, date)
val.set_mutable(False)
rs = RadioSetting("aprs_beacon.date%d" % index, "Date", val)
menu.append(rs)
time = "%02d:%02d" % (
aprs_meta[index].time[0],
aprs_meta[index].time[1])
val = RadioSettingValueString(0, 5, time)
val.set_mutable(False)
rs = RadioSetting("aprs_beacon.time%d" % index, "Time", val)
menu.append(rs)
if dcl not in (255, 0): # ignore if empty dest call
path = bitwise.get_string(aprs_beacon[index].path)\
.replace("\x00", " ")
path = chirp_common.sanitize_string(str(path).strip()
.replace("\xE0", "*"))
val = RadioSettingValueString(0, 32, path)
val.set_mutable(False)
rs = RadioSetting(
"aprs_beacon.path%d" % index, "Digipath", val)
menu.append(rs)
body = bitwise.get_string(aprs_beacon[index].body)\
.rstrip("\xFF")
# checksum = body[-2:]
body = chirp_common.sanitize_string(body)
try:
val = RadioSettingValueString(0, 134, body.strip())
except Exception as e:
LOG.error("Error in APRS beacon at index %s", index)
raise e
val.set_mutable(False)
rs = RadioSetting("aprs_beacon.body%d" % index, "Body", val)
menu.append(rs)
return menu
def _get_aprs_rx_settings(self):
menu = RadioSettingGroup("aprs_rx", "APRS Receive")
aprs = self._memobj.aprs
val = RadioSettingValueList(self._RX_BAUD, current_index=aprs.rx_baud)
rs = RadioSetting("aprs.rx_baud", "Modem RX", val)
menu.append(rs)
val = RadioSettingValueBoolean(aprs.aprs_mute)
rs = RadioSetting("aprs.aprs_mute", "APRS Mute", val)
menu.append(rs)
if self._has_af_dual:
val = RadioSettingValueBoolean(aprs.af_dual)
rs = RadioSetting("aprs.af_dual", "AF Dual", val)
menu.append(rs)
val = RadioSettingValueBoolean(aprs.ring_msg)
rs = RadioSetting("aprs.ring_msg", "Ring on Message RX", val)
menu.append(rs)
val = RadioSettingValueBoolean(aprs.ring_beacon)
rs = RadioSetting("aprs.ring_beacon", "Ring on Beacon RX", val)
menu.append(rs)
val = RadioSettingValueList(self._FLASH,
current_index=aprs.flash_msg)
rs = RadioSetting("aprs.flash_msg", "Flash on personal message", val)
menu.append(rs)
if self._has_vibrate:
val = RadioSettingValueList(self._FLASH,
current_index=aprs.vibrate_msg)
rs = RadioSetting("aprs.vibrate_msg",
"Vibrate on personal message", val)
menu.append(rs)
val = RadioSettingValueList(self._FLASH[:10],
current_index=aprs.flash_bln)
rs = RadioSetting("aprs.flash_bln", "Flash on bulletin message", val)
menu.append(rs)
if self._has_vibrate:
val = RadioSettingValueList(self._FLASH[:10],
current_index=aprs.vibrate_bln)
rs = RadioSetting("aprs.vibrate_bln",
"Vibrate on bulletin message", val)
menu.append(rs)
val = RadioSettingValueList(self._FLASH[:10],
current_index=aprs.flash_grp)
rs = RadioSetting("aprs.flash_grp", "Flash on group message", val)
menu.append(rs)
if self._has_vibrate:
val = RadioSettingValueList(self._FLASH[:10],
current_index=aprs.vibrate_grp)
rs = RadioSetting("aprs.vibrate_grp",
"Vibrate on group message", val)
menu.append(rs)
filter_val = [m.padded_string for m in aprs.msg_group]
filter_val = self._strip_ff_pads(filter_val)
for index, filter_text in enumerate(filter_val):
val = RadioSettingValueString(0, 9, filter_text)
rs = RadioSetting("aprs.msg_group_%d" % index,
"Message Group %d" % (index + 1), val)
menu.append(rs)
rs.set_apply_callback(self.apply_ff_padded_string,
aprs.msg_group[index])
# TODO: Use filter_val as the list entries and update it on edit.
val = RadioSettingValueList(
self._MSG_GROUP_NAMES,
current_index=aprs.selected_msg_group)
rs = RadioSetting("aprs.selected_msg_group", "Selected Message Group",
val)
menu.append(rs)
val = RadioSettingValueBoolean(aprs.filter_mic_e)
rs = RadioSetting("aprs.filter_mic_e", "Receive Mic-E Beacons", val)
menu.append(rs)
val = RadioSettingValueBoolean(aprs.filter_position)
rs = RadioSetting("aprs.filter_position", "Receive Position Beacons",
val)
menu.append(rs)
val = RadioSettingValueBoolean(aprs.filter_weather)
rs = RadioSetting("aprs.filter_weather", "Receive Weather Beacons",
val)
menu.append(rs)
val = RadioSettingValueBoolean(aprs.filter_object)
rs = RadioSetting("aprs.filter_object", "Receive Object Beacons", val)
menu.append(rs)
val = RadioSettingValueBoolean(aprs.filter_item)
rs = RadioSetting("aprs.filter_item", "Receive Item Beacons", val)
menu.append(rs)
val = RadioSettingValueBoolean(aprs.filter_status)
rs = RadioSetting("aprs.filter_status", "Receive Status Beacons", val)
menu.append(rs)
val = RadioSettingValueBoolean(aprs.filter_other)
rs = RadioSetting("aprs.filter_other", "Receive Other Beacons", val)
menu.append(rs)
return menu
def _get_aprs_tx_settings(self):
menu = RadioSettingGroup("aprs_tx", "APRS Transmit")
aprs = self._memobj.aprs
beacon_type = (aprs.tx_smartbeacon << 1) | aprs.tx_interval_beacon
val = RadioSettingValueList(self._BEACON_TYPE,
current_index=beacon_type)
rs = RadioSetting("aprs.transmit", "TX Beacons", val)
rs.set_apply_callback(self.apply_beacon_type, aprs)
menu.append(rs)
val = RadioSettingValueList(self._TX_DELAY,
current_index=aprs.tx_delay)
rs = RadioSetting("aprs.tx_delay", "TX Delay", val)
menu.append(rs)
val = RadioSettingValueList(self._BEACON_INT,
current_index=aprs.beacon_interval)
rs = RadioSetting("aprs.beacon_interval", "Beacon Interval", val)
menu.append(rs)
desc = []
status = [m.padded_string for m in self._memobj.aprs_beacon_status_txt]
status = self._strip_ff_pads(status)
for index, msg_text in enumerate(status):
val = RadioSettingValueString(0, 60, msg_text)
desc.append("Beacon Status Text %d" % (index + 1))
rs = RadioSetting("aprs_beacon_status_txt_%d" % index, desc[-1],
val)
rs.set_apply_callback(self.apply_ff_padded_string,
self._memobj.aprs_beacon_status_txt[index])
menu.append(rs)
val = RadioSettingValueList(
desc, current_index=aprs.selected_beacon_status_txt)
rs = RadioSetting("aprs.selected_beacon_status_txt",
"Beacon Status Text", val)
menu.append(rs)
message_macro = [m.padded_string for m in aprs.message_macro]
message_macro = self._strip_ff_pads(message_macro)
for index, msg_text in enumerate(message_macro):
val = RadioSettingValueString(0, 16, msg_text)
rs = RadioSetting("aprs.message_macro_%d" % index,
"Message Macro %d" % (index + 1), val)
rs.set_apply_callback(self.apply_ff_padded_string,
aprs.message_macro[index])
menu.append(rs)
path_str = list(self._DIGI_PATHS)
path_str[3] = self._digi_path_to_str(aprs.digi_path_3_6[0])
val = RadioSettingValueString(0, 22, path_str[3])
rs = RadioSetting("aprs.digi_path_3", "Digi Path 4 (2 entries)", val)
rs.set_apply_callback(self.apply_digi_path, aprs.digi_path_3_6[0])
menu.append(rs)
path_str[4] = self._digi_path_to_str(aprs.digi_path_3_6[1])
val = RadioSettingValueString(0, 22, path_str[4])
rs = RadioSetting("aprs.digi_path_4", "Digi Path 5 (2 entries)", val)
rs.set_apply_callback(self.apply_digi_path, aprs.digi_path_3_6[1])
menu.append(rs)
path_str[5] = self._digi_path_to_str(aprs.digi_path_3_6[2])
val = RadioSettingValueString(0, 22, path_str[5])
rs = RadioSetting("aprs.digi_path_5", "Digi Path 6 (2 entries)", val)
rs.set_apply_callback(self.apply_digi_path, aprs.digi_path_3_6[2])
menu.append(rs)
path_str[6] = self._digi_path_to_str(aprs.digi_path_3_6[3])
val = RadioSettingValueString(0, 22, path_str[6])
rs = RadioSetting("aprs.digi_path_6", "Digi Path 7 (2 entries)", val)
rs.set_apply_callback(self.apply_digi_path, aprs.digi_path_3_6[3])
menu.append(rs)
path_str[7] = self._digi_path_to_str(aprs.digi_path_7)
val = RadioSettingValueString(0, 88, path_str[7])
rs = RadioSetting("aprs.digi_path_7", "Digi Path 8 (8 entries)", val)
rs.set_apply_callback(self.apply_digi_path, aprs.digi_path_7)
menu.append(rs)
# Show friendly messages for empty slots rather than blanks.
# TODO: Rebuild this when digi_path_[34567] change.
# path_str[3] = path_str[3] or self._DIGI_PATHS[3]
# path_str[4] = path_str[4] or self._DIGI_PATHS[4]
# path_str[5] = path_str[5] or self._DIGI_PATHS[5]
# path_str[6] = path_str[6] or self._DIGI_PATHS[6]
# path_str[7] = path_str[7] or self._DIGI_PATHS[7]
path_str[3] = self._DIGI_PATHS[3]
path_str[4] = self._DIGI_PATHS[4]
path_str[5] = self._DIGI_PATHS[5]
path_str[6] = self._DIGI_PATHS[6]
path_str[7] = self._DIGI_PATHS[7]
val = RadioSettingValueList(path_str,
current_index=aprs.selected_digi_path)
rs = RadioSetting("aprs.selected_digi_path", "Selected Digi Path", val)
menu.append(rs)
return menu
def _get_aprs_smartbeacon(self):
menu = RadioSettingGroup("aprs_smartbeacon", "APRS SmartBeacon")
aprs = self._memobj.aprs
val = RadioSettingValueList(
self._SMARTBEACON_PROFILE,
current_index=aprs.active_smartbeaconing)
rs = RadioSetting("aprs.active_smartbeaconing", "SmartBeacon profile",
val)
menu.append(rs)
for profile in range(3):
pfx = "type%d" % (profile + 1)
path = "aprs.smartbeaconing_profile[%d]" % profile
prof = aprs.smartbeaconing_profile[profile]
low_val = RadioSettingValueInteger(2, 30, prof.low_speed_mph)
high_val = RadioSettingValueInteger(3, self._APRS_HIGH_SPEED_MAX,
prof.high_speed_mph)
low_val.get_max = lambda: min(30, int(high_val.get_value()) - 1)
rs = RadioSetting("%s.low_speed_mph" % path,
"%s Low Speed (mph)" % pfx, low_val)
menu.append(rs)
rs = RadioSetting("%s.high_speed_mph" % path,
"%s High Speed (mph)" % pfx, high_val)
menu.append(rs)
val = RadioSettingValueInteger(1, 100, prof.slow_rate_min)
rs = RadioSetting("%s.slow_rate_min" % path,
"%s Slow rate (minutes)" % pfx, val)
menu.append(rs)
val = RadioSettingValueInteger(10, 180, prof.fast_rate_sec)
rs = RadioSetting("%s.fast_rate_sec" % path,
"%s Fast rate (seconds)" % pfx, val)
menu.append(rs)
val = RadioSettingValueInteger(5, 90, prof.turn_angle)
rs = RadioSetting("%s.turn_angle" % path,
"%s Turn angle (degrees)" % pfx, val)
menu.append(rs)
val = RadioSettingValueInteger(1, 255, prof.turn_slop)
rs = RadioSetting("%s.turn_slop" % path,
"%s Turn slop" % pfx, val)
menu.append(rs)
val = RadioSettingValueInteger(5, 180, prof.turn_time_sec)
rs = RadioSetting("%s.turn_time_sec" % path,
"%s Turn time (seconds)" % pfx, val)
menu.append(rs)
return menu
def _get_digital_settings(self):
topmenu = RadioSettingGroup("digital_settings", "Digital")
menu = RadioSettingGroup("settings", "Digital Modes")
topmenu.append(menu)
GMmenu = RadioSettingGroup("first_settings", "Group Monitor(GM)")
topmenu.append(GMmenu)
WXmenu = RadioSettingGroup("WiresX_settings", "Wires-X")
topmenu.append(WXmenu)
# MYCALL
mycall = self._memobj.my_call
mycallstr = bitwise.get_string(mycall.callsign).rstrip("\xff\00 ")
mycalle = RadioSettingValueString(0, 10, mycallstr, False,
charset=self._MYCALL_CHR_SET)
rs = RadioSetting('mycall.callsign',
'MYCALL', mycalle)
rs.set_apply_callback(self.apply_mycall, mycall)
menu.append(rs)
# Short Press AMS button AMS TX Mode
digital_settings = self._memobj.digital_settings
val = RadioSettingValueList(
self._AMS_TX_MODE,
current_index=digital_settings.ams_tx_mode)
rs = RadioSetting("digital_settings.ams_tx_mode",
"AMS TX Mode", val)
menu.append(rs)
# 16 DIG VW Turn the VW mode selection ON or OFF.
val = RadioSettingValueList(
self._VW_MODE,
current_index=digital_settings.vw_mode)
rs = RadioSetting("digital_settings.vw_mode", "VW Mode", val)
menu.append(rs)
# TX DG-ID Long Press Mode Key, Dial
val = RadioSettingValueList(
self._DG_ID,
current_index=digital_settings.tx_dg_id)
rs = RadioSetting("digital_settings.tx_dg_id",
"TX DG-ID", val)
menu.append(rs)
# RX DG-ID Long Press Mode Key, Mode Key to select, Dial
val = RadioSettingValueList(
self._DG_ID,
current_index=digital_settings.rx_dg_id)
rs = RadioSetting("digital_settings.rx_dg_id",
"RX DG-ID", val)
menu.append(rs)
# 15 DIG.POP Call sign display pop up time
digital_settings_more = self._memobj.digital_settings_more
val = RadioSettingValueList(
self._DIG_POP_UP,
current_index=(
0 if digital_settings_more.digital_popup == 0
else digital_settings_more.digital_popup - 9))
rs = RadioSetting("digital_settings_more.digital_popup",
"Digital Popup", val)
rs.set_apply_callback(self.apply_digital_popup,
digital_settings_more)
menu.append(rs)
# 07 BEP.STB Standby Beep in the digital C4FM mode. On/Off
val = RadioSettingValueList(
self._STANDBY_BEEP,
current_index=digital_settings.standby_beep)
rs = RadioSetting("digital_settings.standby_beep",
"Standby Beep", val)
menu.append(rs)
# GM settings
# 24 GM RNG Select the beep option
first_settings = self._memobj.first_settings
val = RadioSettingValueList(
self._GM_RING,
current_index=first_settings.gm_ring)
rs = RadioSetting("first_settings.gm_ring", "GM Ring", val)
GMmenu.append(rs)
# 25 GM INT transmission interval of digital GM info
scan_settings = self._memobj.scan_settings
val = RadioSettingValueList(
self._GM_INTERVAL,
current_index=scan_settings.gm_interval)
rs = RadioSetting("scan_settings.gm_interval",
"GM Interval", val)
GMmenu.append(rs)
m = self._memobj.GM
for i in range(0, 10):
cname = "GM[%d].message" % i
msg = str(m[i].message).rstrip("\xff)")
val = RadioSettingValueString(0, 32, msg)
rs = RadioSetting(cname, "GM Message%2d" % (i + 1),
val)
GMmenu.append(rs)
# WiresX settings
wxc = self._memobj.WiresX_settings
for i in range(5):
WXCmenu = RadioSettingSubGroup(
f"WiresX_settings.Category[{i}].RoomsPerCategory",
f"Category{i + 1} Rooms")
WXmenu.append(WXCmenu)
cname = f"WiresX_settings.Category[{i}].name"
c = str(wxc.Category[i].name).rstrip('\xff').ljust(16)
val = RadioSettingValueString(0, 16, c)
rs = RadioSetting(cname, f"Category {i+1: 2d} Name", val)
WXCmenu.append(rs)
r = wxc.RoomsPerCategory[i]
for j in range(20):
cn = str(r.Rooms[j].name).strip('\xff').ljust(16)
val = RadioSettingValueString(0, 16, cn)
cname = f"WiresX_settings.RoomsPerCategory[{i}]."\
f"Rooms[{j}].name"
dname = f"Category {i + 1} Room{j + 1: 02d}"
rs = RadioSetting(cname, dname + ' Designation', val)
WXCmenu.append(rs)
idn = str(r.Rooms[j].ID).strip('\xff').ljust(5)
val = RadioSettingValueString(0, 5, idn)
vname = f"WiresX_settings.RoomsPerCategory[{i}].Rooms[{j}].ID"
rs = RadioSetting(vname, dname + " YSF Number",
val)
WXCmenu.append(rs)
pass
return topmenu
def _get_dtmf_settings(self):
menu = RadioSettingGroup("dtmf_settings", "DTMF")
dtmf = self._memobj.scan_settings
val = RadioSettingValueList(
self._DTMF_MODE,
current_index=dtmf.dtmf_mode)
rs = RadioSetting("scan_settings.dtmf_mode", "DTMF Mode", val)
menu.append(rs)
val = RadioSettingValueList(
self._DTMF_SPEED,
current_index=dtmf.dtmf_speed)
rs = RadioSetting(
"scan_settings.dtmf_speed", "DTMF AutoDial Speed", val)
menu.append(rs)
val = RadioSettingValueList(
self._DTMF_DELAY,
current_index=dtmf.dtmf_delay)
rs = RadioSetting(
"scan_settings.dtmf_delay", "DTMF AutoDial Delay", val)
menu.append(rs)
for i in range(10):
name = "dtmf_%02d" % i
dtmfsetting = self._memobj.dtmf[i]
dtmfstr = ""
for c in dtmfsetting.memory:
if c == 0xFF:
break
if c < len(FT1_DTMF_CHARS):
dtmfstr += FT1_DTMF_CHARS[c]
dtmfentry = RadioSettingValueString(0, 16, dtmfstr)
dtmfentry.set_charset(FT1_DTMF_CHARS + list("abcd "))
rs = RadioSetting(name, name.upper(), dtmfentry)
rs.set_apply_callback(self.apply_dtmf, i)
menu.append(rs)
return menu
def _get_misc_settings(self):
menu = RadioSettingGroup("misc_settings", "Misc")
scan_settings = self._memobj.scan_settings
val = RadioSettingValueList(
self._LCD_DIMMER,
current_index=scan_settings.lcd_dimmer)
rs = RadioSetting("scan_settings.lcd_dimmer", "LCD Dimmer", val)
menu.append(rs)
val = RadioSettingValueList(
self._LCD_CONTRAST,
current_index=scan_settings.lcd_contrast - 1)
rs = RadioSetting("scan_settings.lcd_contrast", "LCD Contrast",
val)
rs.set_apply_callback(self.apply_lcd_contrast, scan_settings)
menu.append(rs)
val = RadioSettingValueList(
self._LAMP_KEY,
current_index=scan_settings.lamp)
rs = RadioSetting("scan_settings.lamp", "Lamp", val)
menu.append(rs)
beep_select = self._memobj.beep_settings.beep_select
val = RadioSettingValueList(
self._BEEP_SELECT,
current_index=beep_select)
rs = RadioSetting("beep_settings.beep_select", "Beep Select", val)
menu.append(rs)
opening_message = self._memobj.opening_message
val = RadioSettingValueList(
self._OPENING_MESSAGE,
current_index=opening_message.flag)
rs = RadioSetting("opening_message.flag", "Opening Msg Mode",
val)
menu.append(rs)
rs = self._decode_opening_message(opening_message)
menu.append(rs)
return menu
def _decode_opening_message(self, opening_message) -> RadioSetting:
msg = ""
for i in opening_message.message.padded_yaesu:
if i == 0xFF:
break
msg += CHARSET[i & 0x7F]
val = RadioSettingValueString(0, 16, msg)
rs = RadioSetting("opening_message.message.padded_yaesu",
"Opening Message", val)
rs.set_apply_callback(self.apply_ff_padded_yaesu,
opening_message.message)
return rs
def backtrack_ll_validate(self, number, min, max):
if str(number).lstrip('0').strip().isdigit() and \
int(str(number).lstrip('0')) <= max and \
int(str(number).lstrip('0')) >= min:
return True
return False
@staticmethod
def zero_pad(number, length):
"""
Applies a leading zero pad of length `length` to `number`
"""
return str(number).rjust(length, "0")
def _get_backtrack_settings(self):
menu = RadioSettingGroup("backtrack", "Backtrack")
for i in range(3):
prefix = ''
if i == 0:
prefix = "Star "
if i == 1:
prefix = "L1 "
if i == 2:
prefix = "L2 "
bt_idx = "backtrack[%d]" % i
bt = self._memobj.backtrack[i]
val = RadioSettingValueList(
self._BACKTRACK_STATUS,
current_index=0 if bt.status == 1 else 1)
rs = RadioSetting(
"%s.status" % bt_idx,
prefix + "status", val)
rs.set_apply_callback(self.apply_backtrack_status, bt)
menu.append(rs)
if bt.status == 1 and int(bt.year) < 100:
val = RadioSettingValueInteger(0, 99, bt.year)
else:
val = RadioSettingValueInteger(0, 99, 0)
rs = RadioSetting(
"%s.year" % bt_idx,
prefix + "year", val)
menu.append(rs)
if bt.status == 1 and int(bt.mon) <= 12:
val = RadioSettingValueInteger(0, 12, bt.mon)
else:
val = RadioSettingValueInteger(0, 12, 0)
rs = RadioSetting(
"%s.mon" % bt_idx,
prefix + "month", val)
menu.append(rs)
if bt.status == 1:
val = RadioSettingValueInteger(0, 31, bt.day)
else:
val = RadioSettingValueInteger(0, 31, 0)
rs = RadioSetting(
"%s.day" % bt_idx,
prefix + "day", val)
menu.append(rs)
if bt.status == 1:
val = RadioSettingValueInteger(0, 23, bt.hour)
else:
val = RadioSettingValueInteger(0, 23, 0)
rs = RadioSetting(
"%s.hour" % bt_idx,
prefix + "hour", val)
menu.append(rs)
if bt.status == 1:
val = RadioSettingValueInteger(0, 59, bt.min)
else:
val = RadioSettingValueInteger(0, 59, 0)
rs = RadioSetting(
"%s.min" % bt_idx,
prefix + "min", val)
menu.append(rs)
if bt.status == 1 and \
(str(bt.NShemi) == 'N' or str(bt.NShemi) == 'S'):
val = RadioSettingValueString(0, 1, str(bt.NShemi))
else:
val = RadioSettingValueString(0, 1, ' ')
rs = RadioSetting(
"%s.NShemi" % bt_idx,
prefix + "NS hemisphere", val)
rs.set_apply_callback(self.apply_NShemi, bt)
menu.append(rs)
if bt.status == 1 and self.backtrack_ll_validate(bt.lat, 0, 90):
val = RadioSettingValueString(
0, 3, self.zero_pad(bt.lat, 3))
else:
val = RadioSettingValueString(0, 3, ' ')
rs = RadioSetting("%s.lat" % bt_idx, prefix + "Latitude", val)
rs.set_apply_callback(self.apply_bt_lat, bt)
menu.append(rs)
if bt.status == 1 and \
self.backtrack_ll_validate(bt.lat_min, 0, 59):
val = RadioSettingValueString(
0, 2, self.zero_pad(bt.lat_min, 2))
else:
val = RadioSettingValueString(0, 2, ' ')
rs = RadioSetting(
"%s.lat_min" % bt_idx,
prefix + "Latitude Minutes", val)
rs.set_apply_callback(self.apply_bt_lat_min, bt)
menu.append(rs)
if bt.status == 1 and \
self.backtrack_ll_validate(bt.lat_dec_sec, 0, 9999):
val = RadioSettingValueString(
0, 4, self.zero_pad(bt.lat_dec_sec, 4))
else:
val = RadioSettingValueString(0, 4, ' ')
rs = RadioSetting(
"%s.lat_dec_sec" % bt_idx,
prefix + "Latitude Decimal Seconds", val)
rs.set_apply_callback(self.apply_bt_lat_dec_sec, bt)
menu.append(rs)
if bt.status == 1 and \
(str(bt.WEhemi) == 'W' or str(bt.WEhemi) == 'E'):
val = RadioSettingValueString(
0, 1, str(bt.WEhemi))
else:
val = RadioSettingValueString(0, 1, ' ')
rs = RadioSetting(
"%s.WEhemi" % bt_idx,
prefix + "WE hemisphere", val)
rs.set_apply_callback(self.apply_WEhemi, bt)
menu.append(rs)
if bt.status == 1 and self.backtrack_ll_validate(bt.lon, 0, 180):
val = RadioSettingValueString(
0, 3, self.zero_pad(bt.lon, 3))
else:
val = RadioSettingValueString(0, 3, ' ')
rs = RadioSetting("%s.lon" % bt_idx, prefix + "Longitude", val)
rs.set_apply_callback(self.apply_bt_lon, bt)
menu.append(rs)
if bt.status == 1 and \
self.backtrack_ll_validate(bt.lon_min, 0, 59):
val = RadioSettingValueString(
0, 2, self.zero_pad(bt.lon_min, 2))
else:
val = RadioSettingValueString(0, 2, ' ')
rs = RadioSetting(
"%s.lon_min" % bt_idx,
prefix + "Longitude Minutes", val)
rs.set_apply_callback(self.apply_bt_lon_min, bt)
menu.append(rs)
if bt.status == 1 and \
self.backtrack_ll_validate(bt.lon_dec_sec, 0, 9999):
val = RadioSettingValueString(
0, 4, self.zero_pad(bt.lon_dec_sec, 4))
else:
val = RadioSettingValueString(0, 4, ' ')
rs = RadioSetting(
"%s.lon_dec_sec" % bt_idx,
prefix + "Longitude Decimal Seconds", val)
rs.set_apply_callback(self.apply_bt_lon_dec_sec, bt)
menu.append(rs)
return menu
def _get_scan_settings(self):
menu = RadioSettingGroup("scan_settings", "Scan")
scan_settings = self._memobj.scan_settings
val = RadioSettingValueList(
self._VOL_MODE,
current_index=scan_settings.vol_mode)
rs = RadioSetting("scan_settings.vol_mode", "Volume Mode", val)
menu.append(rs)
vfoa = self._memobj.vfo_info[0]
val = RadioSettingValueList(
self._VOLUME,
current_index=vfoa.volume)
rs = RadioSetting("vfo_info[0].volume", "VFO A Volume", val)
rs.set_apply_callback(self.apply_volume, 0)
menu.append(rs)
vfob = self._memobj.vfo_info[1]
val = RadioSettingValueList(
self._VOLUME,
current_index=vfob.volume)
rs = RadioSetting("vfo_info[1].volume", "VFO B Volume", val)
rs.set_apply_callback(self.apply_volume, 1)
menu.append(rs)
squelch = self._memobj.squelch
val = RadioSettingValueList(
self._SQUELCH,
current_index=squelch.vfo_a)
rs = RadioSetting("squelch.vfo_a", "VFO A Squelch", val)
menu.append(rs)
val = RadioSettingValueList(
self._SQUELCH,
current_index=squelch.vfo_b)
rs = RadioSetting("squelch.vfo_b", "VFO B Squelch", val)
menu.append(rs)
val = RadioSettingValueList(
self._SCAN_RESTART,
current_index=scan_settings.scan_restart)
rs = RadioSetting("scan_settings.scan_restart", "Scan Restart", val)
menu.append(rs)
val = RadioSettingValueList(
self._SCAN_RESUME,
current_index=scan_settings.scan_resume)
rs = RadioSetting("scan_settings.scan_resume", "Scan Resume", val)
menu.append(rs)
val = RadioSettingValueList(
self._OFF_ON,
current_index=scan_settings.busy_led)
rs = RadioSetting("scan_settings.busy_led", "Busy LED", val)
menu.append(rs)
val = RadioSettingValueList(
self._OFF_ON,
current_index=scan_settings.scan_lamp)
rs = RadioSetting("scan_settings.scan_lamp", "Scan Lamp", val)
menu.append(rs)
val = RadioSettingValueList(
self._TOT_TIME,
current_index=scan_settings.tot)
rs = RadioSetting("scan_settings.tot", "Transmit Timeout (TOT)", val)
menu.append(rs)
return menu
def _get_settings(self):
top = RadioSettings(self._get_aprs_settings(),
self._get_digital_settings(),
self._get_dtmf_settings(),
self._get_misc_settings(),
self._get_scan_settings(),
self._get_backtrack_settings())
return top
def get_settings(self):
try:
return self._get_settings()
except:
import traceback
LOG.error("Failed to parse settings: %s", traceback.format_exc())
return None
@staticmethod
def apply_custom_symbol(setting, obj):
# Ensure new value falls within known bounds, otherwise leave it as
# it's a custom value from the radio that's outside our list.
if setting.value.get_value() in chirp_common.APRS_SYMBOLS:
setattr(obj, "custom_symbol",
chirp_common.APRS_SYMBOLS.index(setting.value.get_value()))
@classmethod
def _apply_callsign(cls, callsign, obj, default_ssid=None):
ssid = default_ssid
dash_index = callsign.find("-")
if dash_index >= 0:
ssid = callsign[dash_index + 1:]
callsign = callsign[:dash_index]
try:
ssid = int(ssid) % 16
except ValueError:
ssid = default_ssid
setattr(obj, "callsign", cls._add_ff_pad(callsign.encode(), 6))
if ssid is not None:
setattr(obj, "ssid", ssid)
def apply_beacon_type(cls, setting, obj):
beacon_type = str(setting.value.get_value())
beacon_index = cls._BEACON_TYPE.index(beacon_type)
tx_smartbeacon = beacon_index >> 1
tx_interval_beacon = beacon_index & 1
if tx_interval_beacon:
setattr(obj, "tx_interval_beacon", 1)
setattr(obj, "tx_smartbeacon", 0)
elif tx_smartbeacon:
setattr(obj, "tx_interval_beacon", 0)
setattr(obj, "tx_smartbeacon", 1)
else:
setattr(obj, "tx_interval_beacon", 0)
setattr(obj, "tx_smartbeacon", 0)
# digital settings callback routines
def apply_digital_popup(cls, setting, obj):
rawval = setting.value.get_value()
val = 0 if cls._DIG_POP_UP.index(rawval) == 0 \
else cls._DIG_POP_UP.index(rawval) + 9
obj.digital_popup = val
def apply_mycall(cls, setting, obj):
cs = setting.value.get_value()
if cs[0] in ('-', '/'):
raise InvalidValueError("First character of"
" call sign can't be - or /: {0:s}"
.format(cs))
else:
obj.callsign = cls._add_ff_pad(cs.rstrip(), 10)
@classmethod
def apply_callsign(cls, setting, obj, default_ssid=None):
# Uppercase, strip SSID then FF pad to max string length.
callsign = setting.value.get_value().upper()
cls._apply_callsign(callsign, obj, default_ssid)
def apply_digi_path(self, setting, obj):
# Parse and map to aprs.digi_path_4_7[0-3] or aprs.digi_path_8
# and FF terminate.
path = str(setting.value.get_value())
callsigns = [c.strip() for c in path.split(",")]
for index in range(len(obj.entry)):
try:
self._apply_callsign(callsigns[index], obj.entry[index], 0)
except IndexError:
self._apply_callsign("", obj.entry[index], 0)
if len(callsigns) > len(obj.entry):
raise Exception("This path only supports %d entries" % (index + 1))
@classmethod
def apply_ff_padded_string(cls, setting, obj):
# FF pad.
val = setting.value.get_value()
max_len = getattr(obj, "padded_string").size() // 8
val = str(val).rstrip()
setattr(obj, "padded_string", cls._add_ff_pad(val, max_len))
@classmethod
def apply_lat_long(cls, setting, obj):
name = setting.get_name()
is_latitude = name.endswith("latitude")
lat_long = setting.value.get_value().strip()
sign, l_d, l_m, l_s = cls._str_to_latlong(lat_long, is_latitude)
LOG.debug("%s: %d %d %d %d" % (name, sign, l_d, l_m, l_s))
setattr(obj, "%s_sign" % name, sign)
setattr(obj, "%s_degree" % name, l_d)
setattr(obj, "%s_minute" % name, l_m)
setattr(obj, "%s_second" % name, l_s)
def set_settings(self, settings):
_mem = self._memobj
for element in settings:
if not isinstance(element, RadioSetting):
self.set_settings(element)
continue
if not element.changed():
continue
try:
if element.has_apply_callback():
LOG.debug("Using apply callback")
try:
element.run_apply_callback()
except NotImplementedError as e:
LOG.error("ft1d.set_settings: %s", e)
continue
# Find the object containing setting.
obj = _mem
bits = element.get_name().split(".")
setting = bits[-1]
for name in bits[:-1]:
if name.endswith("]"):
name, index = name.split("[")
index = int(index[:-1])
obj = getattr(obj, name)[index]
else:
obj = getattr(obj, name)
try:
old_val = getattr(obj, setting)
LOG.debug("Setting %s(%r) <= %s" % (
element.get_name(), old_val, element.value))
setattr(obj, setting, element.value)
except AttributeError as e:
LOG.error("Setting %s is not in the memory map: %s" %
(element.get_name(), e))
except Exception:
LOG.debug(f'Trouble setting "{element.get_name()}"')
raise
def apply_ff_padded_yaesu(cls, setting, obj):
# FF pad yaesus custom string format.
rawval = setting.value.get_value()
max_len = getattr(obj, "padded_yaesu").size() // 8
rawval = str(rawval).rstrip()
val = [CHARSET.index(x) for x in rawval]
for x in range(len(val), max_len):
val.append(0xFF)
obj.padded_yaesu = val
def apply_volume(cls, setting, vfo):
val = setting.value.get_value()
cls._memobj.vfo_info[(vfo * 2)].volume = val
cls._memobj.vfo_info[(vfo * 2) + 1].volume = val
def apply_lcd_contrast(cls, setting, obj):
rawval = setting.value.get_value()
val = cls._LCD_CONTRAST.index(rawval) + 1
obj.lcd_contrast = val
def apply_dtmf(cls, setting, i):
rawval = setting.value.get_value().upper().rstrip()
val = [FT1_DTMF_CHARS.index(x) for x in rawval]
for x in range(len(val), 16):
val.append(0xFF)
cls._memobj.dtmf[i].memory = val
def apply_backtrack_status(cls, setting, obj):
status = setting.value.get_value()
if status == 'Valid':
val = 1
else:
val = 8
setattr(obj, "status", val)
def apply_NShemi(cls, setting, obj):
hemi = setting.value.get_value().upper()
if hemi != 'N' and hemi != 'S':
hemi = ' '
setattr(obj, "NShemi", hemi)
def apply_WEhemi(cls, setting, obj):
hemi = setting.value.get_value().upper()
if hemi != 'W' and hemi != 'E':
hemi = ' '
setattr(obj, "WEhemi", hemi)
def apply_bt_lat(cls, setting, obj):
val = setting.value.get_value()
val = cls.zero_pad(val, 3)
setattr(obj, "lat", val)
def apply_bt_lat_min(cls, setting, obj):
val = setting.value.get_value()
val = cls.zero_pad(val, 2)
setattr(obj, "lat_min", val)
def apply_bt_lat_dec_sec(cls, setting, obj):
val = setting.value.get_value()
val = cls.zero_pad(val, 4)
setattr(obj, "lat_dec_sec", val)
def apply_bt_lon(cls, setting, obj):
val = setting.value.get_value()
val = cls.zero_pad(val, 3)
setattr(obj, "lon", val)
def apply_bt_lon_min(cls, setting, obj):
val = setting.value.get_value()
val = cls.zero_pad(val, 2)
setattr(obj, "lon_min", val)
def apply_bt_lon_dec_sec(cls, setting, obj):
val = setting.value.get_value()
val = cls.zero_pad(val, 4)
setattr(obj, "lon_dec_sec", val)
def load_mmap(self, filename):
if filename.lower().endswith(self._adms_ext):
with open(filename, 'rb') as f:
self._adms_header = f.read(0x16)
LOG.debug('ADMS Header:\n%s',
util.hexprint(self._adms_header))
self._mmap = memmap.MemoryMapBytes(self._model + f.read())
LOG.info('Loaded ADMS file')
self.process_mmap()
else:
chirp_common.CloneModeRadio.load_mmap(self, filename)
def save_mmap(self, filename):
if filename.lower().endswith(self._adms_ext):
if not hasattr(self, '_adms_header'):
raise Exception('Unable to save .img to %s' % self._adms_ext)
with open(filename, 'wb') as f:
f.write(self._adms_header)
f.write(self._mmap.get_packed()[5:])
LOG.info('Wrote file')
else:
chirp_common.CloneModeRadio.save_mmap(self, filename)
chirp-20261030/chirp/drivers/ft2800.py 0000644 0001750 0001750 00000024405 14665027664 016137 0 ustar hibby hibby # Copyright 2011 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 time
import logging
from chirp import util, memmap, chirp_common, bitwise, directory, errors
from chirp.drivers.yaesu_clone import YaesuCloneModeRadio
LOG = logging.getLogger(__name__)
CHUNK_SIZE = 16
def _send(s, data):
for i in range(0, len(data), CHUNK_SIZE):
chunk = data[i:i+CHUNK_SIZE]
s.write(chunk)
echo = s.read(len(chunk))
if chunk != echo:
raise Exception("Failed to read echo chunk")
# The IDBLOCK is the first thing sent during an upload or download
# and indicates the radio subtype:
# USA Unmodified: b"\x0c\x01\x41\x33\x35\x02\x00\xb8"
# USA With extended TX mod: b"\x0c\x01\x41\x33\x35\x03\x00\xb9"
SUPPORTED_IDBLOCKS = [b"\x0c\x01\x41\x33\x35\x02\x00\xb8",
b"\x0c\x01\x41\x33\x35\x03\x00\xb9"]
TRAILER = b"\x0c\x02\x41\x33\x35\x00\x00\xb7"
ACK = b"\x0C\x06\x00"
def _download(radio):
data = b""
attempts = 30
for _i in range(0, attempts):
data = radio.pipe.read(8)
if data in SUPPORTED_IDBLOCKS:
radio.subtype = data
break
LOG.debug('Download attempt %i received %i: %s',
_i, len(data), util.hexprint(data))
if radio.status_fn:
status = chirp_common.Status()
status.max = 1
status.cur = 0
status.msg = "Waiting for radio (%i)" % (
attempts - (_i + 1))
radio.status_fn(status)
LOG.debug("Header:\n%s" % util.hexprint(data))
if len(data) != 8:
raise Exception("Failed to read header")
_send(radio.pipe, ACK)
data = b""
while len(data) < radio._block_sizes[1]:
time.sleep(0.1)
chunk = radio.pipe.read(38)
LOG.debug("Got: %i:\n%s" % (len(chunk), util.hexprint(chunk)))
if len(chunk) == 8:
LOG.debug("END?")
elif len(chunk) != 38:
LOG.debug("Should fail?")
break
# raise Exception("Failed to get full data block")
else:
cs = 0
for byte in chunk[:-1]:
cs += byte
if chunk[-1] != (cs & 0xFF):
raise Exception("Block failed checksum!")
data += chunk[5:-1]
_send(radio.pipe, ACK)
if radio.status_fn:
status = chirp_common.Status()
status.max = radio._block_sizes[1]
status.cur = len(data)
status.msg = "Cloning from radio"
radio.status_fn(status)
LOG.debug("Total: %i" % len(data))
return memmap.MemoryMapBytes(data)
def _upload(radio):
for _i in range(0, 10):
data = radio.pipe.read(256)
if not data:
break
LOG.debug("What is this garbage?\n%s" % util.hexprint(data))
_send(radio.pipe, radio.subtype)
time.sleep(1)
ack = radio.pipe.read(300)
LOG.debug("Ack was (%i):\n%s" % (len(ack), util.hexprint(ack)))
if ack != ACK:
raise Exception("Radio did not ack ID")
block = 0
while block < (radio.get_memsize() // 32):
data = b"\x0C\x03\x00\x00" + bytes([block])
data += radio.get_mmap()[block*32:(block+1)*32]
cs = 0
for byte in data:
cs += byte
data += bytes([cs & 0xFF])
LOG.debug("Writing block %i:\n%s" % (block, util.hexprint(data)))
_send(radio.pipe, data)
time.sleep(0.1)
ack = radio.pipe.read(3)
if ack != ACK:
raise Exception("Radio did not ack block %i" % block)
if radio.status_fn:
status = chirp_common.Status()
status.max = radio._block_sizes[1]
status.cur = block * 32
status.msg = "Cloning to radio"
radio.status_fn(status)
block += 1
_send(radio.pipe, TRAILER)
MEM_FORMAT = """
struct {
bbcd freq[4];
u8 unknown1[4];
bbcd offset[2];
u8 unknown2[2];
u8 pskip:1,
skip:1,
unknown3:1,
isnarrow:1,
power:2,
duplex:2;
u8 unknown4:6,
tmode:2;
u8 tone;
u8 dtcs;
} memory[200];
#seekto 0x0E00;
struct {
char name[6];
} names[200];
"""
MODES = ["FM", "NFM"]
TMODES = ["", "Tone", "TSQL", "DTCS"]
DUPLEX = ["", "-", "+", ""]
POWER_LEVELS = [chirp_common.PowerLevel("Hi", watts=65),
chirp_common.PowerLevel("Mid", watts=25),
chirp_common.PowerLevel("Low2", watts=10),
chirp_common.PowerLevel("Low1", watts=5),
]
CHARSET = chirp_common.CHARSET_UPPER_NUMERIC + "()+-=*/???|_"
@directory.register
class FT2800Radio(YaesuCloneModeRadio):
"""Yaesu FT-2800"""
VENDOR = "Yaesu"
MODEL = "FT-2800M"
_block_sizes = [8, 7680]
_memsize = 7680
@property
def subtype(self):
# If our image is from before the subtype was stashed, assume
# the default unmodified US ID block
return bytes(self.metadata.get('subtype_idblock',
SUPPORTED_IDBLOCKS[0]))
@subtype.setter
def subtype(self, value):
self.metadata = {'subtype_idblock': [x for x in value]}
@classmethod
def get_prompts(cls):
rp = chirp_common.RadioPrompts()
rp.pre_download = _(
"1. Turn radio off.\n"
"2. Connect cable\n"
"3. Press and hold in the MHz, Low, and D/MR keys on the radio "
"while turning it on\n"
"4. Radio is in clone mode when TX/RX is flashing\n"
"5. After clicking OK , "
"press the MHz key on the radio to send"
" image.\n"
" (\"TX\" will appear on the LCD). \n")
rp.pre_upload = _(
"1. Turn radio off.\n"
"2. Connect cable\n"
"3. Press and hold in the MHz, Low, and D/MR keys on the radio "
"while turning it on\n"
"4. Radio is in clone mode when TX/RX is flashing\n"
"5. Press the Low key on the radio "
"(\"RX\" will appear on the LCD).\n"
"6. Click OK.")
return rp
def get_features(self):
rf = chirp_common.RadioFeatures()
rf.memory_bounds = (0, 199)
rf.has_ctone = False
rf.has_tuning_step = False
rf.has_dtcs_polarity = False
rf.has_bank = False
rf.valid_tuning_steps = [5.0, 10.0, 12.5, 15.0,
20.0, 25.0, 50.0, 100.0]
rf.valid_modes = MODES
rf.valid_tmodes = TMODES
rf.valid_bands = [(137000000, 174000000)]
rf.valid_power_levels = POWER_LEVELS
rf.valid_duplexes = DUPLEX
rf.valid_skips = ["", "S", "P"]
rf.valid_name_length = 6
rf.valid_characters = CHARSET
return rf
def sync_in(self):
self.pipe.parity = "E"
self.pipe.timeout = 1
start = time.time()
try:
self._mmap = _download(self)
except errors.RadioError:
raise
except Exception as e:
LOG.exception('Failed download')
raise errors.RadioError("Failed to communicate with radio: %s" % e)
LOG.info("Downloaded in %.2f sec" % (time.time() - start))
self.process_mmap()
def sync_out(self):
self.pipe.timeout = 1
self.pipe.parity = "E"
start = time.time()
try:
_upload(self)
except errors.RadioError:
raise
except Exception as e:
LOG.exception('Failed upload')
raise errors.RadioError("Failed to communicate with radio: %s" % e)
LOG.info("Uploaded in %.2f sec" % (time.time() - start))
def process_mmap(self):
self._memobj = bitwise.parse(MEM_FORMAT, self._mmap)
def get_raw_memory(self, number):
return repr(self._memobj.memory[number])
def get_memory(self, number):
_mem = self._memobj.memory[number]
_nam = self._memobj.names[number]
mem = chirp_common.Memory()
mem.number = number
if _mem.get_raw(asbytes=False)[0] == "\xFF":
mem.empty = True
return mem
mem.freq = int(_mem.freq) * 10
mem.offset = int(_mem.offset) * 100000
mem.duplex = DUPLEX[_mem.duplex]
mem.tmode = TMODES[_mem.tmode]
mem.rtone = chirp_common.TONES[_mem.tone]
mem.dtcs = chirp_common.DTCS_CODES[_mem.dtcs]
mem.name = str(_nam.name).rstrip()
mem.mode = _mem.isnarrow and "NFM" or "FM"
mem.skip = _mem.pskip and "P" or _mem.skip and "S" or ""
mem.power = POWER_LEVELS[_mem.power]
return mem
def set_memory(self, mem):
_mem = self._memobj.memory[mem.number]
_nam = self._memobj.names[mem.number]
if mem.empty:
_mem.set_raw("\xFF" * (_mem.size() // 8))
return
if _mem.get_raw(asbytes=False)[0] == "\xFF":
# Empty -> Non-empty, so initialize
_mem.set_raw("\x00" * (_mem.size() // 8))
# initializing unknowns
_mem.unknown1 = (0xFF, 0xFF, 0xFF, 0xFF)
_mem.unknown2 = (0x00, 0x00)
_mem.unknown3 = 0x01
_mem.unknown4 = 0x3C
_mem.freq = mem.freq / 10
_mem.offset = mem.offset / 100000
_mem.duplex = DUPLEX.index(mem.duplex)
_mem.tmode = TMODES.index(mem.tmode)
_mem.tone = chirp_common.TONES.index(mem.rtone)
_mem.dtcs = chirp_common.DTCS_CODES.index(mem.dtcs)
_mem.isnarrow = MODES.index(mem.mode)
_mem.pskip = mem.skip == "P"
_mem.skip = mem.skip == "S"
if mem.power:
_mem.power = POWER_LEVELS.index(mem.power)
else:
_mem.power = 0
_nam.name = mem.name.ljust(6)[:6]
@classmethod
def match_model(cls, filedata, filename):
return len(filedata) == cls._memsize
chirp-20261030/chirp/drivers/ft2900.py 0000644 0001750 0001750 00000123542 14670320477 016134 0 ustar hibby hibby # Copyright 2011 Dan Smith
#
# FT-2900-specific modifications by Richard Cochran,
# Initial work on settings by Chris Fosnight,
#
# 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 time
import logging
from chirp import util, memmap, chirp_common, bitwise, directory, errors
from chirp.drivers.yaesu_clone import YaesuCloneModeRadio
from chirp.settings import RadioSetting, RadioSettingGroup, \
RadioSettingValueList, RadioSettingValueString, RadioSettings
LOG = logging.getLogger(__name__)
def _send(s, data):
s.write(data)
echo = s.read(len(data))
if data != echo:
raise Exception("Failed to read echo")
LOG.debug("got echo\n%s\n" % util.hexprint(echo))
ACK = b"\x06"
INITIAL_CHECKSUM = 0
def _download(radio):
blankChunk = b""
for _i in range(0, 32):
blankChunk += b"\xff"
LOG.debug("in _download\n")
data = b""
for _i in range(0, 20):
data = radio.pipe.read(20)
LOG.debug("Header:\n%s" % util.hexprint(data))
LOG.debug("len(header) = %s\n" % len(data))
if data == radio.IDBLOCK:
break
if data != radio.IDBLOCK:
raise Exception("Failed to read header")
_send(radio.pipe, ACK)
# initialize data, the big var that holds all memory
data = b""
_blockNum = 0
while len(data) < radio._block_sizes[1]:
_blockNum += 1
time.sleep(0.03)
chunk = radio.pipe.read(32)
LOG.debug("Block %i " % (_blockNum))
if chunk == blankChunk:
LOG.debug("blank chunk\n")
else:
LOG.debug("Got: %i:\n%s" % (len(chunk), util.hexprint(chunk)))
if len(chunk) != 32:
LOG.debug("len chunk is %i\n" % (len(chunk)))
raise Exception("Failed to get full data block")
else:
data += chunk
if radio.status_fn:
status = chirp_common.Status()
status.max = radio._block_sizes[1]
status.cur = len(data)
status.msg = "Cloning from radio"
radio.status_fn(status)
LOG.debug("Total: %i" % len(data))
# radio should send us one final termination byte, containing
# checksum
chunk = radio.pipe.read(32)
if len(chunk) != 1:
LOG.debug("len(chunk) is %i\n" % len(chunk))
raise Exception("radio sent extra unknown data")
LOG.debug("Got: %i:\n%s" % (len(chunk), util.hexprint(chunk)))
# compute checksum
cs = INITIAL_CHECKSUM
for byte in radio.IDBLOCK:
cs += byte
for byte in data:
cs += byte
LOG.debug("calculated checksum is %x\n" % (cs & 0xff))
LOG.debug("Radio sent checksum is %x\n" % chunk[0])
if (cs & 0xff) != chunk[0]:
raise Exception("Failed checksum on read.")
# for debugging purposes, dump the channels, in hex.
for _i in range(0, 200):
_startData = 1892 + 20 * _i
chunk = data[_startData:_startData + 20]
LOG.debug("channel %i:\n%s" % (_i, util.hexprint(chunk)))
return memmap.MemoryMapBytes(data)
def _upload(radio):
for _i in range(0, 10):
data = radio.pipe.read(256)
if not data:
break
LOG.debug("What is this garbage?\n%s" % util.hexprint(data))
raise Exception("Radio sent unrecognized data")
_send(radio.pipe, radio.IDBLOCK)
time.sleep(.2)
ack = radio.pipe.read(300)
LOG.debug("Ack was (%i):\n%s" % (len(ack), util.hexprint(ack)))
if ack != ACK:
raise Exception("Radio did not ack ID. Check cable, verify"
" radio is not locked.\n"
" (press & Hold red \"*L\" button to unlock"
" radio if needed)")
block = 0
cs = INITIAL_CHECKSUM
for byte in radio.IDBLOCK:
cs += byte
while block < (radio.get_memsize() // 32):
data = radio.get_mmap()[block * 32:(block + 1) * 32]
LOG.debug("Writing block %i:\n%s" % (block, util.hexprint(data)))
_send(radio.pipe, data)
time.sleep(0.03)
for byte in data:
cs += byte
if radio.status_fn:
status = chirp_common.Status()
status.max = radio._block_sizes[1]
status.cur = block * 32
status.msg = "Cloning to radio"
radio.status_fn(status)
block += 1
_send(radio.pipe, bytes([cs & 0xFF]))
MEM_FORMAT = """
#seekto 0x0080;
struct {
u8 apo;
u8 arts_beep;
u8 bell;
u8 dimmer;
u8 cw_id_string[16];
u8 cw_trng;
u8 x95;
u8 x96;
u8 x97;
u8 int_cd;
u8 int_set;
u8 x9A;
u8 x9B;
u8 lock;
u8 x9D;
u8 mic_gain;
u8 open_msg;
u8 openMsg_Text[6];
u8 rf_sql;
u8 unk:6,
pag_abk:1,
unk1:1;
u8 pag_cdr_1;
u8 pag_cdr_2;
u8 pag_cdt_1;
u8 pag_cdt_2;
u8 prog_p1;
u8 xAD;
u8 prog_p2;
u8 xAF;
u8 prog_p3;
u8 xB1;
u8 prog_p4;
u8 xB3;
u8 resume;
u8 tot;
u8 unk2:1,
cw_id:1,
unk3:1,
ts_speed:1,
ars:1,
unk4:2,
dtmf_mode:1;
u8 unk5:1,
ts_mut:1,
wires_auto:1,
busy_lockout:1,
edge_beep:1,
unk6:3;
u8 unk7:2,
s_search:1,
unk8:2,
cw_trng_units:1,
unk9:2;
u8 dtmf_speed:1,
unk10:2,
arts_interval:1,
unk11:1,
inverted_dcs:1,
unk12:1,
mw_mode:1;
u8 unk13:2,
wires_mode:1,
wx_alert:1,
unk14:1,
wx_vol_max:1,
revert:1,
unk15:1;
u8 vfo_scan;
u8 scan_mode;
u8 dtmf_delay;
u8 beep;
u8 xBF;
} settings;
//#seekto 0x00c0;
struct {
u16 in_use;
} bank_used[8];
//#seekto 0x00d0;
u8 passwd[4];
u8 mbs;
#seekto 0x00ef;
u8 currentTone;
//#seekto 0x00f0;
u8 curChannelMem[20];
#seekto 0x0127;
u8 curChannelNum;
#seekto 0x012a;
u8 banksoff1;
#seekto 0x15f;
u8 checksum1;
#seekto 0x16f;
u8 curentTone2;
#seekto 0x1aa;
u16 banksoff2;
#seekto 0x1df;
u8 checksum2;
//#seekto 0x1e0;
struct {
u8 dtmf_string[16];
} dtmf_strings[10];
#seekto 0x0360;
struct{
u8 name[6];
} bank_names[8];
#seekto 0x03c4;
struct{
u16 channels[50];
} banks[8];
//#seekto 0x06e4;
struct {
u8 even_pskip:1,
even_skip:1,
even_valid:1,
even_masked:1,
odd_pskip:1,
odd_skip:1,
odd_valid:1,
odd_masked:1;
} flags[128];
//#seekto 0x0764;
struct {
u8 unknown0:2,
isnarrow:1,
unknown1:5;
u8 unknown2:2,
duplex:2,
unknown3:1,
step:3;
bbcd freq[3];
u8 power:2,
unknown4:3,
tmode:3;
u8 name[6];
bbcd offset[3];
u8 ctonesplitflag:1,
ctone:7;
u8 rx_dtcssplitflag:1,
rx_dtcs:7;
u8 unknown5;
u8 rtonesplitflag:1,
rtone:7;
u8 dtcssplitflag:1,
dtcs:7;
} memory[200];
"""
MODES = ["FM", "NFM"]
TMODES = ["", "Tone", "TSQL", "DTCS", "TSQL-R", "Cross"]
CROSS_MODES = ["DTCS->", "Tone->DTCS", "DTCS->Tone",
"Tone->Tone", "DTCS->DTCS"]
DUPLEX = ["", "-", "+", "split"]
POWER_LEVELS = [chirp_common.PowerLevel("Hi", watts=75),
chirp_common.PowerLevel("Low3", watts=30),
chirp_common.PowerLevel("Low2", watts=10),
chirp_common.PowerLevel("Low1", watts=5),
]
CHARSET = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ +-/?C[] _"
STEPS = [5.0, 10.0, 12.5, 15.0, 20.0, 25.0, 50.0, 100.0]
def _decode_tone(radiotone):
try:
chirptone = chirp_common.TONES[radiotone]
except IndexError:
chirptone = 100
LOG.debug("found invalid radio tone: %i\n" % radiotone)
return chirptone
def _decode_dtcs(radiodtcs):
try:
chirpdtcs = chirp_common.DTCS_CODES[radiodtcs]
except IndexError:
chirpdtcs = 23
LOG.debug("found invalid radio dtcs code: %i\n" % radiodtcs)
return chirpdtcs
def _decode_name(mem):
name = ""
for i in mem:
if (i & 0x7F) == 0x7F:
break
try:
name += CHARSET[i & 0x7F]
except IndexError:
LOG.debug("Unknown char index: %x " % (i))
name = name.strip()
return name
def _encode_name(mem):
if (mem.strip() == ""):
return [0xff] * 6
name = [None] * 6
for i in range(0, 6):
try:
name[i] = CHARSET.index(mem[i])
except IndexError:
name[i] = CHARSET.index(" ")
name[0] = name[0] | 0x80
return name
def _wipe_memory(mem):
mem.fill_raw(b"\xff")
class FT2900Bank(chirp_common.NamedBank):
def get_name(self):
_bank = self._model._radio._memobj.bank_names[self.index]
name = ""
for i in _bank.name:
if i == 0xff:
break
name += CHARSET[i & 0x7f]
return name.rstrip()
def set_name(self, name):
name = name.upper().ljust(6)[:6]
_bank = self._model._radio._memobj.bank_names[self.index]
_bank.name = [CHARSET.index(x) for x in name.ljust(6)[:6]]
class FT2900BankModel(chirp_common.BankModel):
def get_num_mappings(self):
return 8
def get_mappings(self):
banks = self._radio._memobj.banks
bank_mappings = []
for index, _bank in enumerate(banks):
bank = FT2900Bank(self, "%i" % index, "b%i" % (index + 1))
bank.index = index
bank_mappings.append(bank)
return bank_mappings
def _get_channel_numbers_in_bank(self, bank):
_bank_used = self._radio._memobj.bank_used[bank.index]
if _bank_used.in_use == 0xffff:
return set()
_members = self._radio._memobj.banks[bank.index]
return set([int(ch) for ch in _members.channels if ch != 0xffff])
def _update_bank_with_channel_numbers(self, bank, channels_in_bank):
_members = self._radio._memobj.banks[bank.index]
if len(channels_in_bank) > len(_members.channels):
raise Exception("More than %i entries in bank %d" %
(len(_members.channels), bank.index))
empty = 0
for index, channel_number in enumerate(sorted(channels_in_bank)):
_members.channels[index] = channel_number
empty = index + 1
for index in range(empty, len(_members.channels)):
_members.channels[index] = 0xffff
_bank_used = self._radio._memobj.bank_used[bank.index]
if empty == 0:
_bank_used.in_use = 0xffff
else:
_bank_used.in_use = empty - 1
def add_memory_to_mapping(self, memory, bank):
channels_in_bank = self._get_channel_numbers_in_bank(bank)
channels_in_bank.add(memory.number)
self._update_bank_with_channel_numbers(bank, channels_in_bank)
# tells radio that banks are active
self._radio._memobj.banksoff1 = bank.index
self._radio._memobj.banksoff2 = bank.index
def remove_memory_from_mapping(self, memory, bank):
channels_in_bank = self._get_channel_numbers_in_bank(bank)
try:
channels_in_bank.remove(memory.number)
except KeyError:
raise Exception("Memory %i is not in bank %s. Cannot remove" %
(memory.number, bank))
self._update_bank_with_channel_numbers(bank, channels_in_bank)
def get_mapping_memories(self, bank):
memories = []
for channel in self._get_channel_numbers_in_bank(bank):
memories.append(self._radio.get_memory(channel))
return memories
def get_memory_mappings(self, memory):
banks = []
for bank in self.get_mappings():
if memory.number in self._get_channel_numbers_in_bank(bank):
banks.append(bank)
return banks
@directory.register
class FT2900Radio(YaesuCloneModeRadio):
"""Yaesu FT-2900"""
VENDOR = "Yaesu"
MODEL = "FT-2900R/1900R"
IDBLOCK = b"\x56\x43\x32\x33\x00\x02\x46\x01\x01\x01"
BAUD_RATE = 19200
_memsize = 8000
_block_sizes = [8, 8000]
def get_features(self):
rf = chirp_common.RadioFeatures()
rf.memory_bounds = (0, 199)
rf.can_odd_split = True
rf.has_ctone = True
rf.has_rx_dtcs = True
rf.has_cross = True
rf.has_dtcs_polarity = False
rf.has_bank = True
rf.has_bank_names = True
rf.has_settings = True
rf.valid_tuning_steps = STEPS
rf.valid_modes = MODES
rf.valid_tmodes = TMODES
rf.valid_cross_modes = CROSS_MODES
rf.valid_bands = [(136000000, 174000000)]
rf.valid_power_levels = POWER_LEVELS
rf.valid_duplexes = DUPLEX
rf.valid_skips = ["", "S", "P"]
rf.valid_name_length = 6
rf.valid_characters = CHARSET
return rf
def sync_in(self):
start = time.time()
try:
self._mmap = _download(self)
except errors.RadioError:
raise
except Exception as e:
raise errors.RadioError("Failed to communicate with radio: %s" % e)
LOG.info("Downloaded in %.2f sec" % (time.time() - start))
self.process_mmap()
def sync_out(self):
self.pipe.timeout = 1
start = time.time()
try:
_upload(self)
except errors.RadioError:
raise
except Exception as e:
raise errors.RadioError("Failed to communicate with radio: %s" % e)
LOG.info("Uploaded in %.2f sec" % (time.time() - start))
def process_mmap(self):
self._memobj = bitwise.parse(MEM_FORMAT, self._mmap)
def get_raw_memory(self, number):
return repr(self._memobj.memory[number])
def get_memory(self, number):
_mem = self._memobj.memory[number]
_flag = self._memobj.flags[(number) / 2]
nibble = ((number) % 2) and "even" or "odd"
used = _flag["%s_masked" % nibble]
valid = _flag["%s_valid" % nibble]
pskip = _flag["%s_pskip" % nibble]
skip = _flag["%s_skip" % nibble]
mem = chirp_common.Memory()
mem.number = number
if _mem.get_raw()[0] == 0xFF or not valid or not used:
mem.empty = True
return mem
mem.tuning_step = STEPS[_mem.step]
mem.freq = int(_mem.freq) * 1000
# compensate for 12.5 kHz tuning steps, add 500 Hz if needed
if (mem.tuning_step == 12.5):
lastdigit = int(_mem.freq) % 10
if (lastdigit == 2 or lastdigit == 7):
mem.freq += 500
mem.offset = chirp_common.fix_rounded_step(int(_mem.offset) * 1000)
mem.duplex = DUPLEX[_mem.duplex]
if _mem.tmode < TMODES.index("Cross"):
mem.tmode = TMODES[_mem.tmode]
mem.cross_mode = CROSS_MODES[0]
else:
mem.tmode = "Cross"
mem.cross_mode = CROSS_MODES[_mem.tmode - TMODES.index("Cross")]
mem.rtone = _decode_tone(_mem.rtone)
mem.ctone = _decode_tone(_mem.ctone)
# check for unequal ctone/rtone in TSQL mode. map it as a
# cross tone mode
if mem.rtone != mem.ctone and (mem.tmode == "TSQL" or
mem.tmode == "Tone"):
mem.tmode = "Cross"
mem.cross_mode = "Tone->Tone"
mem.dtcs = _decode_dtcs(_mem.dtcs)
mem.rx_dtcs = _decode_dtcs(_mem.rx_dtcs)
# check for unequal dtcs/rx_dtcs in DTCS mode. map it as a
# cross tone mode
if mem.dtcs != mem.rx_dtcs and mem.tmode == "DTCS":
mem.tmode = "Cross"
mem.cross_mode = "DTCS->DTCS"
if (int(_mem.name[0]) & 0x80) != 0:
mem.name = _decode_name(_mem.name)
mem.mode = _mem.isnarrow and "NFM" or "FM"
mem.skip = pskip and "P" or skip and "S" or ""
mem.power = POWER_LEVELS[3 - _mem.power]
return mem
def set_memory(self, mem):
_mem = self._memobj.memory[mem.number]
_flag = self._memobj.flags[(mem.number) / 2]
nibble = ((mem.number) % 2) and "even" or "odd"
valid = _flag["%s_valid" % nibble]
used = _flag["%s_masked" % nibble]
if not valid:
_wipe_memory(_mem)
if mem.empty and valid and not used:
_flag["%s_valid" % nibble] = False
return
_flag["%s_masked" % nibble] = not mem.empty
if mem.empty:
return
_flag["%s_valid" % nibble] = True
_mem.freq = mem.freq / 1000
_mem.offset = mem.offset / 1000
_mem.duplex = DUPLEX.index(mem.duplex)
# clear all the split tone flags -- we'll set them as needed below
_mem.ctonesplitflag = 0
_mem.rx_dtcssplitflag = 0
_mem.rtonesplitflag = 0
_mem.dtcssplitflag = 0
if mem.tmode != "Cross":
_mem.tmode = TMODES.index(mem.tmode)
# for the non-cross modes, use ONE tone for both send
# and receive but figure out where to get it from.
if mem.tmode == "TSQL" or mem.tmode == "TSQL-R":
_mem.rtone = chirp_common.TONES.index(mem.ctone)
_mem.ctone = chirp_common.TONES.index(mem.ctone)
else:
_mem.rtone = chirp_common.TONES.index(mem.rtone)
_mem.ctone = chirp_common.TONES.index(mem.rtone)
# and one tone for dtcs, but this is always the sending one
_mem.dtcs = chirp_common.DTCS_CODES.index(mem.dtcs)
_mem.rx_dtcs = chirp_common.DTCS_CODES.index(mem.dtcs)
else:
_mem.rtone = chirp_common.TONES.index(mem.rtone)
_mem.ctone = chirp_common.TONES.index(mem.ctone)
_mem.dtcs = chirp_common.DTCS_CODES.index(mem.dtcs)
_mem.rx_dtcs = chirp_common.DTCS_CODES.index(mem.rx_dtcs)
if mem.cross_mode == "Tone->Tone":
# tone->tone cross mode is treated as
# TSQL, but with separate tones for
# send and receive
_mem.tmode = TMODES.index("TSQL")
_mem.rtonesplitflag = 1
elif mem.cross_mode == "DTCS->DTCS":
# DTCS->DTCS cross mode is treated as
# DTCS, but with separate codes for
# send and receive
_mem.tmode = TMODES.index("DTCS")
_mem.dtcssplitflag = 1
else:
_mem.tmode = TMODES.index("Cross") + \
CROSS_MODES.index(mem.cross_mode)
_mem.isnarrow = MODES.index(mem.mode)
_mem.step = STEPS.index(mem.tuning_step)
_flag["%s_pskip" % nibble] = mem.skip == "P"
_flag["%s_skip" % nibble] = mem.skip == "S"
if mem.power:
_mem.power = 3 - POWER_LEVELS.index(mem.power)
else:
_mem.power = 3
_mem.name = _encode_name(mem.name)
# set all unknown areas of the memory map to 0
_mem.unknown0 = 0
_mem.unknown1 = 0
_mem.unknown2 = 0
_mem.unknown3 = 0
_mem.unknown4 = 0
_mem.unknown5 = 0
LOG.debug("encoded mem\n%s\n" % (util.hexprint(
_mem.get_raw()[0:20])))
def get_settings(self):
_settings = self._memobj.settings
_dtmf_strings = self._memobj.dtmf_strings
_passwd = self._memobj.passwd
repeater = RadioSettingGroup("repeater", "Repeater Settings")
ctcss = RadioSettingGroup("ctcss", "CTCSS/DCS/EPCS Settings")
arts = RadioSettingGroup("arts", "ARTS Settings")
mbls = RadioSettingGroup("banks", "Memory Settings")
scan = RadioSettingGroup("scan", "Scan Settings")
dtmf = RadioSettingGroup("dtmf", "DTMF Settings")
wires = RadioSettingGroup("wires", "WiRES(tm) Settings")
switch = RadioSettingGroup("switch", "Switch/Knob Settings")
disp = RadioSettingGroup("disp", "Display Settings")
misc = RadioSettingGroup("misc", "Miscellaneous Settings")
setmode = RadioSettings(repeater, ctcss, arts, mbls, scan,
dtmf, wires, switch, disp, misc)
# numbers and names of settings refer to the way they're
# presented in the set menu, as well as the list starting on
# page 74 of the manual
# 1 APO
opts = ["Off", "30 Min", "1 Hour", "3 Hour", "5 Hour", "8 Hour"]
misc.append(
RadioSetting(
"apo", "Automatic Power Off",
RadioSettingValueList(opts, current_index=_settings.apo)))
# 2 AR.BEP
opts = ["Off", "In Range", "Always"]
arts.append(
RadioSetting(
"arts_beep", "ARTS Beep",
RadioSettingValueList(
opts, current_index=_settings.arts_beep)))
# 3 AR.INT
opts = ["15 Sec", "25 Sec"]
arts.append(
RadioSetting(
"arts_interval", "ARTS Polling Interval",
RadioSettingValueList(
opts, current_index=_settings.arts_interval)))
# 4 ARS
opts = ["Off", "On"]
repeater.append(
RadioSetting(
"ars", "Automatic Repeater Shift",
RadioSettingValueList(opts, current_index=_settings.ars)))
# 5 BCLO
opts = ["Off", "On"]
misc.append(RadioSetting(
"busy_lockout", "Busy Channel Lock-Out",
RadioSettingValueList(opts, current_index=_settings.busy_lockout)))
# 6 BEEP
opts = ["Off", "Key+Scan", "Key"]
switch.append(RadioSetting(
"beep", "Enable the Beeper",
RadioSettingValueList(opts, current_index=_settings.beep)))
# 7 BELL
opts = ["Off", "1", "3", "5", "8", "Continuous"]
ctcss.append(
RadioSetting(
"bell", "Bell Repetitions",
RadioSettingValueList(
opts, current_index=_settings.bell)))
# 8 BNK.LNK
for i in range(0, 8):
opts = ["Off", "On"]
mbs = (self._memobj.mbs >> i) & 1
rs = RadioSetting("mbs%i" % i, "Bank %s Scan" % (i + 1),
RadioSettingValueList(opts, current_index=mbs))
def apply_mbs(s, index):
if int(s.value):
self._memobj.mbs |= (1 << index)
else:
self._memobj.mbs &= ~(1 << index)
rs.set_apply_callback(apply_mbs, i)
mbls.append(rs)
# 9 BNK.NM - A per-bank attribute, nothing to do here.
# 10 CLK.SFT - A per-channel attribute, nothing to do here.
# 11 CW.ID
opts = ["Off", "On"]
arts.append(
RadioSetting(
"cw_id", "CW ID Enable",
RadioSettingValueList(
opts, current_index=_settings.cw_id)))
cw_id_text = ""
for i in _settings.cw_id_string:
try:
cw_id_text += CHARSET[i & 0x7F]
except IndexError:
if i != 0xff:
LOG.debug("unknown char index in cw id: %x " % (i))
val = RadioSettingValueString(0, 16, cw_id_text, True)
val.set_charset(CHARSET + "abcdefghijklmnopqrstuvwxyz")
rs = RadioSetting("cw_id_string", "CW Identifier Text", val)
def apply_cw_id(s):
str = s.value.get_value().upper().rstrip()
mval = ""
mval = [chr(CHARSET.index(x)) for x in str]
for x in range(len(mval), 16):
mval.append(chr(0xff))
for x in range(0, 16):
_settings.cw_id_string[x] = ord(mval[x])
rs.set_apply_callback(apply_cw_id)
arts.append(rs)
# 12 CWTRNG
opts = ["Off", "4WPM", "5WPM", "6WPM", "7WPM", "8WPM", "9WPM",
"10WPM", "11WPM", "12WPM", "13WPM", "15WPM", "17WPM",
"20WPM", "24WPM", "30WPM", "40WPM"]
misc.append(
RadioSetting(
"cw_trng", "CW Training",
RadioSettingValueList(
opts, current_index=_settings.cw_trng)))
# todo: make the setting of the units here affect the display
# of the speed. Not critical, but would be slick.
opts = ["CPM", "WPM"]
misc.append(RadioSetting("cw_trng_units", "CW Training Units",
RadioSettingValueList(opts,
current_index=_settings.
cw_trng_units)))
# 13 DC VLT - a read-only status, so nothing to do here
# 14 DCS CD - A per-channel attribute, nothing to do here
# 15 DCS.RV
opts = ["Disabled", "Enabled"]
ctcss.append(
RadioSetting(
"inverted_dcs", "\"Inverted\" DCS Code Decoding",
RadioSettingValueList(
opts, current_index=_settings.inverted_dcs)))
# 16 DIMMER
opts = ["Off"] + ["Level %d" % (x) for x in range(1, 11)]
disp.append(RadioSetting("dimmer", "Dimmer",
RadioSettingValueList(opts,
current_index=_settings
.dimmer)))
# 17 DT.A/M
opts = ["Manual", "Auto"]
dtmf.append(RadioSetting("dtmf_mode", "DTMF Autodialer",
RadioSettingValueList(opts,
current_index=_settings
.dtmf_mode)))
# 18 DT.DLY
opts = ["50 ms", "250 ms", "450 ms", "750 ms", "1000 ms"]
dtmf.append(RadioSetting("dtmf_delay", "DTMF Autodialer Delay Time",
RadioSettingValueList(opts,
current_index=_settings
.dtmf_delay)))
# 19 DT.SET
for memslot in range(0, 10):
dtmf_memory = ""
for i in _dtmf_strings[memslot].dtmf_string:
if i != 0xFF:
try:
dtmf_memory += CHARSET[i]
except IndexError:
LOG.debug("unknown char index in dtmf: %x " % (i))
val = RadioSettingValueString(0, 16, dtmf_memory, True)
val.set_charset(CHARSET + "abcdef")
rs = RadioSetting("dtmf_string_%d" % memslot,
"DTMF Memory %d" % memslot, val)
def apply_dtmf(s, i):
LOG.debug("applying dtmf for %x\n" % i)
str = s.value.get_value().upper().rstrip()
LOG.debug("str is %s\n" % str)
mval = ""
mval = [chr(CHARSET.index(x)) for x in str]
for x in range(len(mval), 16):
mval.append(chr(0xff))
for x in range(0, 16):
_dtmf_strings[i].dtmf_string[x] = ord(mval[x])
rs.set_apply_callback(apply_dtmf, memslot)
dtmf.append(rs)
# 20 DT.SPD
opts = ["50 ms", "100 ms"]
dtmf.append(RadioSetting("dtmf_speed",
"DTMF Autodialer Sending Speed",
RadioSettingValueList(opts,
current_index=_settings.
dtmf_speed)))
# 21 EDG.BEP
opts = ["Off", "On"]
mbls.append(RadioSetting("edge_beep", "Band Edge Beeper",
RadioSettingValueList(opts,
current_index=_settings.
edge_beep)))
# 22 INT.CD
opts = ["DTMF %X" % (x) for x in range(0, 16)]
wires.append(
RadioSetting(
"int_cd", "Access Number for WiRES(TM)",
RadioSettingValueList(
opts, current_index=_settings.int_cd)))
# 23 ING MD
opts = ["Sister Radio Group", "Friends Radio Group"]
wires.append(
RadioSetting(
"wires_mode", "Internet Link Connection Mode",
RadioSettingValueList(
opts, current_index=_settings.wires_mode)))
# 24 INT.A/M
opts = ["Manual", "Auto"]
wires.append(RadioSetting("wires_auto", "Internet Link Autodialer",
RadioSettingValueList(opts,
current_index=_settings
.wires_auto)))
# 25 INT.SET
opts = ["F%d" % (x) for x in range(0, 10)]
wires.append(RadioSetting("int_set", "Memory Register for "
"non-WiRES Internet",
RadioSettingValueList(opts,
current_index=_settings
.int_set)))
# 26 LOCK
opts = ["Key", "Dial", "Key + Dial", "PTT",
"Key + PTT", "Dial + PTT", "All"]
switch.append(
RadioSetting(
"lock", "Control Locking",
RadioSettingValueList(
opts, current_index=_settings.lock)))
# 27 MCGAIN
opts = ["Level %d" % (x) for x in range(1, 10)]
misc.append(RadioSetting("mic_gain", "Microphone Gain",
RadioSettingValueList(opts,
current_index=_settings
.mic_gain)))
# 28 MEM.SCN
opts = ["Tag 1", "Tag 2", "All Channels"]
rs = RadioSetting("scan_mode", "Memory Scan Mode",
RadioSettingValueList(opts,
current_index=_settings
.scan_mode - 1))
# this setting is unusual in that it starts at 1 instead of 0.
# that is, index 1 corresponds to "Tag 1", and index 0 is invalid.
# so we create a custom callback to handle this.
def apply_scan_mode(s):
myopts = ["Tag 1", "Tag 2", "All Channels"]
_settings.scan_mode = myopts.index(s.value.get_value()) + 1
rs.set_apply_callback(apply_scan_mode)
mbls.append(rs)
# 29 MW MD
opts = ["Lower", "Next"]
mbls.append(RadioSetting("mw_mode", "Memory Write Mode",
RadioSettingValueList(opts,
current_index=_settings
.mw_mode)))
# 30 NM SET - This is per channel, so nothing to do here
# 31 OPN.MSG
opts = ["Off", "DC Supply Voltage", "Text Message"]
disp.append(RadioSetting("open_msg", "Opening Message Type",
RadioSettingValueList(opts,
current_index=_settings.
open_msg)))
openmsg = ""
for i in _settings.openMsg_Text:
try:
openmsg += CHARSET[i & 0x7F]
except IndexError:
if i != 0xff:
LOG.debug("unknown char index in openmsg: %x " % (i))
val = RadioSettingValueString(0, 6, openmsg, True)
val.set_charset(CHARSET + "abcdefghijklmnopqrstuvwxyz")
rs = RadioSetting("openMsg_Text", "Opening Message Text", val)
def apply_openmsg(s):
str = s.value.get_value().upper().rstrip()
mval = ""
mval = [chr(CHARSET.index(x)) for x in str]
for x in range(len(mval), 6):
mval.append(chr(0xff))
for x in range(0, 6):
_settings.openMsg_Text[x] = ord(mval[x])
rs.set_apply_callback(apply_openmsg)
disp.append(rs)
# 32 PAGER - a per-channel attribute
# 33 PAG.ABK
opts = ["Off", "On"]
ctcss.append(RadioSetting("pag_abk", "Paging Answer Back",
RadioSettingValueList(opts,
current_index=_settings
.pag_abk)))
# 34 PAG.CDR
opts = ["%2.2d" % (x) for x in range(1, 50)]
ctcss.append(RadioSetting("pag_cdr_1", "Receive Page Code 1",
RadioSettingValueList(opts,
current_index=_settings
.pag_cdr_1)))
ctcss.append(RadioSetting("pag_cdr_2", "Receive Page Code 2",
RadioSettingValueList(opts,
current_index=_settings
.pag_cdr_2)))
# 35 PAG.CDT
opts = ["%2.2d" % (x) for x in range(1, 50)]
ctcss.append(RadioSetting("pag_cdt_1", "Transmit Page Code 1",
RadioSettingValueList(opts,
current_index=_settings
.pag_cdt_1)))
ctcss.append(RadioSetting("pag_cdt_2", "Transmit Page Code 2",
RadioSettingValueList(opts,
current_index=_settings
.pag_cdt_2)))
# Common Button Options
button_opts = ["Squelch Off", "Weather", "Smart Search",
"Tone Scan", "Scan", "T Call", "ARTS"]
# 36 PRG P1
opts = button_opts + ["DC Volts"]
switch.append(RadioSetting(
"prog_p1", "P1 Button",
RadioSettingValueList(opts, current_index=_settings.prog_p1)))
# 37 PRG P2
opts = button_opts + ["Dimmer"]
switch.append(RadioSetting(
"prog_p2", "P2 Button",
RadioSettingValueList(opts, current_index=_settings.prog_p2)))
# 38 PRG P3
opts = button_opts + ["Mic Gain"]
switch.append(RadioSetting(
"prog_p3", "P3 Button",
RadioSettingValueList(opts, current_index=_settings.prog_p3)))
# 39 PRG P4
opts = button_opts + ["Skip"]
switch.append(RadioSetting(
"prog_p4", "P4 Button",
RadioSettingValueList(opts, current_index=_settings.prog_p4)))
# 40 PSWD
password = ""
for i in _passwd:
if i != 0xFF:
try:
password += CHARSET[i]
except IndexError:
LOG.debug("unknown char index in password: %x " % (i))
val = RadioSettingValueString(0, 4, password, True)
val.set_charset(CHARSET[0:15] + "abcdef ")
rs = RadioSetting("passwd", "Password", val)
def apply_password(s):
str = s.value.get_value().upper().rstrip()
mval = ""
mval = [chr(CHARSET.index(x)) for x in str]
for x in range(len(mval), 4):
mval.append(chr(0xff))
for x in range(0, 4):
_passwd[x] = ord(mval[x])
rs.set_apply_callback(apply_password)
misc.append(rs)
# 41 RESUME
opts = ["3 Sec", "5 Sec", "10 Sec", "Busy", "Hold"]
scan.append(
RadioSetting(
"resume", "Scan Resume Mode",
RadioSettingValueList(
opts, current_index=_settings.resume)))
# 42 RF.SQL
opts = ["Off"] + ["S-%d" % (x) for x in range(1, 10)]
misc.append(
RadioSetting(
"rf_sql", "RF Squelch Threshold",
RadioSettingValueList(
opts, current_index=_settings.rf_sql)))
# 43 RPT - per channel attribute, nothing to do here
# 44 RVRT
opts = ["Off", "On"]
misc.append(
RadioSetting(
"revert", "Priority Revert",
RadioSettingValueList(
opts, current_index=_settings.revert)))
# 45 S.SRCH
opts = ["Single", "Continuous"]
misc.append(
RadioSetting(
"s_search", "Smart Search Sweep Mode",
RadioSettingValueList(
opts, current_index=_settings.s_search)))
# 46 SHIFT - per channel setting, nothing to do here
# 47 SKIP = per channel setting, nothing to do here
# 48 SPLIT - per channel attribute, nothing to do here
# 49 SQL.TYP - per channel attribute, nothing to do here
# 50 STEP - per channel attribute, nothing to do here
# 51 TEMP - read-only status, nothing to do here
# 52 TN FRQ - per channel attribute, nothing to do here
# 53 TOT
opts = ["Off", "1 Min", "3 Min", "5 Min", "10 Min"]
misc.append(
RadioSetting(
"tot", "Timeout Timer",
RadioSettingValueList(
opts, current_index=_settings.tot)))
# 54 TS MUT
opts = ["Off", "On"]
ctcss.append(RadioSetting("ts_mut", "Tone Search Mute",
RadioSettingValueList(opts,
current_index=_settings
.ts_mut)))
# 55 TS SPEED
opts = ["Fast", "Slow"]
ctcss.append(RadioSetting("ts_speed", "Tone Search Scanner Speed",
RadioSettingValueList(opts,
current_index=_settings
.ts_speed)))
# 56 VFO.SCN
opts = ["+/- 1 MHz", "+/- 2 MHz", "+/-5 MHz", "All"]
scan.append(RadioSetting("vfo_scan", "VFO Scanner Width",
RadioSettingValueList(opts,
current_index=_settings
.vfo_scan)))
# 57 WX.ALT
opts = ["Off", "On"]
misc.append(
RadioSetting(
"wx_alert", "Weather Alert Scan",
RadioSettingValueList(
opts, current_index=_settings.wx_alert)))
# 58 WX.VOL
opts = ["Normal", "Maximum"]
misc.append(
RadioSetting(
"wx_vol_max", "Weather Alert Volume",
RadioSettingValueList(
opts, current_index=_settings.wx_vol_max)))
# 59 W/N DV - this is a per-channel attribute, nothing to do here
return setmode
def set_settings(self, uisettings):
_settings = self._memobj.settings
for element in uisettings:
if not isinstance(element, RadioSetting):
self.set_settings(element)
continue
if not element.changed():
continue
try:
name = element.get_name()
value = element.value
if element.has_apply_callback():
LOG.debug("Using apply callback")
element.run_apply_callback()
else:
obj = getattr(_settings, name)
setattr(_settings, name, value)
LOG.debug("Setting %s: %s" % (name, value))
except Exception:
LOG.debug(element.get_name())
raise
def get_bank_model(self):
return FT2900BankModel(self)
@classmethod
def match_model(cls, filedata, filename):
return len(filedata) == cls._memsize
@classmethod
def get_prompts(cls):
rp = chirp_common.RadioPrompts()
rp.pre_download = _(
"1. Turn Radio off.\n"
"2. Connect data cable.\n"
"3. While holding \"A/N LOW\" button, turn radio on.\n"
"4. After clicking OK , press \"SET MHz\" to send image.\n")
rp.pre_upload = _(
"1. Turn Radio off.\n"
"2. Connect data cable.\n"
"3. While holding \"A/N LOW\" button, turn radio on.\n"
"4. Press \"MW D/MR\" to receive image.\n"
"5. Make sure display says \"-WAIT-\" (see note below if not)\n"
"6. Click OK to dismiss this dialog and start transfer.\n"
"Note: if you don't see \"-WAIT-\" at step 5, try cycling\n"
" power and pressing and holding red \"*L\" button to"
" unlock\n"
" radio, then start back at step 1.\n")
return rp
# the FT2900E is the European version of the radio, almost identical
# to the R (USA) version, except for the model number and ID Block. We
# create and register a class for it, with only the needed overrides
# NOTE: Disabled until detection is fixed
# @directory.register
class FT2900ERadio(FT2900Radio):
"""Yaesu FT-2900E"""
MODEL = "FT-2900E/1900E"
VARIANT = "E"
IDBLOCK = b"\x56\x43\x32\x33\x00\x02\x41\x02\x01\x01"
# This class is for the TX Modified FT-1900/FT-2900 (MARS/CAP Mod).
# Enabling out of band TX changes the header received by CHIRP
@directory.register
class FT2900ModRadio(FT2900Radio):
"""Yaesu FT-2900Mod"""
MODEL = "FT-2900R/1900R(TXMod)"
VARIANT = "Opened Xmit"
IDBLOCK = b"\x56\x43\x32\x33\x00\x02\xc7\x01\x01\x01"
@classmethod
def match_model(cls, filedata, filename):
return False
chirp-20261030/chirp/drivers/ft2d.py 0000644 0001750 0001750 00000017604 15250742640 016043 0 ustar hibby hibby # Copyright 2010 Dan Smith
# Portions Copyright 2017 Wade Simmons
# Copyright 2017 Declan Rieb
#
# 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.drivers import ft1d
from chirp import chirp_common, directory
from chirp import errors
from chirp import memmap
from chirp.settings import RadioSetting
from chirp.settings import RadioSettingValueString
from chirp import util
# Differences from Yaesu FT1D
# Text in memory and memory bank structures is ASCII encoded
# Expanded modes
# Slightly different clone-mode instructions
LOG = logging.getLogger(__name__)
TMODES = ["", "Tone", "TSQL", "DTCS", "RTone", "JRfrq", "PRSQL", "Pager"]
class FT2Bank(chirp_common.NamedBank): # Like FT1D except for name in ASCII
def get_name(self):
_bank = self._model._radio._memobj.bank_info[self.index]
name = ""
for i in _bank.name:
if i == 0xff:
break
name += chr(i & 0xFF)
return name.rstrip()
def set_name(self, name):
_bank = self._model._radio._memobj.bank_info[self.index]
_bank.name = [ord(x) for x in name.ljust(16, chr(0xFF))[:16]]
class FT2BankModel(ft1d.FT1BankModel): # Just need this one to launch FT2Bank
"""A FT1D bank model"""
def __init__(self, radio, name='Banks'):
super(FT2BankModel, self).__init__(radio, name)
_banks = self._radio._memobj.bank_info
self._bank_mappings = []
for index, _bank in enumerate(_banks):
bank = FT2Bank(self, "%i" % index, "BANK-%i" % index)
bank.index = index
self._bank_mappings.append(bank)
@directory.register
class FT2D(ft1d.FT1Radio):
"""Yaesu FT-2D"""
BAUD_RATE = 38400
VENDOR = "Yaesu"
MODEL = "FT2D" # Yaesu doesn't use a hyphen in its documents
VARIANT = "R"
_model = b"AH60M" # Get this from chirp .img file after saving once
_has_vibrate = True
MAX_MEM_SLOTS = 900
_mem_params = {
"memnum": 900, # size of memories array
"flgnum": 900, # size of flags array
"dtmadd": 0x94A, # address of DTMF strings
}
_adms_ext = '.ft2d'
_APRS_HIGH_SPEED_MAX = 90
FORMATS = [directory.register_format('FT2D ADMS-8', '*.ft2d')]
@classmethod
def get_prompts(cls):
rp = chirp_common.RadioPrompts()
rp.pre_download = _(
"1. Turn radio off.\n"
"2. Connect cable to DATA terminal.\n"
"3. Press and hold [DISP] key while turning on radio\n"
" (\"CLONE\" will appear on radio LCD).\n"
"4. After clicking OK here in Chirp ,\n"
" press the [Send] screen button.\n")
rp.pre_upload = _(
"1. Turn radio off.\n"
"2. Connect cable to DATA terminal.\n"
"3. Press and hold in [DISP] key while turning on radio\n"
" (\"CLONE\" will appear on radio LCD).\n"
"4. Press [RECEIVE] screen button\n"
" (\"-WAIT-\" will appear on radio LCD).\n"
"5. Finally, press OK button below.\n")
return rp
def get_features(self): # AFAICT only TMODES & memory bounds are different
rf = super(FT2D, self).get_features()
rf.valid_tmodes = list(TMODES)
rf.memory_bounds = (1, self.MAX_MEM_SLOTS)
return rf
def get_bank_model(self): # here only to launch the bank model
return FT2BankModel(self)
def _decode_label(self, mem):
return str(mem.label).rstrip("\xFF")
def _encode_label(self, mem):
label = mem.name.rstrip().encode('ascii', 'ignore')
return self._add_ff_pad(label, 16)
def _decode_opening_message(self, opening_message):
msg = ""
for i in opening_message.message.padded_yaesu:
if i == 0xFF:
break
msg += chr(int(i))
val = RadioSettingValueString(0, 16, msg)
rs = RadioSetting("opening_message.message.padded_yaesu",
"Opening Message", val)
rs.set_apply_callback(self._apply_opening_message,
opening_message.message.padded_yaesu)
return rs
def _apply_opening_message(self, setting, obj):
data = self._add_ff_pad(setting.value.get_value().rstrip(), 16)
val = []
for i in data:
val.append(ord(i))
self._memobj.opening_message.message.padded_yaesu = val
@directory.register
class FT2Dv2(FT2D):
"""Yaesu FT-2D v2 firmware"""
VARIANT = "Rv2"
_model = b"AH60G"
@directory.register
class FT3D(FT2D):
"""Yaesu FT-3D"""
MODEL = "FT3D"
VARIANT = "R"
_adms_ext = '.ft3d'
_model = b"AH72M"
FORMATS = [directory.register_format('FT3D ADMS-11', '*.ft3d')]
def load_mmap(self, filename):
if filename.lower().endswith(self._adms_ext):
with open(filename, 'rb') as f:
self._adms_header = f.read(0x18C)
if b'ADMS11, Version=1.0.0.0' not in self._adms_header:
raise errors.ImageDetectFailed(
'Unsupported version found in ADMS file')
LOG.debug('ADMS Header:\n%s',
util.hexprint(self._adms_header))
self._mmap = memmap.MemoryMapBytes(f.read())
LOG.info('Loaded ADMS-11 file at offset 0x18C')
self.process_mmap()
else:
chirp_common.CloneModeRadio.load_mmap(self, filename)
def save_mmap(self, filename):
if filename.lower().endswith(self._adms_ext):
if not hasattr(self, '_adms_header'):
raise Exception(f'Unable to save .img to {self._adms_ext}')
with open(filename, 'wb') as f:
f.write(self._adms_header)
f.write(self._mmap.get_packed())
LOG.info('Wrote ADMS-11 file')
else:
chirp_common.CloneModeRadio.save_mmap(self, filename)
@directory.register
class FT5D(FT2D):
"""Yaesu FT-5D"""
MODEL = "FT5D"
VARIANT = "R"
_adms_ext = '.ft5d'
_model = b"AH82M"
FORMATS = [directory.register_format('FT5D ADMS-14', '*.ft5d')]
def load_mmap(self, filename):
if filename.lower().endswith(self._adms_ext):
with open(filename, 'rb') as f:
self._adms_header = f.read(0x18C)
if b'ADMS14, Version=1.0.' not in self._adms_header:
raise errors.ImageDetectFailed(
'Unsupported version found in ADMS file')
LOG.debug('ADMS Header:\n%s',
util.hexprint(self._adms_header))
self._mmap = memmap.MemoryMapBytes(f.read())
LOG.info('Loaded ADMS-14 file at offset 0x18C')
self.process_mmap()
else:
chirp_common.CloneModeRadio.load_mmap(self, filename)
def save_mmap(self, filename):
if filename.lower().endswith(self._adms_ext):
if not hasattr(self, '_adms_header'):
raise Exception(f'Unable to save .img to {self._adms_ext}')
with open(filename, 'wb') as f:
f.write(self._adms_header)
f.write(self._mmap.get_packed())
LOG.info('Wrote ADMS-14 file')
else:
chirp_common.CloneModeRadio.save_mmap(self, filename)
chirp-20261030/chirp/drivers/ft4.py 0000644 0001750 0001750 00000162142 15266622247 015706 0 ustar hibby hibby # Copyright 2019 Dan Clemmensen
# Derives loosely from two sources released under GPLv2:
# ./template.py, Copyright 2012 Dan Smith
# ./ft60.py, Copyright 2011 Dan Smith
# Edited 2020, 2023 Bernhard Hailer AE6YN
#
# 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 .
"""
CHIRP driver for Yaesu radios that use the SCU-35 cable. This includes at
least the FT-4X, FT-4V, FT-65, and FT-25. This driver will not work with older
Yaesu models.
"""
import logging
import struct
import copy
from chirp import chirp_common, directory, memmap, bitwise, errors, util
from chirp.settings import RadioSetting, RadioSettingGroup, \
RadioSettingValueList, RadioSettingValueString, RadioSettings
LOG = logging.getLogger(__name__)
COMM_DIRECTION_FROM = 0
COMM_DIRECTION_TO = 1
PROGRAM_CMD = b"PROGRAM"
# Layout of Radio memory image.
# This module and the serial protocol treat the FT-4 memory as 16-byte blocks.
# There in nothing magic about 16-byte blocks, but it simplifies the
# description. There are 17 groups of blocks, each with a different purpose
# and format. Five groups consist of channel memories, or "mems" in CHIRP.
# A "mem" describes a radio channel, and all "mems" have the same internal
# format. Three of the groups consist of bitmaps, which all have the same
# internal mapping. also groups for Name, misc, DTMF digits, and prog,
# plus some unused groups. The MEM_FORMAT describes the radio image memory.
# MEM_FORMAT is parsed in module ../bitwise.py. Syntax is similar but not
# identical to C data and structure definitions.
# Define the structures for each type of group here, but do not associate them
# with actual memory addresses yet
MEM_FORMAT = """
struct slot {
u8 tx_pwr; //0, 1, 2 == lo, medium, high
bbcd freq[4]; // Hz/10 but must end in 00
u8 tx_ctcss; //see ctcss table, but radio code = CHIRP code+1. 0==off
u8 rx_ctcss; //see ctcss table, but radio code = CHIRP code+1. 0==off
u8 tx_dcs; //see dcs table, but radio code = CHIRP code+1. 0==off
u8 rx_dcs; //see dcs table, but radio code = CHIRP code+1. 0==off
u8 unknown1:5,
duplex:3; //(auto,offset). (0,2,4,5)= (+,-,0, auto)
ul16 offset; //little-endian binary * scaler, +- per duplex
//scaler is 25 kHz for FT-4, 50 kHz for FT-65.
u8 unknown2:7,
tx_width:1; //0=wide, 1=narrow
u8 step; //STEPS (0-9)=(auto,5,6.25,10,12.5,15,20,25,50,100) kHz
u8 sql_type; //(0-6)==(off,r-tone,t-tone,tsql,rev tn,dcs,pager)
u8 unused;
};
// one bit per channel. 220 bits (200 mem+ 2*10 PMS) padded to fill
//exactly 2 blocks
struct bitmap {
u8 b8[28];
u8 unused[4];
};
//name struct occupies half a block (8 bytes)
//the code restricts the actual len to 6 for an FT-4
struct name {
u8 chrs[8]; //[0-9,A-z,a-z, -] padded with spaces
};
//txfreq struct occupies 4 bytes (1/4 slot)
struct txfreq {
bbcd freq[4];
};
//miscellaneous params. One 4-block group.
//"SMI": "Set Mode Index" of the FT-4 radio keypad function to set parameter.
//"SMI numbers on the FT-65 are different but the names in mem are the same.
struct misc {
u8 apo; //SMI 01. 0==off, (1-24) is the number of half-hours.
u8 arts_beep; //SMI 02. 0==off, 1==inrange, 2==always
u8 arts_intv; //SMI 03. 0==25 seconds, 1==15 seconds
u8 battsave; //SMI 32. 0==off, (1-5)==(200,300,500,1000,2000) ms
u8 bclo; //SMI 04. 0==off, 1==on
u8 beep; //SMI 05. (0-2)==(key+scan,key, off)
u8 bell; //SMI 06. (0-5)==(0,1,3,5,8,continuous) bells
u8 cw_id[6]; //SMI 08. callsign (A_Z,0-9) (pad with space if <6)
u8 unknown1[3];
// addr= 2010
u8 dtmf_mode; //SMI 12. 0==manual, 1==auto
u8 dtmf_delay; //SMI 11. (0-4)==(50,250,450,750,1000) ms
u8 dtmf_speed; //SMI 13. (0,1)=(50,100) ms
u8 edg_beep; //SMI 14. 0==off, 1==on
u8 key_lock; //SMI 18. (0-2)==(key,ptt,key+ptt)
u8 lamp; //SMI 15. (0-4)==(5,10,30,continuous,off) secKEY
u8 tx_led; //SMI 17. 0==off, 1==on
u8 bsy_led; //SMI 16. 0==off, 1==on
u8 moni_tcall; //SMI 19. (0-4)==(mon,1750,2100,1000,1450) tcall Hz.
u8 pri_rvt; //SMI 23. 0==off, 1==on
u8 scan_resume; //SMI 34. (0-2)==(busy,hold,time)
u8 rf_squelch; //SMI 28. 0==off, 8==full, (1-7)==(S1-S7)
u8 scan_lamp; //SMI 33 0==off,1==on
u8 unknown2;
u8 use_cwid; //SMI 7. 0==no, 1==yes
u8 compander; // compander on FT_65
// addr 2020
u8 unknown3;
u8 tx_save; //SMI 41. 0==off, 1==on (addr==2021)
u8 vfo_spl; //SMI 42. 0==off, 1==on
u8 vox; //SMI 43. 0==off, 1==on
u8 wfm_rcv; //SMI 44. 0==off, 1==on
u8 unknown4;
u8 wx_alert; //SMI 46. 0==off, 1==0n
u8 tot; //SMI 39. 0-off, (1-30)== (1-30) minutes
u8 pager_tx1; //SMI 21. (0-49)=(1-50) epcs code (i.e., value is code-1)
u8 pager_tx2; //SMI 21 same
u8 pager_rx1; //SMI 23 same
u8 pager_rx2; //SMI 23 same
u8 pager_ack; //SMI 22 same
u8 unknown5[3]; //possibly sql_setting and pgm_vfo_scan on FT-65?
// addr 2030
u8 use_passwd; //SMI 26 0==no, 1==yes
u8 passwd[4]; //SMI 27 ASCII (0-9)
u8 unused2[11]; // pad out to a block boundary
};
struct dtmfset {
u8 digit[16]; //ASCII (*,#,-,0-9,A-D). (dash-filled)
};
// area to be filled with 0xff, or just ignored
struct notused {
u8 unused[16];
};
// areas we are still analyzing
struct unknown {
u8 notknown[16];
};
struct progc {
u8 usage; //P key is (0-2) == unused, use P channel, use parm
u8 submode:2, //if parm!=0 submode 0-3 of mode
parm:6; //if usage == 2: if 0, use m-channel, else mode
};
"""
# Actual memory layout. 0x215 blocks, in 20 groups.
MEM_FORMAT += """
#seekto 0x0000;
struct unknown radiotype; //0000 probably a radio type ID but not sure
struct slot memory[200]; //0010 channel memory array
struct slot pms[20]; //0c90 10 PMS (L,U) slot pairs
struct slot vfo[5]; //0dd0 VFO (A UHF, A VHF, B FM, B UHF, B VHF)
struct slot home[3]; //0e20 Home (FM, VHF, UHF)
struct bitmap enable; //0e50
struct bitmap scan; //0e70
struct notused notused0; //0e90
struct bitmap bankmask[10]; //0ea0
struct notused notused1[2]; //0fe0
struct name names[220]; //1000 220 names in 110 blocks
struct notused notused2[2]; //16e0
struct txfreq txfreqs[220]; //1700 220 freqs in 55 blocks
struct notused notused3[89]; //1a20
struct misc settings; //2000 4-block collection of misc params
struct notused notused4[2]; //2040
struct dtmfset dtmf[9]; //2060 sets 1-9
struct notused notused5; //20f0
//struct progs progkeys; //2100 not a struct. bitwise.py refuses
struct progc progctl[4]; //2100 8 bytes. 1 2-byte struct per P key
u8 pmemndx[4]; //2108 4 bytes, 1 per P key
u8 notused6[4]; //210c fill out the block
struct slot prog[4]; //2110 P key "channel" array
//---------------- end of FT-4 mem?
"""
# The remaining mem is (apparently) not available on the FT4 but is
# reported to be available on the FT-65. Not implemented here yet.
# Possibly, memory-mapped control registers that allow for "live-mode"
# operation instead of "clone-mode" operation.
# 2150 27ff (unused?)
# 2800 285f 6 MRU operation?
# 2860 2fff (unused?)
# 3000 310f 17 (version info, etc?)
# ----------END of memory map
# Begin Serial transfer utilities for the SCU-35 cable.
# The serial transfer protocol was implemented here after snooping the wire.
# After it was implemented, we noticed that it is identical to the protocol
# implemented in th9000.py. A non-echo version is implemented in anytone_ht.py.
#
# The pipe.read and pipe.write functions use bytes, not strings. The serial
# transfer utilities operate only to move data between the memory object and
# the serial port. The code runs on either Python 2 or Python3, so some
# constructs could be better optimized for one or the other, but not both.
def get_mmap_data(radio):
"""
horrible kludge needed until we convert entirely to Python 3 OR we add a
slightly less horrible kludge to the Py2 or Py3 versions of memmap.py.
The minimal change have Py3 code return a bytestring instead of a string.
This is the only function in this module that must explicitly test for the
data string type. It is used only in the do_upload function.
returns the memobj data as a byte-like object.
"""
data = radio.get_mmap().get_packed()
if isinstance(data, bytes):
return data
return bytearray(radio.get_mmap()._data)
def checkSum8(data):
"""
Calculate the 8 bit checksum of buffer
Input: buffer - bytes
returns: integer
"""
return sum(x for x in bytearray(data)) & 0xFF
def variable_len_resp(pipe):
"""
when length of expected reply is not known, read byte at a time
until the ack character is found.
"""
response = b""
i = 0
toolong = 256 # arbitrary
while True:
b = pipe.read(1)
if not b:
raise errors.RadioError("No response from radio")
if b == b'\x06':
break
else:
response += b
i += 1
if i > toolong:
LOG.debug("Response too long. got" + util.hexprint(response))
raise errors.RadioError("Response from radio too long.")
return response
def sendcmd(pipe, cmd, response_len, retry=0):
"""
send a command bytelist to radio,receive and return the resulting bytelist.
Input: pipe - serial port object to use
cmd - bytes to send
response_len - number of bytes of expected response,
not including the ACK. (if None, read until ack)
This cable is "two-wire": The TxD and RxD are "or'ed" so we receive
whatever we send and then whatever response the radio sends. We check the
echo and strip it, returning only the radio's response.
We also check and strip the ACK character at the end of the response.
"""
pipe.write(cmd)
echo = pipe.read(len(cmd))
if not echo:
raise errors.RadioError("No echo from cable")
if echo != cmd:
msg = "Bad echo. Sent:" + util.hexprint(cmd) + ", "
msg += "Received:" + util.hexprint(echo)
LOG.debug(msg)
raise errors.RadioError(
"Incorrect echo on serial port. Radio off? Bad cable?")
if response_len is None:
return variable_len_resp(pipe)
if response_len > 0:
response = pipe.read(response_len)
if not response:
if cmd == PROGRAM_CMD:
raise errors.RadioNoResponse()
else:
response = b""
ack = pipe.read(1)
if ack != b'\x06':
if retry < 5:
LOG.debug("retry: " + str(retry))
retry += 1
return sendcmd(pipe, cmd, response_len, retry)
else:
LOG.debug("missing ack: expected 0x06, got" + util.hexprint(ack))
raise errors.RadioError("Incorrect ACK on serial port.")
return response
def enter_clonemode(radio):
"""
Send the PROGRAM command and check the response. Retry if
needed. After 3 tries, send an "END" and try some more if
it is acknowledged.
"""
for use_end in range(0, 3):
for i in range(0, 3):
try:
if b"QX" == sendcmd(radio.pipe, PROGRAM_CMD, 2):
return
except errors.RadioNoResponse:
raise
except Exception:
continue
sendcmd(radio.pipe, b"END", 0)
raise errors.RadioError("expected QX from radio.")
def startcomms(radio, direction):
"""
For either upload or download, put the radio into PROGRAM mode
and check the radio's ID. In this preliminary version of the driver,
the exact nature of the ID has been inferred from a single test case.
set up the progress bar
send "PROGRAM" to command the radio into clone mode
read the initial string (version?)
"""
progress_messages = {
COMM_DIRECTION_FROM: _("Cloning from radio"),
COMM_DIRECTION_TO: _("Cloning to radio")
}
progressbar = chirp_common.Status()
if direction in progress_messages:
progressbar.msg = progress_messages[direction]
else:
LOG.debug(
f"direction parameter value is not recognised: {direction}"
)
raise errors.InvalidValueError(
f"Unexpected parameter received: {direction}"
)
progressbar.max = radio.numblocks
enter_clonemode(radio)
id_response = sendcmd(radio.pipe, b'\x02', None)
# The last byte of the ID contains info about regional differences
radio.subtype = id_response[-1]
# Set last byte of the ID zero so that no ID warning is thrown
# Unfortunately, the returned object is an immutable bytes object,
# so we need to convert into a bytearray, modify, and convert back.
ba_id_response = bytearray(id_response)
ba_id_response[len(ba_id_response) - 1] = 0
id_response_mod = bytes(ba_id_response)
if id_response != radio.id_str:
substr0 = radio.id_str[:radio.id_str.find(b'\x00')]
if id_response[:id_response.find(b'\x00')] != substr0:
msg = "ID mismatch.\nExpected:\n{}Received:\n{}".format(
util.hexprint(radio.id_str), util.hexprint(id_response_mod)
)
LOG.warning(msg)
raise errors.RadioError("Incorrect ID read from radio.")
else:
msg = "ID suspect.\nExpected:\n{}Received:\n{}".format(
util.hexprint(radio.id_str), util.hexprint(id_response_mod)
)
LOG.warning(msg)
return progressbar
def getblock(pipe, addr, image):
"""
read a single 16-byte block from the radio.
send the command and check the response
places the response into the correct offset in the supplied bytearray
returns True if successful, False if error.
"""
cmd = struct.pack(">cHb", b"R", addr, 16)
response = sendcmd(pipe, cmd, 21)
if (response[0] != b"W"[0]) or (response[1:4] != cmd[1:4]):
msg = "Bad response. Sent:\n%s" % util.hexprint(cmd)
msg += "\nReceived:\n%s" % util.hexprint(response)
LOG.debug(msg)
return False
if checkSum8(response[1:20]) != bytearray(response)[20]:
LOG.debug("Bad checksum:\n%s" % util.hexprint(response))
LOG.debug('%r != %r' % (checkSum8(response[1:20]),
bytearray(response)[20]))
return False
image[addr:addr+16] = response[4:20]
return True
def do_download(radio):
"""
Read memory from the radio.
call startcomms to go into program mode and check version
create an mmap
read the memory blocks and place the data into the mmap
send "END"
"""
image = bytearray(radio.get_memsize())
pipe = radio.pipe # Get the serial port connection
progressbar = startcomms(radio, COMM_DIRECTION_FROM)
for blocknum in range(radio.numblocks):
for i in range(0, 3):
if getblock(pipe, 16 * blocknum, image):
break
if i == 2:
raise errors.RadioError(
"read block from radio failed 3 times")
progressbar.cur = blocknum
radio.status_fn(progressbar)
sendcmd(pipe, b"END", 0)
return memmap.MemoryMapBytes(bytes(image))
def putblock(pipe, addr, data):
"""
write a single 16-byte block to the radio
send the command and check the response
"""
chkstr = struct.pack(">Hb", addr, 16) + data
msg = b'W' + chkstr + struct.pack('B', checkSum8(chkstr)) + b'\x06'
sendcmd(pipe, msg, 0)
def do_upload(radio):
"""
Write memory image to radio
call startcomms to go into program mode and check version
write the memory blocks. Skip the first block
send "END"
"""
pipe = radio.pipe # Get the serial port connection
progressbar = startcomms(radio, COMM_DIRECTION_TO)
data = get_mmap_data(radio)
for _i in range(1, radio.numblocks):
putblock(pipe, 16*_i, data[16*_i:16*(_i+1)])
progressbar.cur = _i
radio.status_fn(progressbar)
sendcmd(pipe, b"END", 0)
return
# End serial transfer utilities
def bit_loc(bitnum):
"""
return the ndx and mask for a bit location
"""
return (bitnum // 8, 1 << (bitnum & 7))
def store_bit(bankmem, bitnum, val):
"""
store a bit in a bankmem. Store 0 or 1 for False or True
"""
ndx, mask = bit_loc(bitnum)
if val:
bankmem.b8[ndx] |= mask
else:
bankmem.b8[ndx] &= ~mask
return
def retrieve_bit(bankmem, bitnum):
"""
return True or False for a bit in a bankmem
"""
ndx, mask = bit_loc(bitnum)
return (bankmem.b8[ndx] & mask) != 0
# A bank is a bitmap of 220 bits. 200 mem slots and 2*10 PMS slots.
# There are 10 banks.
class YaesuSC35GenericBankModel(chirp_common.BankModel):
def get_num_mappings(self):
return 10
def get_mappings(self):
banks = []
for i in range(1, 1 + self.get_num_mappings()):
bank = chirp_common.Bank(self, "%i" % i, "Bank %i" % i)
bank.index = i - 1
banks.append(bank)
return banks
def add_memory_to_mapping(self, memory, bank):
bankmem = self._radio._memobj.bankmask[bank.index]
store_bit(bankmem, memory.number-1, True)
def remove_memory_from_mapping(self, memory, bank):
bankmem = self._radio._memobj.bankmask[bank.index]
if not retrieve_bit(bankmem, memory.number-1):
raise Exception("Memory %i is not in bank %s." %
(memory.number, bank))
store_bit(bankmem, memory.number-1, False)
# return a list of slots in a bank
def get_mapping_memories(self, bank):
memories = []
for i in range(*self._radio.get_features().memory_bounds):
if retrieve_bit(self._radio._memobj.bankmask[bank.index], i - 1):
memories.append(self._radio.get_memory(i))
return memories
# return a list of banks a slot is a member of
def get_memory_mappings(self, memory):
memndx = memory.number - 1
banks = []
for bank in self.get_mappings():
if retrieve_bit(self._radio._memobj.bankmask[bank.index], memndx):
banks.append(bank)
return banks
# the values in these lists must also be in the canonical UI list
# we can re-arrange the order, and we don't need to have all
# the values, but we cannot add our own values here.
DUPLEX = ["+", "", "-", "", "off", "", "split"] # (0,2,4,5) = (+, -, 0, auto)
# The radio implements duplex "auto" as 5. We map to "". It is a convenience
# function in the radio that affects the offset, which sets the duplex value
# according to the frequency in use. Yaesu doesn't entirely adhere to the band
# plans; but worse, they save the value 'auto' instead of + or -. Why Yaesu
# is doing such a thing is beyond me. [AE6YN]
DUPLEX_AUTO_US = [
[145100000, 145495000, 2],
[146000000, 146395000, 0],
[146600000, 146995000, 2],
[147000000, 147395000, 0],
[147600000, 147995000, 2]]
# (There are no automatic duplex values in IARU-2 70CM.)
DUPLEX_AUTO_EU = [
[145600000, 145800000, 2],
[438200000, 439425000, 2]]
SKIPS = ["S", ""]
POWER_LEVELS = [
chirp_common.PowerLevel("High", watts=5.0), # high must be first (0)
chirp_common.PowerLevel("Mid", watts=2.5),
chirp_common.PowerLevel("Low", watts=0.5)]
# these steps encode to 0-9 on all radios, but encoding #2 is disallowed
# on the US versions (FT-4XR)
STEP_CODE = [0, 5.0, 6.25, 10.0, 12.5, 15.0, 20.0, 25.0, 50.0, 100.0]
VALID_STEPS = [x for x in STEP_CODE if x]
US_LEGAL_STEPS = list(VALID_STEPS) # copy to pass to UI on US radios
US_LEGAL_STEPS.remove(6.25) # euro radios just use STEP_CODE
def _valid_tuning_step(radio, mem):
"""Return an explicit legal step compatible with the memory frequency."""
if mem.tuning_step in radio.legal_steps:
return mem.tuning_step
try:
return chirp_common.required_step(mem.freq, radio.legal_steps)
except errors.InvalidDataError:
# Preserve an otherwise-invalid frequency without exposing Auto;
# memory validation reports that the frequency is unsupported.
return radio.legal_steps[0]
# Map the radio image sql_type (0-6) to the CHIRP mem values.
# Yaesu "TSQL" and "DCS" each map to different CHIRP values depending on the
# radio values of the tx and rx tone codes. The table is a list of rows, one
# per Yaesu sql_type (0-5). The code does not use this table when the sql_type
# is 6 (PAGER). Each row is a tuple. Its first member is a list of
# [tmode,cross] or [tmode, cross, suppress]. "Suppress" is used only when
# encoding UI-->radio. When decoding radio-->UI, two of the sql_types each
# result in 5 possible UI decodings depending on the tx and rx codes, and the
# list in each of these rows has five members. These two row tuples each have
# two additional members to specify which of the radio fields to examine.
# The map from CHIRP UI to radio image types is also built from this table.
RADIO_TMODES = [
([["", None], ], ), # sql_type= 0. off
([["Cross", "->Tone"], ], ), # sql_type= 1. R-TONE
([["Tone", None], ], ), # sql_type= 2. T-TONE
([ # sql_type= 3. TSQL:
["", None], # tx==0, rx==0 : invalid
["TSQL", None], # tx==0
["Tone", None], # rx==0
["Cross", "Tone->Tone"], # tx!=rx
["TSQL", None] # tx==rx
], "tx_ctcss", "rx_ctcss"), # tx and rx fields to check
([["TSQL-R", None], ], ), # sql_type= 4. REV TN
([ # sql_type= 5.DCS:
["", None], # tx==0, rx==0 : invalid
["Cross", "->DTCS", "tx_dcs"], # tx==0. suppress tx
["Cross", "DTCS->", "rx_dcs"], # rx==0. suppress rx
["Cross", "DTCS->DTCS"], # tx!=rx
["DTCS", None] # tx==rx
], "tx_dcs", "rx_dcs"), # tx and rx fields to check
# # sql_type= 6. PAGER is a CHIRP "extra"
]
# Find all legal values for the tmode and cross fields for the UI.
# We build two dictionaries to do the lookups when encoding.
# The reversed range is a kludge: by happenstance, earlier duplicates
# in the above table are the preferred mapping, they override the
# later ones when we process the table backwards.
TONE_DICT = {} # encode sql_type.
CROSS_DICT = {} # encode sql_type.
for sql_type in reversed(range(0, len(RADIO_TMODES))):
sql_type_row: list = RADIO_TMODES[sql_type]
for decode_row in sql_type_row[0]:
suppress = None
if len(decode_row) == 3:
suppress = decode_row[2]
TONE_DICT[decode_row[0]] = (sql_type, suppress)
if decode_row[1]:
CROSS_DICT[decode_row[1]] = (sql_type, suppress)
# The keys are added to the "VALID" lists using code that puts those lists
# in the same order as the UI's default order instead of the random dict
# order or the arbitrary build order.
VALID_TONE_MODES = [] # list for UI.
VALID_CROSS_MODES = [] # list for UI.
for name in chirp_common.TONE_MODES:
if name in TONE_DICT:
VALID_TONE_MODES += [name]
for name in chirp_common.CROSS_MODES:
if name in CROSS_DICT:
VALID_CROSS_MODES += [name]
DTMF_CHARS = "0123456789ABCD*#- "
CW_ID_CHARS = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ "
PASSWD_CHARS = "0123456789"
CHARSET = CW_ID_CHARS + "abcdefghijklmnopqrstuvwxyz*+-/@"
PMSNAMES = ["%s%02d" % (c, i) for i in range(1, 11) for c in ('L', 'U')]
# Four separate arrays of special channel mems.
# Each special has unique constraints: band, name yes/no, and pms L/U
# The FT-65 class replaces the "prog" entry in this list.
# The name field must be the name of a slot array in MEM_FORMAT
SPECIALS_FT4 = [
("pms", PMSNAMES),
("vfo", ["VFO A UHF", "VFO A VHF", "VFO B FM", "VFO B VHF", "VFO B UHF"]),
("home", ["HOME FM", "HOME VHF", "HOME UHF"]),
("prog", ["P1", "P2"])
]
SPECIALS_FT65 = SPECIALS_FT4
FT65_PROGS = ("prog", ["P1", "P2", "P3", "P4"])
SPECIALS_FT65[-1] = FT65_PROGS # replace the last entry (P key names)
VALID_BANDS_DUAL = [
(65000000, 108000000), # broadcast FM, receive only
(136000000, 174000000), # VHF
(400000000, 480000000) # UHF
]
VALID_BANDS_VHF = [
(65000000, 108000000), # broadcast FM, receive only
(136000000, 174000000), # VHF
]
# bands for the five VFO and three home channel memories
BAND_ASSIGNMENTS_DUALBAND = [2, 1, 0, 1, 2, 0, 1, 2] # all locations used
BAND_ASSIGNMENTS_MONO_VHF = [1, 1, 0, 1, 1, 0, 1, 1] # UHF locations unused
# None, and 50 Tones. Use this explicit array because the
# one in chirp_common could change and no longer describe our radio
TONE_MAP = [
None, 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
]
# None, and 104 DTCS Codes. Use this explicit array because the
# one in chirp_common could change and no longer describe our radio
DTCS_MAP = [
None, 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
]
# The legal PAGER codes are the same as the CTCSS codes, but we
# pass them to the UI as a list of strings. The radio stores the
# code minus one (0-49 for codes 1-50), so the list starts with the
# first CTCSS tone.
EPCS_CODES = [format(flt) for flt in TONE_MAP[1:]]
# allow a child class to add a param to its class
# description list. used when a specific radio class has
# a param that is not in the generic list.
def add_paramdesc(desc_list, group, param):
for description in desc_list:
groupname, title, parms = description
if group == groupname:
description[2].append(param)
return
class YaesuSC35GenericRadio(chirp_common.CloneModeRadio,
chirp_common.ExperimentalRadio):
"""
Base class for all Yaesu radios using the SCU-35 programming cable
and its protocol. Classes for sub families extend this class and
are found towards the end of this file.
"""
VENDOR = "Yaesu"
MODEL = "SCU-35Generic" # No radio directly uses the base class
BAUD_RATE = 9600
MAX_MEM_SLOT = 200
DUPLEX_OFF_VIA_OFFSET = False
# These settings are common to all radios in this family.
_valid_chars = chirp_common.CHARSET_ASCII
numblocks = 0x215 # number of 16-byte blocks in the radio
_memsize = 16 * numblocks # used by CHIRP file loader to guess radio type
MAX_MEM_SLOT = 200
# ID string last byte can contain extra info. It is usually 0;
# however, on FT25R/65R sold in Asia we have seen that it can be 3,
# which affects the scaler: instead of the default 50000 it's then 25000.
@property
def subtype(self):
return self.metadata.get('ft4_subtype', 0)
@subtype.setter
def subtype(self, value):
self.metadata = {'ft4_subtype': int(value)}
@classmethod
def get_prompts(cls):
rp = chirp_common.RadioPrompts()
rp.experimental = _(
"Tested and mostly works, but may give you issues\n"
"when using lesser common radio variants.\n"
"Proceed at your own risk, and let us know about issues!"
)
rp.pre_download = _(
"1. Connect programming cable to MIC jack.\n"
"2. Press OK."
)
rp.pre_upload = rp.pre_download
return rp
# identify the features that can be manipulated on this radio.
# mentioned here only when different from defaults in chirp_common.py
def get_features(self):
rf = chirp_common.RadioFeatures()
specials = [name for s in self.class_specials for name in s[1]]
rf.valid_special_chans = specials
rf.memory_bounds = (1, self.MAX_MEM_SLOT)
rf.valid_duplexes = DUPLEX
rf.valid_tmodes = VALID_TONE_MODES
rf.valid_cross_modes = VALID_CROSS_MODES
rf.valid_power_levels = POWER_LEVELS
rf.valid_tuning_steps = self.legal_steps
rf.valid_skips = SKIPS
rf.valid_characters = CHARSET
rf.valid_name_length = self.namelen
rf.valid_modes = ["FM", "NFM"]
rf.valid_bands = self.valid_bands
rf.can_odd_split = True
rf.has_ctone = True
rf.has_rx_dtcs = True
rf.has_dtcs_polarity = False # REV TN reverses the tone, not the dcs
rf.has_cross = True
rf.has_settings = True
return rf
def get_bank_model(self):
return YaesuSC35GenericBankModel(self)
# read and parse the radio memory
def sync_in(self):
try:
self._mmap = do_download(self)
except errors.RadioError:
raise
except Exception as e:
raise errors.RadioError("Failed to communicate with radio: %s" % e)
self.process_mmap()
# write the memory image to the radio
def sync_out(self):
try:
do_upload(self)
except errors.RadioError:
raise
except Exception as e:
raise errors.RadioError("Failed to communicate with radio: %s" % e)
def process_mmap(self):
self._memobj = bitwise.parse(MEM_FORMAT, self._mmap)
# There are about 40 settings and most are handled generically in
# get_settings. get_settings invokes these handlers for the few
# that are more complicated.
# callback for setting byte arrays: DTMF[0-9], passwd, and CW_ID
def apply_str_to_bytearray(self, element, obj):
lng = len(obj)
string = (element.value.get_value() + " ")[:lng]
bytes = bytearray(string, "ascii")
for x in range(0, lng): # memobj cannot iterate, so byte-by-byte
obj[x] = bytes[x]
return
# add a string value to the RadioSettings
def get_string_setting(self, obj, valid_chars, desc1, desc2, group):
content = ''
maxlen = len(obj)
for x in range(0, maxlen):
content += chr(obj[x])
val = RadioSettingValueString(0, maxlen, content, True, valid_chars)
rs = RadioSetting(desc1, desc2, val)
rs.set_apply_callback(self.apply_str_to_bytearray, obj)
group.append(rs)
# called when found in the group_descriptions table to handle string value
def get_strset(self, group, parm):
# parm =(paramname, paramtitle,(handler,[handler params])).
objname, title, fparms = parm
myparms = fparms[1]
obj = getattr(self._memobj.settings, objname)
self.get_string_setting(obj, myparms[0], objname, title, group)
# called when found in the group_descriptions table for DTMF strings
def get_dtmfs(self, group, parm):
objname, title, fparms = parm
for i in range(1, 10):
dtmf_digits = self._memobj.dtmf[i - 1].digit
self.get_string_setting(
dtmf_digits, DTMF_CHARS,
"dtmf_%i" % i, "DTMF Autodialer Memory %i" % i, group)
def apply_P(self, element, pnum, memobj):
memobj.progctl[pnum].usage = element.value
def apply_Pmode(self, element, pnum, memobj):
memobj.progctl[pnum].parm = element.value
def apply_Psubmode(self, element, pnum, memobj):
memobj.progctl[pnum].submode = element.value
def apply_Pmem(self, element, pnum, memobj):
memobj.pmemndx[pnum] = element.value
MEMLIST = ["%d" % i for i in range(1, MAX_MEM_SLOT + 1)] + PMSNAMES
USAGE_LIST = ["unused", "P channel", "mode or M channel"]
# called when found in the group_descriptions table
# returns the settings for the programmable keys (P1-P4)
def get_progs(self, group, parm):
_progctl = self._memobj.progctl
_progndx = self._memobj.pmemndx
def get_prog(i, val_list, valndx, sname, longname, f_apply):
k = str(i + 1)
val = val_list[valndx]
valuelist = RadioSettingValueList(val_list, val)
rs = RadioSetting(sname + k, longname + k, valuelist)
rs.set_apply_callback(f_apply, i, self._memobj)
group.append(rs)
for i in range(0, self.Pkeys):
get_prog(i, self.USAGE_LIST, _progctl[i].usage,
"P", "Programmable key ", self.apply_P)
get_prog(i, self.SETMODES, _progctl[i].parm, "modeP",
"mode for Programmable key ", self.apply_Pmode)
get_prog(i, ["0", "1", "2", "3"], _progctl[i].submode, "submodeP",
"submode for Programmable key ", self.apply_Psubmode)
get_prog(i, self.MEMLIST, _progndx[i], "memP",
"mem for Programmable key ", self.apply_Pmem)
# ------------ End of special settings handlers.
# list of group description tuples: [groupame,group title, [param list]].
# A param is a tuple:
# for a simple param: (paramname, paramtitle,[valuename list])
# for a handler param: (paramname, paramtitle,( handler,[handler params]))
# This is a class variable. subclasses must create a variable named
# class_group_descs. The FT-4 classes simply equate this, but the
# FT-65 classes must copy and modify this.
group_descriptions = [
("misc", "Miscellaneous Settings", [ # misc
("apo", "Automatic Power Off",
["OFF"] + ["%0.1f" % (x * 0.5) for x in range(1, 24 + 1)]),
("bclo", "Busy Channel Lock-Out", ["OFF", "ON"]),
("beep", "Enable the Beeper", ["KEY+SC", "KEY", "OFF"]),
("bsy_led", "Busy LED", ["OFF", "ON"]),
("edg_beep", "Band Edge Beeper", ["OFF", "ON"]),
("vox", "VOX", ["OFF", "ON"]),
("rf_squelch", "RF Squelch Threshold",
["OFF", "S-1", "S-2", "S-3", "S-4", "S-5", "S-6", "S-7", "S-FULL"]),
("tot", "Timeout Timer",
["OFF"] + ["%dMIN" % (x) for x in range(1, 30 + 1)]),
("tx_led", "TX LED", ["OFF", "ON"]),
("use_cwid", "use CW ID", ["NO", "YES"]),
("cw_id", "CW ID Callsign", (get_strset, [CW_ID_CHARS])), # handler
("vfo_spl", "VFO Split", ["OFF", "ON"]),
("wfm_rcv", "Enable Broadband FM", ["OFF", "ON"]),
("passwd", "Password", (get_strset, [PASSWD_CHARS])) # handler
]),
("arts", "ARTS Settings", [ # arts
("arts_beep", "ARTS BEEP", ["OFF", "INRANG", "ALWAYS"]),
("arts_intv", "ARTS Polling Interval", ["25 SEC", "15 SEC"])
]),
("ctcss", "CTCSS/DCS/DTMF Settings", [ # ctcss
("bell", "Bell Repetitions", ["OFF", "1T", "3T", "5T", "8T", "CONT"]),
("dtmf_mode", "DTMF Mode", ["Manual", "Auto"]),
("dtmf_delay", "DTMF Autodialer Delay Time",
["50 ms", "100 ms", "250 ms", "450 ms", "750 ms", "1000 ms"]),
("dtmf_speed", "DTMF Autodialer Sending Speed", ["50 ms", "100 ms"]),
("dtmf", "DTMF Autodialer Memory ", (get_dtmfs, [])) # handler
]),
("switch", "Switch/Knob Settings", [ # switch
("lamp", "Lamp Mode", ["5SEC", "10SEC", "30SEC", "KEY", "OFF"]),
("moni_tcall", "MONI Switch Function",
["MONI", "1750", "2100", "1000", "1450"]),
("key_lock", "Lock Function", ["KEY", "PTT", "KEY+PTT"]),
("Pkeys", "Pkey fields", (get_progs, []))
]),
("scan", "Scan Settings", [ # scan
("scan_resume", "Scan Resume Mode", ["BUSY", "HOLD", "TIME"]),
("pri_rvt", "Priority Revert", ["OFF", "ON"]),
("scan_lamp", "Scan Lamp", ["OFF", "ON"]),
("wx_alert", "Weather Alert Scan", ["OFF", "ON"])
]),
("power", "Power Saver Settings", [ # power
("battsave", "Receive Mode Battery Save Interval",
["OFF", "200 ms", "300 ms", "500 ms", "1 s", "2 s"]),
("tx_save", "Transmitter Battery Saver", ["OFF", "ON"])
]),
("eai", "EAI/EPCS Settings", [ # eai
("pager_tx1", "TX pager frequency 1", EPCS_CODES),
("pager_tx2", "TX pager frequency 2", EPCS_CODES),
("pager_rx1", "RX pager frequency 1", EPCS_CODES),
("pager_rx2", "RX pager frequency 2", EPCS_CODES),
("pager_ack", "Pager answerback", ["NO", "YES"])
])
]
# ----------------end of group_descriptions
# returns the current values of all the settings in the radio memory image,
# in the form of a RadioSettings list. Uses the class_group_descs
# list to create the groups and params. Valuelist scalars are handled
# inline. More complex params are built by calling the special handlers.
def get_settings(self):
_settings = self._memobj.settings
groups = RadioSettings()
for description in self.class_group_descs:
groupname, title, parms = description
group = RadioSettingGroup(groupname, title)
groups.append(group)
for parm in parms:
param, title, opts = parm
try:
if isinstance(opts, list):
# setting is a single value from the list
objval = getattr(_settings, param)
value = opts[objval]
valuelist = RadioSettingValueList(opts, value)
group.append(RadioSetting(param, title, valuelist))
else:
# setting needs special handling. opts[0] is a
# function name
opts[0](self, group, parm)
except Exception as e:
LOG.debug(
"%s: cannot set %s to %s" % (e, param, repr(objval))
)
return groups
# end of get_settings
# modify settings values in the radio memory image
def set_settings(self, uisettings):
_settings = self._memobj.settings
for element in uisettings:
if not isinstance(element, RadioSetting):
self.set_settings(element)
continue
if not element.changed():
continue
try:
name = element.get_name()
value = element.value
if element.has_apply_callback():
LOG.debug("Using apply callback")
element.run_apply_callback()
else:
setattr(_settings, name, value)
LOG.debug("Setting %s: %s" % (name, value))
except Exception:
LOG.debug(element.get_name())
raise
# maps a boolean pair (tx==0,rx==0) to the numbers 0-3
LOOKUP = [[True, True], [True, False], [False, True], [False, False]]
def decode_sql(self, mem, chan):
"""
examine the radio channel fields and determine the correct
CHIRP CSV values for tmode, cross_mode, and sql_override
"""
mem.extra = RadioSettingGroup("Extra", "extra")
extra_modes = ["(None)", "PAGER"]
value = extra_modes[chan.sql_type == 6]
valuelist = RadioSettingValueList(extra_modes, value)
rs = RadioSetting("sql_override", "Squelch override", valuelist)
mem.extra.append(rs)
if chan.sql_type == 6:
return
sql_map = RADIO_TMODES[chan.sql_type]
ndx = 0
if len(sql_map[0]) > 1:
# the sql_type is TSQL or DCS, so there are multiple UI mappings
x = getattr(chan, sql_map[1])
r = getattr(chan, sql_map[2])
ndx = self.LOOKUP.index([x == 0, r == 0])
if ndx == 3 and x == r:
ndx = 4
mem.tmode = sql_map[0][ndx][0]
cross = sql_map[0][ndx][1]
if cross:
mem.cross_mode = cross
if chan.rx_ctcss:
mem.ctone = TONE_MAP[chan.rx_ctcss]
if chan.tx_ctcss:
mem.rtone = TONE_MAP[chan.tx_ctcss]
if chan.tx_dcs:
mem.dtcs = DTCS_MAP[chan.tx_dcs]
if chan.rx_dcs:
mem.rx_dtcs = DTCS_MAP[chan.rx_dcs]
def encode_sql(self, mem, chan):
"""
examine CHIRP's mem.tmode and mem.cross_mode and set the values
for the radio sql_type, dcs codes, and ctcss codes. We set all four
codes, and then zero out a code if needed when Tone or DCS is one-way
"""
chan.tx_ctcss = TONE_MAP.index(mem.rtone)
chan.tx_dcs = DTCS_MAP.index(mem.dtcs)
chan.rx_ctcss = TONE_MAP.index(mem.ctone)
chan.rx_dcs = DTCS_MAP.index(mem.rx_dtcs)
if mem.tmode == "TSQL":
chan.tx_ctcss = chan.rx_ctcss # CHIRP uses ctone for TSQL
if mem.tmode == "DTCS":
chan.rx_dcs = chan.tx_dcs # CHIRP uses rx_dtcs for DTCS
# select the correct internal dictionary and key
mode_dict, key = [
(TONE_DICT, mem.tmode),
(CROSS_DICT, mem.cross_mode)
][mem.tmode == "Cross"]
# now look up that key in that dictionary.
chan.sql_type, suppress = mode_dict[key]
if suppress:
setattr(chan, suppress, 0)
for setting in mem.extra:
if (setting.get_name() == 'sql_override'):
value = str(setting.value)
if value == "PAGER":
chan.sql_type = 6
return
# given a CHIRP memory ref, get the radio memobj for it.
# A memref is either a number or the name of a special
# CHIRP will sometimes use numbers (>MAX_SLOTS) for specials
# returns the obj and several attributes
def slotloc(self, memref):
array = None
num = memref
sname = memref
if isinstance(memref, str): # named special?
num = self.MAX_MEM_SLOT + 1
for x in self.class_specials:
try:
ndx = x[1].index(memref)
array = x[0]
break
except Exception:
num += len(x[1])
if array is None:
LOG.debug("unknown Special %s", memref)
raise
num += ndx
elif memref > self.MAX_MEM_SLOT: # numbered special?
ndx = memref - (self.MAX_MEM_SLOT + 1)
for x in self.class_specials:
if ndx < len(x[1]):
array = x[0]
sname = x[1][ndx]
break
ndx -= len(x[1])
if array is None:
LOG.debug("memref number %d out of range", memref)
raise
else: # regular memory slot
array = "memory"
ndx = memref - 1
memloc = getattr(self._memobj, array)[ndx]
return (memloc, ndx, num, array, sname)
# end of slotloc
# return the raw info for a memory channel
def get_raw_memory(self, memref):
memloc, ndx, num, regtype, sname = self.slotloc(memref)
if regtype == "memory":
return repr(memloc)
else:
return repr(memloc) + repr(self._memobj.names[ndx])
# return the info for a memory channel In CHIRP canonical form
def get_memory(self, memref):
def clean_name(obj): # helper func to tidy up the name
name = ''
for x in range(0, self.namelen):
y = obj[x]
if y == 0:
break
if y == 0x7F: # when programmed from VFO
y = 0x20
name += chr(y)
return name.rstrip()
def get_duplex(freq): # auto duplex to real duplex
"""
Select the duplex direction if duplex == 'auto'.
0 is +, 2 is -, and 4 is none.
"""
return_value = 4 # off, if not in auto range
for x in self.DUPLEX_AUTO:
if freq in range(x[0], x[1]):
return_value = x[2]
return return_value
mem = chirp_common.Memory()
_mem, ndx, num, regtype, sname = self.slotloc(memref)
mem.number = num
freq_offset_factor = self.freq_offset_factor
# FT-25R/65R Asia version (US is 0)?
if self.subtype == 3 and freq_offset_factor == 2:
freq_offset_factor = 1 # 25000 scaler
# First, we need to know whether a channel is enabled,
# then we can process any channel parameters.
# It was found (at least on an FT-25) that channels might be
# uninitialized and memory is just completely filled with 0xFF.
if regtype == "pms":
mem.extd_number = sname
if regtype in ["memory", "pms"]:
ndx = num - 1
mem.name = clean_name(self._memobj.names[ndx].chrs)
mem.empty = not retrieve_bit(self._memobj.enable, ndx)
mem.skip = SKIPS[retrieve_bit(self._memobj.scan, ndx)]
else:
# A programmable key's channel slot is only written when the
# key is set to use one; otherwise it stays erased (all 0xFF).
mem.empty = (regtype == "prog" and
_mem.get_raw() == b"\xFF" * len(_mem.get_raw()))
mem.extd_number = sname
mem.immutable = ["number", "extd_number", "name", "skip"]
# So, now if channel is not empty, we can do the evaluation of
# all parameters. Otherwise we set them to defaults.
if mem.empty:
mem.freq = 0
mem.offset = 0
mem.duplex = "off"
mem.power = POWER_LEVELS[0] # "High"
mem.mode = "FM"
mem.tuning_step = 0
else:
mem.freq = int(_mem.freq) * 10
txfreq = int(self._memobj.txfreqs[ndx].freq) * 10
if _mem.duplex == 5: # auto offset
mem.duplex = DUPLEX[get_duplex(mem.freq)]
else:
mem.duplex = DUPLEX[_mem.duplex]
if _mem.duplex == 6: # split: offset is tx frequency
mem.offset = txfreq
else:
mem.offset = int(_mem.offset) * 25000 * freq_offset_factor
if self.DUPLEX_OFF_VIA_OFFSET and mem.duplex in ['+', '-']:
band = self.band_for_freq(mem.freq)
if band is not None and self.DUPLEX_OFF_VIA_OFFSET[band]:
sign = 1 if mem.duplex == "+" else -1
tx = mem.freq + (sign * mem.offset)
if self.band_for_freq(tx) is None:
mem.duplex = "off"
mem.offset = 0
self.decode_sql(mem, _mem)
mem.power = POWER_LEVELS[2 - _mem.tx_pwr]
mem.mode = ["FM", "NFM"][_mem.tx_width]
mem.tuning_step = STEP_CODE[_mem.step]
return mem
def band_for_freq(self, freq):
"""
We need to find the valid_bands list index a frequency falls
within, if any, to help determine if DUPLEX_OFF_VIA_OFFSET
logic is required
"""
band_index = None
for i, limits in enumerate(self.valid_bands):
lo, hi = limits
if lo <= freq <= hi:
band_index = i
break
return band_index
def enforce_band(self, memloc, freq, mem_num, sname):
"""
vfo and home channels are each restricted to a particular band.
If the frequency is not in the band, use the lower bound
Raise an exception to cause UI to pop up an error message
"""
first_vfo_num = self.MAX_MEM_SLOT + len(PMSNAMES) + 1
band = self.BAND_ASSIGNMENTS[mem_num - first_vfo_num]
frange = self.valid_bands[band]
if freq >= frange[0] and freq <= frange[1]:
memloc.freq = freq / 10
return freq
memloc.freq = frange[0] / 10
raise Exception("freq out of range for %s" % sname)
# modify a radio channel in memobj based on info in CHIRP canonical form
def set_memory(self, mem):
_mem, ndx, num, regtype, sname = self.slotloc(mem.number)
assert _mem
if mem.empty:
if regtype in ["memory", "pms"]:
store_bit(self._memobj.enable, ndx, False)
return
# Clear unknowns
_mem.unused = 0
_mem.unknown1 = 0
_mem.unknown2 = 0
txfreq = mem.freq / 10 # really. RX freq is used for TX base
_mem.freq = txfreq
self.encode_sql(mem, _mem)
if mem.power:
_mem.tx_pwr = 2 - POWER_LEVELS.index(mem.power)
else:
_mem.tx_pwr = 0 # set to "High" if CHIRP canonical value is None
_mem.tx_width = mem.mode == "NFM"
_mem.step = STEP_CODE.index(mem.tuning_step)
duplex = mem.duplex
offset = mem.offset
if self.DUPLEX_OFF_VIA_OFFSET and duplex == "off":
band = self.band_for_freq(mem.freq)
if band is not None and self.DUPLEX_OFF_VIA_OFFSET[band]:
duplex, offset = self.DUPLEX_OFF_VIA_OFFSET[band]
freq_offset_factor = self.freq_offset_factor
# FT-25R/65R Asia version (US version is 0)?
if self.subtype == 3 and freq_offset_factor == 2:
freq_offset_factor = 1 # 25000 scaler
_mem.offset = offset / (25000 * freq_offset_factor)
if regtype in ["memory", "pms"]:
ndx = num - 1
store_bit(self._memobj.enable, ndx, True)
store_bit(self._memobj.scan, ndx, SKIPS.index(mem.skip))
nametrim = (mem.name + " ")[:8]
self._memobj.names[ndx].chrs = bytearray(nametrim, "ascii")
if duplex == "split":
txfreq = offset / 10
self._memobj.txfreqs[num-1].freq = txfreq
_mem.duplex = DUPLEX.index(duplex)
if regtype in ["vfo", "home"]:
self.enforce_band(_mem, mem.freq, num, sname)
return
class YaesuFT4GenericRadio(YaesuSC35GenericRadio):
"""
FT-4 sub family class. Classes for individual radios extend
these classes and are found at the end of this file.
"""
class_specials = SPECIALS_FT4
Pkeys = 2 # number of programmable keys
namelen = 6 # length of the mem name display on the front-panel
freq_offset_factor = 1 # 25000 * 1
class_group_descs = YaesuSC35GenericRadio.group_descriptions
# names for the setmode function for the programmable keys. Mode zero means
# that the key is programmed for a memory not a setmode.
SETMODES = [
"mem", "apo", "ar bep", "ar int", "beclo", # 00-04
"beep", "bell", "cw id", "cw wrt", "dc vlt", # 05-09
"dcs cod", "dt dly", "dt set", "dtc spd", "edg.bep", # 10-14
"lamp", "led.bsy", "led.tx", "lock", "m/t-cl", # 15-19
"mem.del", "mem.tag", "pag.abk", "pag.cdr", "pag.cdt", # 20-24
"pri.rvt", "pswd", "pswdwt", "rf sql", "rpt.ars", # 25-29
"rpt.frq", "rpt.sft", "rxsave", "scn.lmp", "scn.rsm", # 30-34
"skip", "sql.typ", "step", "tn frq", "tot", # 35-39
"tx pwr", "tx save", "vfo.spl", "vox", "wfm.rcv", # 40-44
"w/n.dev", "wx.alert" # 45-46
]
def _valid_tuning_step(self, mem):
"""Return an explicit legal step compatible with the frequency."""
return _valid_tuning_step(self, mem)
def get_memory(self, memref):
mem = super().get_memory(memref)
mem.tuning_step = self._valid_tuning_step(mem)
return mem
def set_memory(self, mem):
mem = mem.dupe()
mem.tuning_step = self._valid_tuning_step(mem)
return super().set_memory(mem)
class YaesuFT65GenericRadio(YaesuSC35GenericRadio):
"""
FT-65 sub family class. Classes for individual radios extend
these classes and are found at the end of this file.
"""
class_specials = SPECIALS_FT65
Pkeys = 4 # number of programmable keys
namelen = 8 # length of the mem name display on the front-panel
freq_offset_factor = 2 # 25000 * 2
# we need a deep copy here because we are adding deeper than the top level.
class_group_descs = copy.deepcopy(YaesuSC35GenericRadio.group_descriptions)
add_paramdesc(
class_group_descs, "misc", ("compander", "Compander", ["OFF", "ON"]))
# names for the setmode function for the programmable keys. Mode zero means
# that the key is programmed for a memory not a setmode.
SETMODES = [
"mem", "apo", "arts", "battsave", "b-ch.l/o", # 00-04
"beep", "bell", "compander", "ctcss", "cw id", # 05-09
"dc volt", "dcs code", "dtmf set", "dtmf wrt", "edg bep", # 10-14
"key lock", "lamp", "ledbsy", "mem del", "mon/t-cl", # 15-19
"name tag", "pager", "password", "pri.rvt", "repeater", # 20-24
"resume", "rf.sql", "scn.lamp", "skip", "sql type", # 25-29
"step", "tot", "tx pwr", "tx save", "vfo.spl", # 30-34
"vox", "wfm.rcv", "wide/nar", "wx alert", "scramble" # 35-39
]
def _valid_tuning_step(self, mem):
"""Return a legal explicit step for an FT-65-family memory."""
if mem.tuning_step in self.legal_steps:
return mem.tuning_step
return _valid_tuning_step(self, mem)
def get_memory(self, memref):
mem = super().get_memory(memref)
mem.tuning_step = self._valid_tuning_step(mem)
return mem
def set_memory(self, mem):
mem = mem.dupe()
mem.tuning_step = self._valid_tuning_step(mem)
return super().set_memory(mem)
# Classes for each individual radio.
@directory.register
class YaesuFT4XRRadio(YaesuFT4GenericRadio):
"""
FT-4X dual band, US version
"""
MODEL = "FT-4XR"
id_str = b'IFT-35R\x00\x00V100\x00\x00'
valid_bands = VALID_BANDS_DUAL
DUPLEX_AUTO = DUPLEX_AUTO_US
legal_steps = US_LEGAL_STEPS
BAND_ASSIGNMENTS = BAND_ASSIGNMENTS_DUALBAND
@directory.register
class YaesuFT4XERadio(YaesuFT4GenericRadio):
"""
FT-4X dual band, EU version
"""
MODEL = "FT-4XE"
id_str = b'IFT-35R\x00\x00V100\x00\x00'
valid_bands = VALID_BANDS_DUAL
DUPLEX_AUTO = DUPLEX_AUTO_EU
legal_steps = VALID_STEPS
BAND_ASSIGNMENTS = BAND_ASSIGNMENTS_DUALBAND
@directory.register
class YaesuFT4VRRadio(YaesuFT4GenericRadio):
"""
FT-4V VHF, US version
"""
MODEL = "FT-4VR"
id_str = b'IFT-15R\x00\x00V100\x00\x00'
valid_bands = VALID_BANDS_VHF
DUPLEX_AUTO = DUPLEX_AUTO_US
legal_steps = US_LEGAL_STEPS
BAND_ASSIGNMENTS = BAND_ASSIGNMENTS_MONO_VHF
# No image available yet
# @directory.register
# class YaesuFT4VERadio(YaesuFT4GenericRadio):
# """
# FT-4V VHF, EU version
# """
# MODEL = "FT-4VE"
# id_str = b'IFT-15R\x00\x00V100\x00\x00'
# valid_bands = VALID_BANDS_VHF
# DUPLEX_AUTO = DUPLEX_AUTO_EU
# legal_steps = STEP_CODE
# BAND_ASSIGNMENTS = BAND_ASSIGNMENTS_MONO_VHF
@directory.register
class YaesuFT65RRadio(YaesuFT65GenericRadio):
"""
FT-65 dual band, US/Asia version
"""
MODEL = "FT-65R"
id_str = b'IH-420\x00\x00\x00V100\x00\x00'
valid_bands = VALID_BANDS_DUAL
DUPLEX_AUTO = DUPLEX_AUTO_US
legal_steps = VALID_STEPS
BAND_ASSIGNMENTS = BAND_ASSIGNMENTS_DUALBAND
DUPLEX_OFF_VIA_OFFSET = [ # matches the order of valid_bands
None, # Don't modify broadcast FM memories
("-", 99_950_000),
("+", 99_950_000),
]
@directory.register
class YaesuFT65ERadio(YaesuFT65GenericRadio):
"""
FT-65 dual band, EU version
"""
MODEL = "FT-65E"
id_str = b'IH-420\x00\x00\x00V100\x00\x00'
valid_bands = VALID_BANDS_DUAL
DUPLEX_AUTO = DUPLEX_AUTO_EU
legal_steps = VALID_STEPS
BAND_ASSIGNMENTS = BAND_ASSIGNMENTS_DUALBAND
@directory.register
class YaesuFT25RRadio(YaesuFT65GenericRadio):
"""
FT-25 VHF, US/Asia version
"""
MODEL = "FT-25R"
id_str = b'IFT-25R\x00\x00V100\x00\x00'
valid_bands = VALID_BANDS_VHF
DUPLEX_AUTO = DUPLEX_AUTO_US
legal_steps = US_LEGAL_STEPS
BAND_ASSIGNMENTS = BAND_ASSIGNMENTS_MONO_VHF
# No image available yet
# @directory.register
# class YaesuFT25ERadio(YaesuFT65GenericRadio):
# """
# FT-25 VHF, EU version
# """
# MODEL = "FT-25E"
# id_str = b'IFT-25R\x00\x00V100\x00\x00'
# valid_bands = VALID_BANDS_VHF
# DUPLEX_AUTO = DUPLEX_AUTO_EU
# legal_steps = STEP_CODE
# BAND_ASSIGNMENTS = BAND_ASSIGNMENTS_MONO_VHF
chirp-20261030/chirp/drivers/ft450d.py 0000644 0001750 0001750 00000260214 15266622247 016216 0 ustar hibby hibby # Copyright 2018 by Rick DeWitt (aa0rd@yahoo.com) V2.0
# PY3 Compliant release.
# Thanks to Filippi Marco for Yaesu processes
#
# 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 .
"""FT450D Yaesu Radio Driver"""
from chirp.drivers import yaesu_clone
from chirp import chirp_common, util, memmap, errors, directory, bitwise
from chirp.settings import RadioSetting, RadioSettingGroup, \
RadioSettingValueInteger, RadioSettingValueList, \
RadioSettingValueBoolean, RadioSettingValueString, \
RadioSettings
import time
import struct
import logging
import itertools
LOG = logging.getLogger(__name__)
CMD_ACK = b'\x06'
# can't update chirp_common data modes for this radio, so for weird
# data modes we will use main mode of DIG and then have these sub-modes
DATA_MODES = ["USER-L", "USER-U", "RTTY", "N/A"]
T_STEPS = sorted(list(chirp_common.TUNING_STEPS))
T_STEPS.remove(30.0)
T_STEPS.remove(100.0)
T_STEPS.remove(125.0)
T_STEPS.remove(200.0)
def filters_for_mode(mode):
if mode in ("USB", "LSB"):
return "sb_width", ["1.8 kHz", "2.4 kHz", "3.0 kHz"]
elif mode in ("CW", "CWR"):
return "cw_width", ["300 Hz", "500 kHz", "2.4 kHz"]
elif mode == "DIG":
return "sb_width", ["300 Hz", "2.4 kHz", "3.0 kHz"]
elif mode == "AM":
return "am_width", ["3.0 kHz", "6.0 kHz", "9.0 kHz"]
elif mode in ("NFM", "FM"):
return "fm_width", ["2.5 kHz", "5.0 kHz"]
else:
raise ValueError("Internal error: unknown mode %s" % mode)
def filter_to_hz(item):
mag, units = item.split(' ')
return float(mag) * (1000 if units == 'kHz' else 1)
def all_filters():
options = list(itertools.chain.from_iterable(
filterlist for k, filterlist in [
filters_for_mode('CW'), filters_for_mode('USB'),
filters_for_mode('DIG'), filters_for_mode('FM'),
filters_for_mode('AM')]))
return sorted(set(options), key=filter_to_hz)
def closest_filter(options, value):
value = filter_to_hz(value)
dists = [abs(value - filter_to_hz(o)) for o in options]
closest = options[dists.index(min(dists))]
LOG.debug('Chose %s as closest filter to %s from %s',
closest, value, ','.join(options))
return closest
@directory.register
class FT450DRadio(yaesu_clone.YaesuCloneModeRadio):
"""Yaesu FT-450D"""
FT450 = False
BAUD_RATE = 38400
COM_BITS = 8 # number of data bits
COM_PRTY = 'N' # parity checking
COM_STOP = 1 # stop bits
MODEL = "FT-450D"
DUPLEX = ["", "-", "+"]
MODES = ["LSB", "USB", "CW", "AM", "FM", "DIG",
"NFM", "CWR"]
TMODES = ["", "Tone", "TSQL"]
STEPSFM = [5.0, 6.25, 10.0, 12.5, 15.0, 20.0, 25.0, 50.0]
STEPSAM = [2.5, 5.0, 9.0, 10.0, 12.5, 25.0]
STEPSSSB = [1.0, 2.5, 5.0]
VALID_BANDS = [(100000, 33000000), (33000000, 56000000)]
FUNC_LIST = ['MONI', 'N/A', 'PBAK', 'PLAY1', 'PLAY2', 'PLAY3', 'QSPLIT',
'SPOT', 'SQLOFF', 'SWR', 'TXW', 'VCC', 'VOICE2', 'VM1MONI',
'VM1REC', 'VM1TX', 'VM2MONI', 'VM2REC', 'VM2TX', 'DOWN', 'FAST',
'UP', 'DSP', 'IPO/ATT', 'NB', 'AGC', 'MODEDN', 'MODEUP',
'DSP/SEL', 'KEYER', 'CLAR', 'BANDDN', 'BANDUP', 'A=B', 'A/B',
'LOCK', 'TUNE', 'VOICE', 'MW', 'V/M', 'HOME', 'RCL', 'VOX', 'STO',
'STEP', 'SPLIT', 'PMS', 'SCAN', 'MENU', 'DIMMER', 'MTR']
CHARSET = list(chirp_common.CHARSET_ASCII)
CHARSET.remove("\\")
MEM_SIZE = 15017
@classmethod
def match_model(cls, filedata, filename):
if len(filedata) == cls.MEM_SIZE + 7:
return filedata[cls.MEM_SIZE:].startswith(cls.MODEL.encode())
return False
# block 9 (135 Bytes long) is to be repeated 101 times
_block_lengths = [4, 84, 135, 162, 135, 162, 151, 130, 135, 127, 189, 103]
MEM_FORMAT = """
struct mem_struct { // 27 bytes per channel
u8 tag_on_off:2, // @ Byte 0 1=Off, 2=On
unk0:2,
mode:4;
u8 duplex:2, // @ byte 1
att:1,
ipo:1,
unka1:1,
tunerbad:1, // ?? Possible tuner failed
unk1b:1, // @@@???
uprband:1;
u8 cnturpk:1, // @ Byte 2 Peak (clr), Null (set)
cnturmd:3, // Contour filter mode
cnturgn:1, // Contour filter gain Low/high
mode2:3; // When mode is data(5)
u8 ssb_step:2, // @ Byte 3
am_step:3,
fm_step:3;
u8 tunerok:1, // @ Byte 4 ?? Poss tuned ok
cnturon:1,
unk4b:1,
dnr_on:1,
notch:1,
unk4c:1,
tmode:2; // Tone/Cross/etc as Off/Enc/Enc+Dec
u8 unk5a:4, // @ byte 5
dnr_val:4;
u8 cw_width:2, // # byte 6, Notch width indexes
fm_width:2,
am_width:2,
sb_width:2;
i8 notch_pos; // @ Byte 7 Signed: - 0 +
u8 tone; // @ Byte 8
u8 unk9; // @ Byte 9 Always set to 0
u8 unkA; // @ Byte A
u8 unkB; // @ Byte B
u32 freq; // @ C-F
u32 offset; // @ 10-13
u8 name[7]; // @ 14-1A
};
struct model{
u8 modname[4];
};
#seekto 0x04;
struct {
u8 set04; // ?Checksum / Clone counter?
u8 set05; // Current VFO?
u8 set06;
u8 fast:1,
lock:1, // Inverted: 1 = Off
nb:1,
agc:5;
u8 set08a:3,
keyer:1,
set08b:2,
mtr_mode:2;
u8 set09;
u8 set0A;
u8 set0B:2,
clk_sft:1,
cont:5; // 1:1
u8 beepvol_sgn:1, // @x0C: set : Link @0x41, clear: fix @ 0x40
set0Ca:3,
clar_btn:1, // 0 = Dial, 1= SEL
cwstone_sgn:1, // Set: Lnk @ x42, clear: Fixed at 0x43
beepton:2; // Index 0-3
u8 set0Da:1,
cw_key:1,
set0Db:3,
dialstp_mode:1,
dialstp:2;
u8 set0E:1,
keyhold:1,
lockmod:1,
set0ea:1,
amfmdial:1, // 0= Enabled. 1 = Disabled
cwpitch:3; // 0-based index
u8 sql_rfg:1,
set0F:2,
cwweigt:5; // Index 1:2.5=0 -> 1:4.5=20
u8 cw_dly; // @x10 ms = val * 10
u8 set11;
u8 cwspeed; // val 4-60 is wpm, *5 is cpm
u8 vox_gain; // val 1:1
u8 set14:2,
emergen:1,
vox_dly:5; // ms = val * 100
u8 set15a:1,
stby_beep:1,
set15b:1,
mem_grp:1,
apo:4;
u8 tot; // Byte x16, 1:1
u8 micscan:1,
set17:5,
micgain:2;
u8 cwpaddl:1, // @x18 0=Key, 1=Mic
set18:7;
u8 set19;
u8 set1A;
u8 set1B;
u8 set1C;
u8 dig_vox; // 1:1
u8 set1E;
i16 d_disp; // @ x1F,x20 signed 16bit
u8 pnl_cs; // 0-based index
u8 pm_up;
u8 pm_fst;
u8 pm_dwn;
u8 set25;
u8 set26;
u8 set27;
u8 set28;
u8 beacon_time; // 1:1
u8 set2A;
u8 cat_rts:1, // @x2b: Enable=0, Disable=1
peakhold:1,
set2B:4,
cat_tot:2; // Index 0-3
u8 set2CA:2,
rtyrpol:1,
rtytpol:1,
rty_sft:2,
rty_ton:1,
set2CC:1;
u8 dig_vox_dupe; // 1:1
u8 ext_mnu:1,
m_tune:1,
set2E:2,
scn_res:4;
u8 cw_auto:1, // Off=0, On=1
cwtrain:2, // Index
set2F:1,
cw_qsk:2, // Index
cw_bfo:2; // Index
u8 mic_eq; // @x30 1:1
u8 set31:5,
catrate:3; // Index 0-4
u8 set32;
u8 dimmer:4,
set33:4;
u8 set34;
u8 set35;
u8 set36;
u8 set37;
u8 set38a:1,
rfpower:7; // 1:1
u8 set39a:2,
tuner:3, // Index 0-4
seldial:3; // Index 0-5
u8 set3A;
u8 set3B;
u8 set3C;
i8 qspl_f; // Signed
u8 set3E;
u8 set3F;
u8 beepvol_fix; // 1:1
i8 beepvol_lnk; // SIGNED 2's compl byte
u8 cwstone_fix;
i8 cwstone_lnk; // signed byte
u8 set44:2,
mym_data:1, // My Mode: Data, set = OFF
mym_fm:1,
mym_am:1,
mym_cw:1,
mym_usb:1,
mym_lsb:1;
u8 myb_24:1, // My Band: 24 MHz set = OFF
myb_21:1,
myb_18:1,
myb_14:1,
myb_10:1,
myb_7:1,
myb_3_5:1,
myb_1_8:1;
u8 set46:6,
myb_28:1,
myb_50:1;
u8 set47;
u8 set48;
u8 set49;
u8 set4A;
u8 set4B;
u8 set4C;
u8 set4D;
u8 set4E;
u8 set4F;
u8 set50;
u8 set51;
u8 set52;
u8 set53;
u8 set54;
u8 set55;
u8 set56a:3,
split:1,
set56b:4;
u8 set57;
} settings;
//#seekto 0x58;
struct mem_struct vfoa[11]; // The current cfgs for each vfo 'band'
struct mem_struct vfob[11];
struct mem_struct home[2]; // The 2 Home cfgs (HF and 6m)
struct mem_struct qmb; // The Quick Memory Bank STO/RCL
struct mem_struct mtqmb; // Last QMB-MemTune cfg (not displayed)
struct mem_struct mtune; // Last MemTune cfg (not displayed)
#seekto 0x343; // chan status
u8 visible[63]; // 1 bit per channel
u8 pmsvisible; // @ 0x382
//#seekto 0x383;
u8 filled[63];
u8 pmsfilled; // @ 0x3c2
//#seekto 0x3C3;
struct mem_struct memory[500];
struct mem_struct pms[4]; // Programmed Scan limits @ x387F
#seekto 0x3906;
struct {
char t1[40]; // CW Beacon Text
char t2[40];
char t3[40];
} beacontext; // to 0x397E
#seekto 0x3985;
struct mem_struct m60[5]; // to 0x3A0B
#seekto 0x03a45;
struct mem_struct current;
"""
_CALLSIGN_CHARSET = [chr(x) for x in list(range(ord("0"), ord("9") + 1)) +
list(range(ord("A"), ord("Z") + 1)) + [ord(" ")]]
_CALLSIGN_CHARSET_REV = dict(zip(_CALLSIGN_CHARSET,
range(0, len(_CALLSIGN_CHARSET))))
# WARNING Indecis are hard wired in get/set_memory code !!!
# Channels print in + increasing index order (PMS first)
SPECIAL_MEMORIES = {
"VFOa-1.8M": -27,
"VFOa-3.5M": -26,
"VFOa-7M": -25,
"VFOa-10M": -24,
"VFOa-14M": -23,
"VFOa-18M": -22,
"VFOa-21M": -21,
"VFOa-24M": -20,
"VFOa-28M": -19,
"VFOa-50M": -18,
"VFOa-HF": -17,
"VFOb-1.8M": -16,
"VFOb-3.5M": -15,
"VFOb-7M": -14,
"VFOb-10M": -13,
"VFOb-14M": -12,
"VFOb-18M": -11,
"VFOb-21M": - 10,
"VFOb-24M": -9,
"VFOb-28M": -8,
"VFOb-50M": -7,
"VFOb-HF": -6,
"HOME-HF": -5,
"HOME-50M": -4,
"QMB": -3,
"QMB-MTune": -2,
"Mem-Tune": -1,
}
FIRST_VFOB_INDEX = -6
LAST_VFOB_INDEX = -16
FIRST_VFOA_INDEX = -17
LAST_VFOA_INDEX = -27
SPECIAL_PMS = {
"PMS1-L": -36,
"PMS1-U": -35,
"PMS2-L": -34,
"PMS2-U": -33,
}
LAST_PMS_INDEX = -36
SPECIAL_MEMORIES.update(SPECIAL_PMS)
SPECIAL_60M = {
"60m-Ch1": -32,
"60m-Ch2": -31,
"60m-Ch3": -30,
"60m-Ch4": -29,
"60m-Ch5": -28,
}
LAST_60M_INDEX = -32
SPECIAL_MEMORIES.update(SPECIAL_60M)
SPECIAL_MEMORIES_REV = dict(zip(SPECIAL_MEMORIES.values(),
SPECIAL_MEMORIES.keys()))
@classmethod
def get_prompts(cls):
rp = chirp_common.RadioPrompts()
rp.info = _(
"The FT-450D radio driver loads the 'Special Channels' tab\n"
"with the PMS scanning range memories (group 11), 60meter\n"
"channels (group 12), the QMB (STO/RCL) memory, the HF and\n"
"50m HOME memories and all the A and B VFO memories.\n"
"There are VFO memories for the last frequency dialed in\n"
"each band. The last mem-tune config is also stored.\n"
"These Special Channels allow limited field editing.\n"
"This driver also populates the 'Other' tab in the channel\n"
"memory Properties window. This tab contains values for\n"
"those channel memory settings that don't fall under the\n"
"standard Chirp display columns. With FT450 support, the gui now\n"
"uses the mode 'DIG' to represent data modes and a new column\n"
"'DATA MODE' to select USER-U, USER-L and RTTY \n")
rp.pre_download = _(
"1. Turn radio off.\n"
"2. Connect cable to ACC jack.\n"
"3. Press and hold in the [MODE <] and [MODE >] keys"
" while\n"
" turning the radio on (\"CLONE MODE\" will appear on the\n"
" display).\n"
"4. After clicking OK here, press the [C.S.] key to\n"
" send image.\n")
rp.pre_upload = _(
"1. Turn radio off.\n"
"2. Connect cable to ACC jack.\n"
"3. Press and hold in the [MODE <] and [MODE >] keys"
" while\n"
" turning the radio on (\"CLONE MODE\" will appear on the\n"
" display).\n"
"4. Click OK here.\n"
" (\"Receiving\" will appear on the LCD).\n")
return rp
def _read(self, block, blocknum):
# be very patient at first block
if blocknum == 0:
attempts = 60
else:
attempts = 5
for _i in range(0, attempts):
data = self.pipe.read(block + 2) # Blocknum, data,checksum
if data:
break
time.sleep(0.5)
if not data:
if blocknum == 0:
raise errors.RadioNoResponse()
raise errors.RadioError("No response reading block %02X" % blocknum)
if len(data) == block + 2 and data[0] == blocknum:
checksum = yaesu_clone.YaesuChecksum(1, block)
if checksum.get_existing(data) != \
checksum.get_calculated(data):
raise Exception("Checksum Failed [%02X<>%02X] block %02X" %
(checksum.get_existing(data),
checksum.get_calculated(data), blocknum))
# Remove the block number and checksum
data = data[1:block + 1]
elif self.FT450:
# unique FT450 (and 450AT?) vs D, only 2 bytes in first block
if blocknum == 0 and len(data) == 4:
checksum = yaesu_clone.YaesuChecksum(1, block - 2)
if checksum.get_existing(data) != \
checksum.get_calculated(data):
raise Exception("Checksum Failed [%02X<>%02X] block %02X" %
(checksum.get_existing(data),
checksum.get_calculated(data), blocknum))
# Remove the block number and checksum
data = data[1:block - 2 + 1]
# pad the data to the correct 450d size
data += b" "
else:
# FT450, but bad block
raise Exception("Unable to read block %i expected %i got %i"
% (blocknum, block + 2, len(data)))
else:
# Use this info to decode a new Yaesu model
# Not an old FT450 or 450AT
raise Exception("Unable to read block %i expected %i got %i"
% (blocknum, block + 2, len(data)))
return data
def _clone_in(self):
# Be very patient with the radio
self.pipe.timeout = 2
self.pipe.baudrate = self.BAUD_RATE
self.pipe.bytesize = self.COM_BITS
self.pipe.parity = self.COM_PRTY
self.pipe.stopbits = self.COM_STOP
self.pipe.rtscts = False
start = time.time()
data = b""
blocks = 0
status = chirp_common.Status()
status.msg = _("Cloning from radio")
nblocks = len(self._block_lengths) + 100 # Block 8 repeats
status.max = nblocks
for block in self._block_lengths:
if blocks == 8:
# repeated read of block 8 same size (chan memory area)
repeat = 101
else:
repeat = 1
for _i in range(0, repeat):
data += self._read(block, blocks)
self.pipe.write(CMD_ACK)
blocks += 1
status.cur = blocks
self.status_fn(status)
data += self.MODEL.encode()
return memmap.MemoryMapBytes(data)
def _clone_out(self):
self.pipe.baudrate = self.BAUD_RATE
self.pipe.bytesize = self.COM_BITS
self.pipe.parity = self.COM_PRTY
self.pipe.stopbits = self.COM_STOP
self.pipe.rtscts = False
delay = 0.5
start = time.time()
blocks = 0
pos = 0
status = chirp_common.Status()
status.msg = _("Cloning to radio")
status.max = len(self._block_lengths) + 100
for block in self._block_lengths:
if blocks == 8:
repeat = 101
else:
repeat = 1
for _i in range(0, repeat):
time.sleep(0.01)
# If this is a 450 model, we need to send just the first two model bytes
# in block 0 and just checksum that part
if self.FT450 and blocks == 0:
checksum = yaesu_clone.YaesuChecksum(pos, pos + block - 2 - 1)
else:
checksum = yaesu_clone.YaesuChecksum(pos, pos + block - 1)
LOG.debug("Sending block %s" % hex(blocks))
self.pipe.write(struct.pack('B', blocks))
if self.FT450 and blocks == 0:
blkdat = self.get_mmap()[pos:pos + block - 2]
else:
blkdat = self.get_mmap()[pos:pos + block]
LOG.debug("Sending %d bytes:\n%s"
% (len(blkdat), util.hexprint(blkdat)))
self.pipe.write(blkdat)
xs = checksum.get_calculated(self.get_mmap())
LOG.debug("Sending checksum %s" % hex(xs))
self.pipe.write(struct.pack('B', xs))
buf = self.pipe.read(1)
if not buf or buf[:1] != CMD_ACK:
time.sleep(delay)
buf = self.pipe.read(1)
if not buf:
if blocks == 0:
raise errors.RadioNoResponse()
raise errors.RadioError("Radio did not ack block %i" % blocks)
if buf[:1] != CMD_ACK:
raise Exception(_("Radio did not ack block %i") % blocks)
pos += block
blocks += 1
status.cur = blocks
self.status_fn(status)
def sync_in(self):
try:
self._mmap = self._clone_in()
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._clone_out()
except errors.RadioError:
raise
except Exception as e:
raise errors.RadioError("Failed to communicate with radio: %s"
% e)
def process_mmap(self):
self._memobj = bitwise.parse(self.MEM_FORMAT, self._mmap)
def get_features(self):
rf = chirp_common.RadioFeatures()
rf.has_bank = False
rf.has_dtcs = False
rf.valid_modes = [x for x in self.MODES if x in chirp_common.MODES]
rf.valid_tmodes = list(self.TMODES)
rf.valid_duplexes = list(self.DUPLEX)
rf.valid_tuning_steps = list(T_STEPS)
rf.valid_bands = self.VALID_BANDS
rf.valid_power_levels = []
rf.valid_characters = "".join(self.CHARSET)
rf.valid_name_length = 7
rf.valid_skips = []
rf.valid_special_chans = sorted(self.SPECIAL_MEMORIES.keys())
rf.memory_bounds = (1, 500)
rf.has_ctone = True
rf.has_settings = True
rf.has_cross = True
rf.has_nostep_tuning = True
return rf
def get_raw_memory(self, number):
return repr(self._memobj.memory[number - 1])
def _get_tmode(self, mem, _mem):
mem.tmode = self.TMODES[_mem.tmode]
mem.rtone = chirp_common.TONES[_mem.tone]
mem.ctone = mem.rtone
def _set_duplex(self, mem, _mem):
_mem.duplex = self.DUPLEX.index(mem.duplex)
def get_memory(self, number):
if isinstance(number, str):
return self._get_special(number)
elif number < 0:
# I can't stop delete operation from losing extd_number but
# I know how to get it back
return self._get_special(self.SPECIAL_MEMORIES_REV[number])
else:
return self._get_normal(number)
def set_memory(self, memory):
# If this is called due to a delete request on the gui,
# then the memory passed might be passed as a str,
# since the special memories have string tags on the GUI and not
# numbers. Convert these back to the int for the specials
if isinstance(memory.number, str):
numeric_number = self.SPECIAL_MEMORIES[memory.number]
return self._set_special(memory, mem_number=numeric_number)
if memory.number < 0:
return self._set_special(memory)
else:
return self._set_normal(memory)
def _get_special(self, number):
mem = chirp_common.Memory()
mem.number = self.SPECIAL_MEMORIES[number]
mem.extd_number = number
if mem.number in range(self.FIRST_VFOA_INDEX,
self.LAST_VFOA_INDEX - 1, -1):
_mem = self._memobj.vfoa[-self.LAST_VFOA_INDEX + mem.number]
immutable = ["number", "extd_number", "name", "power"]
elif mem.number in range(self.FIRST_VFOB_INDEX,
self.LAST_VFOB_INDEX - 1, -1):
_mem = self._memobj.vfob[-self.LAST_VFOB_INDEX + mem.number]
immutable = ["number", "extd_number", "name", "power"]
elif mem.number in range(-4, -6, -1): # 2 Home Chans
_mem = self._memobj.home[5 + mem.number]
immutable = ["number", "extd_number", "name", "power"]
elif mem.number == -3:
_mem = self._memobj.qmb
immutable = ["number", "extd_number", "name", "power"]
elif mem.number == -2:
_mem = self._memobj.mtqmb
immutable = ["number", "extd_number", "name", "power"]
elif mem.number == -1:
_mem = self._memobj.mtune
immutable = ["number", "extd_number", "name", "power"]
elif mem.number in self.SPECIAL_PMS.values():
# ft450 does NOT use the pmsvisible or pmsfilled fields,
# it uses the next 4 bits after the 500 normal memories
# in the main visible and filled settings area.
if self.FT450:
pmsnum = 501 + (-self.LAST_PMS_INDEX) + mem.number
used = (self._memobj.visible[(pmsnum - 1) // 8] >> \
(pmsnum - 1) % 8) & 0x01
valid = (self._memobj.filled[(pmsnum - 1) // 8] >> \
(pmsnum - 1) % 8) & 0x01
else:
bitindex = (-self.LAST_PMS_INDEX) + mem.number
used = (self._memobj.pmsvisible >> bitindex) & 0x01
valid = (self._memobj.pmsfilled >> bitindex) & 0x01
# You can clear the PMS memories on the FT450
# using the CLAR button and then they do not display.
if not used:
mem.empty = True
if not valid:
mem.empty = True
return mem
mx = (-self.LAST_PMS_INDEX) + mem.number
_mem = self._memobj.pms[mx]
mx = mx + 1
immutable = ["number", "rtone", "ctone", "extd_number",
"tmode", "cross_mode",
"power", "duplex", "offset"]
elif mem.number in self.SPECIAL_60M.values():
mx = (-self.LAST_60M_INDEX) + mem.number
_mem = self._memobj.m60[mx]
mx = mx + 1
immutable = ["number", "rtone", "ctone", "extd_number",
"tmode", "cross_mode",
"frequency", "power", "duplex", "offset"]
else:
raise Exception("Sorry, you can't edit that special"
" memory channel %i." % mem.number)
mem = self._get_memory(mem, _mem)
mem.immutable = immutable
return mem
def _set_special(self, mem, mem_number=None):
number = mem_number if mem_number is not None else mem.number
if mem.empty and number not in self.SPECIAL_PMS.values():
# can't delete special memories! But can delete PMS mems
raise errors.RadioError("Sorry, special memory can't be deleted")
cur_mem = self._get_special(self.SPECIAL_MEMORIES_REV[number])
if number in range(self.FIRST_VFOA_INDEX,
self.LAST_VFOA_INDEX - 1, -1):
_mem = self._memobj.vfoa[-self.LAST_VFOA_INDEX + number]
elif number in range(self.FIRST_VFOB_INDEX,
self.LAST_VFOB_INDEX - 1, -1):
_mem = self._memobj.vfob[-self.LAST_VFOB_INDEX + number]
elif number in range(-4, -6, -1):
_mem = self._memobj.home[5 + number]
elif number == -3:
_mem = self._memobj.qmb
elif number == -2:
_mem = self._memobj.mtqmb
elif number == -1:
_mem = self._memobj.mtune
elif number in self.SPECIAL_PMS.values():
# ft450 does NOT use the pmsvisible or pmsfilled fields,
# it uses the next 4 bits after the 500 normal memories
# in the main visible and filled settings area.
if self.FT450:
pmsnum = 501 + (-self.LAST_PMS_INDEX) + number
wasused = (self._memobj.visible[(pmsnum - 1) // 8] >>
(pmsnum - 1) % 8) & 0x01
wasvalid = (self._memobj.filled[(pmsnum - 1) // 8] >>
(pmsnum - 1) % 8) & 0x01
if mem.empty:
if wasvalid and not wasused:
self._memobj.filled[(pmsnum - 1) // 8] &= \
~(1 << (pmsnum - 1) % 8)
self._memobj.visible[(pmsnum - 1) // 8] &= \
~(1 << (pmsnum - 1) % 8)
return
self._memobj.visible[(pmsnum - 1) // 8] |= 1 << (pmsnum - 1) \
% 8
self._memobj.filled[(pmsnum - 1) // 8] |= 1 << (pmsnum - 1) \
% 8
_mem = self._memobj.pms[-self.LAST_PMS_INDEX + number]
else: #self.FT450
bitindex = (-self.LAST_PMS_INDEX) + number
wasused = (self._memobj.pmsvisible >> bitindex) & 0x01
wasvalid = (self._memobj.pmsfilled >> bitindex) & 0x01
if mem.empty:
if wasvalid and not wasused:
# pylint get confused by &= operator
self._memobj.pmsfilled = self._memobj.pmsfilled & \
~ (1 << bitindex)
# pylint get confused by &= operator
self._memobj.pmsvisible = self._memobj.pmsvisible & \
~ (1 << bitindex)
return
# pylint get confused by |= operator
self._memobj.pmsvisible = self._memobj.pmsvisible | 1 << bitindex
self._memobj.pmsfilled = self._memobj.pmsfilled | 1 << bitindex
_mem = self._memobj.pms[-self.LAST_PMS_INDEX + number]
else:
raise errors.RadioError("Sorry, you can't edit"
" that special memory.")
for key in cur_mem.immutable:
if key != "extd_number":
if cur_mem.__dict__[key] != mem.__dict__[key]:
raise errors.RadioError("Editing field `%s' " % key +
"is not supported on this channel")
self._set_memory(mem, _mem)
def _get_normal(self, number):
_mem = self._memobj.memory[number - 1]
used = (self._memobj.visible[(number - 1) // 8] >> (number - 1) % 8) \
& 0x01
valid = (self._memobj.filled[(number - 1) // 8] >> (number - 1) % 8) \
& 0x01
mem = chirp_common.Memory()
mem.number = number
if not used:
mem.empty = True
if not valid or _mem.freq == 0xffffffff:
return mem
if mem.number == 1:
mem.immutable = ['empty']
return self._get_memory(mem, _mem)
def _set_normal(self, mem):
_mem = self._memobj.memory[mem.number - 1]
wasused = (self._memobj.visible[(mem.number - 1) // 8] >>
(mem.number - 1) % 8) & 0x01
wasvalid = (self._memobj.filled[(mem.number - 1) // 8] >>
(mem.number - 1) % 8) & 0x01
if mem.empty:
if mem.number == 1:
raise Exception("Sorry, can't delete first memory")
if wasvalid and not wasused:
self._memobj.filled[(mem.number - 1) // 8] &= \
~(1 << (mem.number - 1) % 8)
_mem.set_raw("\xFF" * (_mem.size() // 8)) # clean up
self._memobj.visible[(mem.number - 1) // 8] &= \
~(1 << (mem.number - 1) % 8)
return
if not wasvalid:
_mem.set_raw("\x00" * (_mem.size() // 8)) # clean up
self._memobj.visible[(mem.number - 1) // 8] |= 1 << (mem.number - 1) \
% 8
self._memobj.filled[(mem.number - 1) // 8] |= 1 << (mem.number - 1) \
% 8
self._set_memory(mem, _mem)
def _get_memory(self, mem, _mem):
mem.freq = int(_mem.freq)
mem.offset = int(_mem.offset)
mem.duplex = self.DUPLEX[_mem.duplex]
# Mode gets tricky with dual (USB+DATA) options
# WDC on FT450, the high bit of the nybble really
# means something else, but I don't know
# what it is. IE Sometimes LSB comes in as 1, sometime 8...
vx = _mem.mode
vx_data = 3 # Assume N/A unless reset below
if vx == 0: # LSB
pass
elif vx == 1: # USB
pass
elif vx == 2: # USB CW (CW)
pass
elif vx == 3: # AM
pass
elif vx == 4: # FM/NFM
if _mem.mode2 == 1:
vx = 6
else:
vx = 4
pass
elif vx == 5: # USER\L
# This looks different from FT450D
if self.FT450:
vx = 5 # set to new "DIG"
vx_data = 1
else:
# on 450D only 5 used for dig modes, no 13
if _mem.mode2 == 0: # RTTY
vx = 5 # Set to new "DIG"
vx_data = 2 # RTTY
elif _mem.mode2 == 1: # USER-L
vx = 5 # set to new "DIG"
vx_data = 0
elif _mem.mode2 == 2: # USER-U variant
vx = 5 # Set to new "DIG"
vx_data = 1 # USER-U with Split?
elif vx == 8: # LSB
vx = 0
elif vx == 9: # USB
vx = 1
elif vx == 10: # LSB CW (CWR)
vx = 7 # CWR
elif vx == 13:
if _mem.mode2 == 0: # RTTY
vx = 5 # Set to new "DIG"
vx_data = 2 # RTTY
elif _mem.mode2 == 1: # USER-L
vx = 5 # set to new "DIG"
vx_data = 0
elif _mem.mode2 == 2: # USER-U variant
vx = 5 # Set to new "DIG"
vx_data = 1 # USER-U with Split?
else:
LOG.error("unknown combo of mem.mode 13 and mem mode 2: %s %s",
vx,_mem.mode2)
else:
LOG.error("unknown _mem.mode value : %s", vx)
try:
mem.mode = self.MODES[vx]
except ValueError:
LOG.error('The FT-450 driver is broken for unsupported modes')
if mem.mode == "FM" or mem.mode == "NFM":
mem.tuning_step = self.STEPSFM[_mem.fm_step]
elif mem.mode == "AM":
mem.tuning_step = self.STEPSAM[_mem.am_step]
elif mem.mode[:2] == "CW":
mem.tuning_step = self.STEPSSSB[_mem.ssb_step]
else:
try:
mem.tuning_step = self.STEPSSSB[_mem.ssb_step]
except IndexError:
pass
self._get_tmode(mem, _mem)
if _mem.tag_on_off == 2:
for i in _mem.name:
if i == 0xFF:
break
if chr(i) in self.CHARSET:
mem.name += chr(i)
else:
# radio has some graphical chars that are not supported
# we replace those with a *
LOG.info("Replacing char %x with *" % i)
mem.name += "*"
mem.name = mem.name.rstrip()
else:
mem.name = ""
mem.extra = RadioSettingGroup("extra", "Extra")
# add a new field to handle the new sub-modes if dig
options = DATA_MODES
rs = RadioSetting("data_modes", "DATA MODE",
RadioSettingValueList(options,
current_index=vx_data))
rs.set_doc(_("Selects the data mode a DIG channel uses. RTTY is FSK "
"radioteletype keyed by a terminal unit or RTTY "
"software, while USER-L and USER-U carry audio based "
"data modes such as packet on the lower or upper "
"sideband. N/A is for channels that are not in DIG "
"mode."))
mem.extra.append(rs)
rs = RadioSetting("ipo", "IPO",
RadioSettingValueBoolean(bool(_mem.ipo)))
rs.set_doc(_("Intercept point optimization. Bypasses the receiver's "
"RF preamplifier so that the incoming signal goes "
"straight to the first mixer. This gives up some "
"sensitivity in exchange for better handling of strong "
"signals and high noise levels."))
mem.extra.append(rs)
rs = RadioSetting("att", "ATT",
RadioSettingValueBoolean(bool(_mem.att)))
rs.set_doc(_("Reduces all incoming signals by 20 dB, for use when "
"extremely strong nearby stations or high noise levels "
"degrade reception. Between 30 kHz and 1.7 MHz the radio "
"always attenuates, whatever this setting."))
mem.extra.append(rs)
rs = RadioSetting("cnturon", "Contour Filter",
RadioSettingValueBoolean(_mem.cnturon))
rs.set_doc(_("Turns on the DSP contour filter, which gently cuts or "
"boosts one part of the receive passband. Cutting "
"suppresses interference or excessive components of a "
"signal, boosting brings out the components that make it "
"easier to understand."))
mem.extra.append(rs)
options = ["Peak", "Null"]
rs = RadioSetting("cnturpk", "Contour Filter Mode",
RadioSettingValueList(options,
current_index=_mem.cnturpk))
rs.set_doc(_("Chooses whether the contour filter boosts (Peak) or "
"cuts (Null) the part of the passband it is set to."))
mem.extra.append(rs)
options = ["Low", "High"]
rs = RadioSetting("cnturgn", "Contour Filter Gain",
RadioSettingValueList(options,
current_index=_mem.cnturgn))
rs.set_doc(_("Sets how strongly the contour filter boosts or cuts. "
"Low gives a gentle effect, High a stronger one."))
mem.extra.append(rs)
options = ["-2", "-1", "Center", "+1", "+2"]
rs = RadioSetting("cnturmd", "Contour Filter Notch",
RadioSettingValueList(options,
current_index=_mem.cnturmd))
rs.set_doc(_("Moves the part of the passband the contour filter acts "
"on, from -2 at the low end through Center to +2 at the "
"high end."))
mem.extra.append(rs)
rs = RadioSetting("notch", "Notch Filter",
RadioSettingValueBoolean(_mem.notch))
rs.set_doc(_("Turns on the IF notch filter, a narrow and deep filter "
"that removes a single interfering carrier or whistle "
"from inside the receive passband."))
mem.extra.append(rs)
vx = 1
options = ["<-", "Center", "+>"]
if _mem.notch_pos < 0:
vx = 0
if _mem.notch_pos > 0:
vx = 2
rs = RadioSetting("notch_pos", "Notch Position",
RadioSettingValueList(options, current_index=vx))
rs.set_doc(_("Places the IF notch filter below the center of the "
"passband, at the center or above it. The exact notch "
"frequency is fine tuned on the radio with the DSP/SEL "
"knob."))
mem.extra.append(rs)
stx, options = filters_for_mode(mem.mode)
vx = getattr(_mem, stx)
rs = RadioSetting("bpfilter", "IF Bandpass Filter Width",
RadioSettingValueList(all_filters(), options[vx]))
rs.set_doc(_("Sets the width of the DSP IF filter. A narrower filter "
"rejects interference close to the signal, a wider one "
"sounds more natural when the band is quiet. The widths "
"depend on the mode: 1.8, 2.4 or 3.0 kHz for SSB, 300 "
"Hz, 500 Hz or 2.4 kHz for CW, 300 Hz, 2.4 or 3.0 kHz "
"for DIG, 3.0, 6.0 or 9.0 kHz for AM, and 2.5 or 5.0 kHz "
"for FM. A width the mode does not offer is replaced "
"with the closest one it does."))
mem.extra.append(rs)
rs = RadioSetting("dnr_on", "DSP Noise Reduction",
RadioSettingValueBoolean(bool(_mem.dnr_on)))
rs.set_doc(_("Turns on the DSP digital noise reduction, which reduces "
"random background noise on the HF and 50 MHz bands. It "
"is especially effective on SSB."))
mem.extra.append(rs)
options = ["Off", "1", "2", "3", "4", "5", "6", "7",
"8", "9", "10", "11"]
rs = RadioSetting("dnr_val", "DSP Noise Reduction Alg",
RadioSettingValueList(options,
current_index=_mem.dnr_val))
rs.set_doc(_("Selects which of the eleven noise reduction algorithms "
"is used. Each one is designed for a different kind of "
"noise, so try them in turn to find the one that best "
"suppresses the noise currently heard. Off also turns "
"DSP Noise Reduction off."))
mem.extra.append(rs)
return mem # end get_memory
def _set_memory(self, mem, _mem):
if len(mem.name) > 0:
_mem.tag_on_off = 2
else:
_mem.tag_on_off = 1
self._set_duplex(mem, _mem)
_mem.mode2 = 0
tmpmemmode = str(mem.mode)
if mem.mode == "DIG":
_mem.mode = 5 # We'll set _mode2 from extra data_modes
elif tmpmemmode == "CWR":
_mem.mode = 10
# 450 looks different
if self.FT450:
_mem.mode2 = 2
else:
_mem.mode2 = 0
elif tmpmemmode == "CW":
_mem.mode = 2
# 450 looks different
if self.FT450:
_mem.mode2 = 2
else:
_mem.mode2 = 0
elif tmpmemmode == "NFM":
_mem.mode = 4
_mem.mode2 = 1
elif tmpmemmode == "FM":
_mem.mode = 4
_mem.mode2 = 2
elif tmpmemmode == "LSB":
_mem.mode = 0
# 450 looks different
if self.FT450:
_mem.mode2 = 2
else:
_mem.mode2 = 0
elif tmpmemmode == "USB":
_mem.mode = 1
if self.FT450:
_mem.mode2 = 2
else:
_mem.mode2 = 0
elif tmpmemmode == "AM":
_mem.mode = 3
if self.FT450:
_mem.mode2 = 2
else:
_mem.mode2 = 0
else: # SHOULD NOT OCCUR
_mem.mode = self.MODES.index(mem.mode)
_mem.mode2 = 0
try:
_mem.ssb_step = self.STEPSSSB.index(mem.tuning_step)
except ValueError:
pass
try:
_mem.am_step = self.STEPSAM.index(mem.tuning_step)
except ValueError:
pass
try:
_mem.fm_step = self.STEPSFM.index(mem.tuning_step)
except ValueError:
pass
_mem.freq = mem.freq
_mem.uprband = 0
if mem.freq >= 33000000:
_mem.uprband = 1
_mem.offset = mem.offset
_mem.tmode = self.TMODES.index(mem.tmode)
_mem.tone = chirp_common.TONES.index(mem.rtone)
_mem.tunerok = 0 # Dont know what these two do...
_mem.tunerbad = 0
for i in range(0, 7):
_mem.name[i] = ord(mem.name.ljust(7)[i])
for setting in mem.extra:
if setting.get_name() == "notch_pos":
vx = 0 # Override list string with signed value
stx = str(setting.value)
if stx == "<-":
vx = -13
if stx == "+>":
vx = 12
setattr(_mem, "notch_pos", vx)
elif setting.get_name() == "dnr_val":
stx = str(setting.value) # Convert string to int
vx = 0
if stx != "Off":
vx = int(stx)
else:
setattr(_mem, "dnr_on", 0)
setattr(_mem, setting.get_name(), vx)
elif setting.get_name() == "data_modes":
if tmpmemmode == "DIG":
stx = str(setting.value)
if stx == "USER-L":
if self.FT450:
_mem.mode = 13
_mem.mode2 = 1
else:
_mem.mode = 5
_mem.mode2 = 1
elif stx == "USER-U":
_mem.mode = 5
_mem.mode2 = 2
elif stx == "RTTY":
if self.FT450:
_mem.mode = 13
_mem.mode2 = 0
else:
_mem.mode = 5
_mem.mode2 = 0
elif stx == "N/A":
_mem.mode = 5
_mem.mode2 = 2
else:
LOG.error("In _set_memory invalid digital data type %s", stx)
elif setting.get_name() == "bpfilter":
element, options = filters_for_mode(mem.mode)
if str(setting.value) in options:
setattr(_mem, element, options.index(str(setting.value)))
else:
default = closest_filter(options, str(setting.value))
LOG.warning('Memory specified filter width of %s for '
'mode %s which is not in allowed list of '
'%s. Defaulting to %s',
str(setting.value), mem.mode,
','.join(options), default)
setattr(_mem, element, options.index(default))
else:
setattr(_mem, setting.get_name(), setting.value)
def _invert_me(self, setting, obj, atrb):
"""Callback: from inverted logic 1-bit booleans"""
invb = not setting.value
setattr(obj, atrb, invb)
return
def _chars2str(self, cary, knt):
"""Convert raw memory char array to a string: NOT a callback."""
stx = ""
for char in cary[0:knt]:
stx += chr(int(char))
return stx
def _my_str2ary(self, setting, obj, atrba, knt):
"""Callback: convert string to fixed-length char array.."""
ary = ""
for j in range(0, knt, 1):
chx = ord(str(setting.value)[j])
if chx < 32 or chx > 125: # strip non-printing
ary += " "
else:
ary += str(setting.value)[j]
setattr(obj, atrba, ary)
return
def get_settings(self):
_settings = self._memobj.settings
_beacon = self._memobj.beacontext
gen = RadioSettingGroup("gen", "General")
cw = RadioSettingGroup("cw", "CW")
pnlcfg = RadioSettingGroup("pnlcfg", "Panel buttons")
pnlset = RadioSettingGroup("pnlset", "Panel settings")
voxdat = RadioSettingGroup("voxdat", "VOX and Data")
mic = RadioSettingGroup("mic", "Microphone")
mybands = RadioSettingGroup("mybands", "My Bands")
mymodes = RadioSettingGroup("mymodes", "My Modes")
top = RadioSettings(gen, cw, pnlcfg, pnlset, voxdat, mic,
mymodes, mybands)
self._do_general_settings(gen)
self._do_cw_settings(cw)
self._do_panel_buttons(pnlcfg)
self._do_panel_settings(pnlset)
self._do_vox_settings(voxdat)
self._do_mic_settings(mic)
self._do_mymodes_settings(mymodes)
self._do_mybands_settings(mybands)
return top
def _do_general_settings(self, tab):
_settings = self._memobj.settings
rs = RadioSetting("ext_mnu", "Extended menu",
RadioSettingValueBoolean(_settings.ext_mnu))
rs.set_doc(_("Shows all 62 items of the radio's Menu Mode. When off, "
"the menu on the radio offers only the 16 most often "
"used items."))
tab.append(rs)
# Issue #8183 bugfix
rs = RadioSetting("apo", "APO time (Hrs)",
RadioSettingValueInteger(0, 12, _settings.apo))
rs.set_doc(_("Turns the radio off automatically once no button, knob "
"or microphone key has been used and nothing has been "
"transmitted for the selected number of hours. APO "
"flashes on the display and a tone sounds in the last "
"minute before the radio switches off. 0 disables "
"automatic power off."))
tab.append(rs)
options = ["%i" % i for i in range(0, 21)]
options[0] = "Off"
rs = RadioSetting("tot", "TX 'TOT' time-out (mins)",
RadioSettingValueList(options,
current_index=_settings.tot))
rs.set_doc(_("Limits how long a single transmission may last. When "
"the time runs out the radio stops transmitting until "
"PTT is released, which prevents overheating and stops a "
"stuck PTT from blocking the channel for everyone else."))
tab.append(rs)
bx = not _settings.cat_rts # Convert from Enable=0
rs = RadioSetting("cat_rts", "CAT RTS flow control",
RadioSettingValueBoolean(bx))
rs.set_apply_callback(self._invert_me, _settings, "cat_rts")
rs.set_doc(_("Enables the RTS line of the CAT jack. Turn it off when "
"the computer or cable does not provide RTS. A radio "
"that follows the frequency of another radio set to "
"F-TRANS also needs it off."))
tab.append(rs)
options = ["0", "100ms", "1000ms", "3000ms"]
rs = RadioSetting("cat_tot", "CAT Timeout",
RadioSettingValueList(options,
current_index=_settings.cat_tot))
rs.set_doc(_("Sets the time-out timer for computer control commands "
"arriving on the CAT jack. Command input that continues "
"for longer than this time is cut off. The radio shows "
"the shortest setting as 10 ms."))
tab.append(rs)
options = ["4800", "9600", "19200", "38400", "Data"]
rs = RadioSetting("catrate", "CAT rate",
RadioSettingValueList(options,
current_index=_settings.catrate))
rs.set_doc(_("Sets the baud rate of the CAT jack, which must match "
"the computer control software. Data turns the CAT jack "
"into an RTTY interface instead: RTTY software keys the "
"FSK shift through the serial data input and uses RTS as "
"PTT."))
tab.append(rs)
rs = RadioSetting("mem_grp", "Mem groups",
RadioSettingValueBoolean(_settings.mem_grp))
rs.set_doc(_("Divides the 500 regular memory channels into ten groups "
"of 50, numbered M-01-01 to M-10-50. Channels are then "
"selected and scanned one group at a time."))
tab.append(rs)
rs = RadioSetting("scn_res", "Resume scan (secs)",
RadioSettingValueInteger(0, 10, _settings.scn_res))
rs.set_doc(_("Sets when the scan resumes after it has paused on a "
"signal. 1 to 10 resumes after that many seconds, 0 "
"selects BUSY, which waits until the signal has gone. In "
"SSB, CW and SSB based data modes the scanner instead "
"steps slowly across a signal, giving you time to stop "
"it."))
tab.append(rs)
rs = RadioSetting("clk_sft", "CPU clock shift",
RadioSettingValueBoolean(_settings.clk_sft))
rs.set_doc(_("Shifts the frequency of the radio's CPU clock slightly. "
"Only use it to move an internally generated spurious "
"signal, or birdie, off a frequency you want to listen "
"on."))
tab.append(rs)
rs = RadioSetting("split", "TX/RX Frequency Split",
RadioSettingValueBoolean(_settings.split))
rs.set_doc(_("Turns on split frequency operation: the radio receives "
"on VFO-A and transmits on VFO-B, or the other way "
"round. The two VFOs may use different modes and even "
"different bands."))
tab.append(rs)
rs = RadioSetting("qspl_f", "Quick-Split freq offset (kHz)",
RadioSettingValueInteger(-20, 20, _settings.qspl_f))
rs.set_doc(_("Sets the offset Quick Split applies. Holding SPLIT/STEP "
"for a second sets the transmit VFO to the receive "
"frequency plus this offset, in the same mode as the "
"receive VFO, and turns split operation on."))
tab.append(rs)
rs = RadioSetting("emergen", "Alaska Emergency Mem 5167.5 kHz",
RadioSettingValueBoolean(_settings.emergen))
rs.set_doc(_("Enables transmit and receive on the Alaska emergency "
"channel, 5167.5 kHz, which then appears as MEM-EMG in "
"Memory mode. It may only be used by stations in or "
"within 92.6 km of Alaska, and only for emergency "
"communications involving the immediate protection of "
"life or property."))
tab.append(rs)
rs = RadioSetting("stby_beep", "PTT release 'Standby' beep",
RadioSettingValueBoolean(_settings.stby_beep))
rs.set_doc(_("Transmits a short beep when PTT is released, before the "
"radio returns to receive, to tell other stations that "
"your transmission has ended."))
tab.append(rs)
options = ["ATAS", "EXT ATU", "INT ATU", "INTRATU", "F-TRANS"]
rs = RadioSetting("tuner", "Antenna Tuner",
RadioSettingValueList(options,
current_index=_settings.tuner))
rs.set_doc(_("Selects what the TUNE button controls. ATAS is the "
"optional ATAS-100/-120 active-tuning antenna, EXT ATU "
"the optional FC-40 external tuner, and INT ATU the "
"built-in tuner acting on the transmit signal only, "
"while INTRATU puts the built-in tuner in the receive "
"path as well. F-TRANS instead sends this radio's "
"frequency to a second radio of the same model whenever "
"TUNE is pressed, for single operator two radio (SO2R) "
"operation."))
tab.append(rs)
rs = RadioSetting("rfpower", "RF power (watts)",
RadioSettingValueInteger(5, 100, _settings.rfpower))
rs.set_doc(_("Sets the transmit power in watts. Use the lowest power "
"that provides reliable communication, and a half to a "
"third of full power for data transmissions longer than "
"a few minutes."))
tab.append(rs) # End of _do_general_settings
def _do_cw_settings(self, cw): # - - - CW - - -
_settings = self._memobj.settings
_beacon = self._memobj.beacontext
rs = RadioSetting("cw_dly", "CW break-in delay (ms * 10)",
RadioSettingValueInteger(0, 300, _settings.cw_dly))
rs.set_doc(_("Sets how long the transmitter stays on after you stop "
"sending in semi break-in CW, in steps of 10 ms, so 25 "
"means 250 ms. A longer delay suits operators who pause "
"often while sending. The radio can also be set to FULL, "
"full break-in, which lets you hear between the "
"characters you send."))
cw.append(rs)
options = ["%i Hz" % i for i in range(400, 801, 100)]
rs = RadioSetting("cwpitch", "CW pitch",
RadioSettingValueList(options,
current_index=_settings.cwpitch))
rs.set_doc(_("Sets the pitch of the CW sidetone, which is also the "
"offset of the transmitted carrier and the center of the "
"CW receive filter. Tuning a station in until its tone "
"matches the sidetone puts you on its frequency."))
cw.append(rs)
rs = RadioSetting("cwspeed", "CW speed (wpm)",
RadioSettingValueInteger(4, 60, _settings.cwspeed))
rs.set_doc(_("Sets the sending speed of the built-in electronic keyer "
"in words per minute. The radio can also show it in "
"characters per minute, five times the wpm value. With a "
"CW QSK delay of 25 ms keep it at 50 wpm or below, and "
"with 30 ms at 42 wpm or below, or the radio cannot "
"transmit."))
cw.append(rs)
options = ["1:%1.1f" % (i / 10) for i in range(25, 46, 1)]
rs = RadioSetting("cwweigt", "CW weight",
RadioSettingValueList(options,
current_index=_settings.cwweigt))
rs.set_doc(_("Sets the dot to dash ratio of the built-in electronic "
"keyer. At 1:3.0, the standard weighting, a dash lasts "
"three times as long as a dot."))
cw.append(rs)
options = ["15ms", "20ms", "25ms", "30ms"]
rs = RadioSetting("cw_qsk", "CW delay before TX in QSK mode",
RadioSettingValueList(options,
current_index=_settings.cw_qsk))
rs.set_doc(_("Sets the delay between closing the key and the carrier "
"being transmitted in QSK operation with the built-in "
"keyer. At 25 ms keep the CW speed at 50 wpm or below, "
"and at 30 ms at 42 wpm or below, or the radio cannot "
"transmit."))
cw.append(rs)
rs = RadioSetting("cwstone_sgn", "CW sidetone volume Linked",
RadioSettingValueBoolean(_settings.cwstone_sgn))
rs.set_doc(_("Makes the CW sidetone volume follow the AF GAIN knob, "
"offset by the CW sidetone linked volume. When off, the "
"sidetone plays at the CW sidetone fixed volume whatever "
"the AF GAIN setting."))
cw.append(rs)
rs = RadioSetting("cwstone_lnk", "CW sidetone linked volume",
RadioSettingValueInteger(-50, 50,
_settings.cwstone_lnk))
rs.set_doc(_("Sets the sidetone volume relative to the receive audio "
"level set by AF GAIN, when CW sidetone volume Linked is "
"on. 0 makes the sidetone as loud as the received audio, "
"negative values make it quieter and positive values "
"louder."))
cw.append(rs)
rs = RadioSetting("cwstone_fix", "CW sidetone fixed volume",
RadioSettingValueInteger(0, 100,
_settings.cwstone_fix))
rs.set_doc(_("Sets the CW sidetone volume, from 0 to 100, used when "
"CW sidetone volume Linked is off."))
cw.append(rs)
options = ["Numeric", "Alpha", "Mixed"]
rs = RadioSetting("cwtrain", "CW Training mode",
RadioSettingValueList(options,
current_index=_settings.cwtrain))
rs.set_doc(_("Selects the characters the CW training feature sends: "
"digits only, letters only, or both mixed. Training "
"plays random five character Morse groups through the "
"sidetone at the CW speed, without transmitting."))
cw.append(rs)
rs = RadioSetting("cw_auto", "CW key jack- auto CW mode",
RadioSettingValueBoolean(_settings.cw_auto))
rs.set_doc(_("Lets the KEY jack send CW while the radio is in SSB "
"mode, so you can answer a station in CW without "
"changing mode. When off, the KEY jack only works in CW "
"mode."))
cw.append(rs)
options = ["Normal", "Reverse"]
rs = RadioSetting("cw_key", "CW paddle wiring",
RadioSettingValueList(options,
current_index=_settings.cw_key))
rs.set_doc(_("Swaps the dot and dash sides of a keyer paddle without "
"rewiring it, for example for a left-handed operator. "
"Normal sends dots from the plug tip and dashes from the "
"ring, Reverse the other way round."))
cw.append(rs)
rs = RadioSetting("beacon_time", "CW beacon Tx interval (secs)",
RadioSettingValueInteger(0, 255,
_settings.beacon_time))
rs.set_doc(_("Sets the pause in seconds between repeats of a CW "
"beacon message. 0 turns the repeat off, so that each "
"message is sent once per button press, as with a CW "
"message keyer."))
cw.append(rs)
tmp = self._chars2str(_beacon.t1, 40)
rs = RadioSetting("t1", "CW Beacon Line 1",
RadioSettingValueString(0, 40, tmp))
rs.set_apply_callback(self._my_str2ary, _beacon, "t1", 40)
rs.set_doc(_("CW beacon message 1, up to 40 characters, sent in CW "
"mode by the PLAY1 function of a programmable button. "
"The radio can send A to Z, 0 to 9, /, space and @."))
cw.append(rs)
tmp = self._chars2str(_beacon.t2, 40)
rs = RadioSetting("t2", "CW Beacon Line 2",
RadioSettingValueString(0, 40, tmp))
rs.set_apply_callback(self._my_str2ary, _beacon, "t2", 40)
rs.set_doc(_("CW beacon message 2, up to 40 characters, sent in CW "
"mode by the PLAY2 function of a programmable button. "
"The radio can send A to Z, 0 to 9, /, space and @."))
cw.append(rs)
tmp = self._chars2str(_beacon.t3, 40)
rs = RadioSetting("t3", "CW Beacon Line 3",
RadioSettingValueString(0, 40, tmp))
rs.set_apply_callback(self._my_str2ary, _beacon, "t3", 40)
rs.set_doc(_("CW beacon message 3, up to 40 characters, sent in CW "
"mode by the PLAY3 function of a programmable button. "
"The radio can send A to Z, 0 to 9, /, space and @."))
cw.append(rs) # END _do_cw_settings
def _do_panel_settings(self, pnlset): # - - - Panel settings
_settings = self._memobj.settings
bx = not _settings.amfmdial # Convert from Enable=0
rs = RadioSetting("amfmdial", "AM&FM Dial",
RadioSettingValueBoolean(bx))
rs.set_apply_callback(self._invert_me, _settings, "amfmdial")
rs.set_doc(_("Lets the MAIN DIAL knob change the frequency in AM and "
"FM modes. When off, the MAIN DIAL does not tune in "
"these modes."))
pnlset.append(rs)
options = ["440 Hz", "880 Hz", "1760 Hz"]
rs = RadioSetting("beepton", "Beep frequency",
RadioSettingValueList(options,
current_index=_settings.beepton))
rs.set_doc(_("Sets the pitch of the beep the radio sounds to confirm "
"button presses."))
pnlset.append(rs)
rs = RadioSetting("beepvol_sgn", "Beep volume Linked",
RadioSettingValueBoolean(_settings.beepvol_sgn))
rs.set_doc(_("Makes the beep volume follow the AF GAIN knob, offset "
"by the Linked beep volume. When off, the beep plays at "
"the Fixed beep volume whatever the AF GAIN setting."))
pnlset.append(rs)
rs = RadioSetting("beepvol_lnk", "Linked beep volume",
RadioSettingValueInteger(-50, 50,
_settings.beepvol_lnk))
rs.set_doc(_("Sets the beep volume relative to the receive audio "
"level set by AF GAIN, when Beep volume Linked is on. 0 "
"makes the beep as loud as the received audio, negative "
"values make it quieter and positive values louder."))
pnlset.append(rs)
rs = RadioSetting("beepvol_fix", "Fixed beep volume",
RadioSettingValueInteger(0, 100,
_settings.beepvol_fix))
rs.set_doc(_("Sets the beep volume, from 0 to 100, used when Beep "
"volume Linked is off."))
pnlset.append(rs)
rs = RadioSetting("cont", "LCD Contrast",
RadioSettingValueInteger(1, 24, _settings.cont))
rs.set_doc(_("Sets the contrast of the display, from 1 to 24. "
"Changing it here has not been seen to have a visible "
"effect on the radio."))
pnlset.append(rs)
rs = RadioSetting("dimmer", "LCD Dimmer",
RadioSettingValueInteger(1, 8, _settings.dimmer))
rs.set_doc(_("Sets the brightness of the display illumination, from 1 "
"to 8."))
pnlset.append(rs)
options = ["RF-Gain", "Squelch"]
rs = RadioSetting("sql_rfg", "Squelch/RF-Gain",
RadioSettingValueList(options,
current_index=_settings.sql_rfg))
rs.set_doc(_("Chooses what the SQL/RF GAIN knob controls. RF-Gain "
"sets the gain of the receiver's RF and IF stages, "
"Squelch mutes the receiver while no signal is present. "
"Scanning needs Squelch, because the scanner stops on "
"signals strong enough to open the squelch."))
pnlset.append(rs)
options = ["Frequencies", "Panel", "All"]
rs = RadioSetting("lockmod", "Lock Mode",
RadioSettingValueList(options,
current_index=_settings.lockmod))
rs.set_doc(_("Selects what the LOCK button locks. Frequencies locks "
"the controls that change the frequency, including the "
"microphone UP, DWN and FAST keys. Panel locks all front "
"panel keys and knobs, and All locks both the front "
"panel and the microphone keys. POWER and LOCK always "
"remain usable."))
pnlset.append(rs)
options = ["Dial", "SEL"]
rs = RadioSetting("clar_btn", "CLAR button control",
RadioSettingValueList(options,
current_index=_settings.clar_btn))
rs.set_doc(_("Chooses which knob sets the clarifier offset after CLAR "
"is pressed: the MAIN DIAL or the DSP/SEL knob. The "
"clarifier shifts the receive frequency without changing "
"the transmit frequency."))
pnlset.append(rs)
if _settings.dialstp_mode == 0: # AM/FM
options = ["SSB/CW:1Hz", "SSB/CW:10Hz", "SSB/CW:20Hz"]
else:
options = ["AM/FM:100Hz", "AM/FM:200Hz"]
rs = RadioSetting("dialstp", "Dial tuning step",
RadioSettingValueList(options,
current_index=_settings.dialstp))
rs.set_doc(_("Sets how far the frequency moves for each step of the "
"MAIN DIAL knob: 1, 10 or 20 Hz in SSB and CW, 100 or "
"200 Hz in AM and FM."))
pnlset.append(rs)
options = ["0.5secs", "1.0secs", "1.5secs", "2.0secs"]
rs = RadioSetting("keyhold", "Buttons hold-to-activate time",
RadioSettingValueList(options,
current_index=_settings.keyhold))
rs.set_doc(_("Sets how long a button has to be held to activate its "
"press and hold function. A shorter press activates the "
"button's normal function."))
pnlset.append(rs)
rs = RadioSetting("m_tune", "Memory tune",
RadioSettingValueBoolean(_settings.m_tune))
rs.set_doc(_("Allows tuning away from a recalled memory or Home "
"channel with the MAIN DIAL, as with a VFO. The stored "
"channel does not change unless you write the new "
"settings to it."))
pnlset.append(rs)
rs = RadioSetting("peakhold", "S-Meter display hold (1sec)",
RadioSettingValueBoolean(_settings.peakhold))
rs.set_doc(_("Holds the peak S-meter reading for one second, which "
"makes the meter easier to read."))
pnlset.append(rs)
options = ["CW Sidetone", "CW Speed", "100 kHz step", "1 MHz Step",
"Mic Gain", "RF Power"]
rs = RadioSetting("seldial", "SEL dial 2nd function (push)",
RadioSettingValueList(options,
current_index=_settings.seldial))
rs.set_doc(_("Assigns the second function the DSP/SEL knob takes on "
"after it is pressed briefly: adjusting the CW sidetone "
"volume, the CW keyer speed, the microphone gain or the "
"RF power, or tuning in 100 kHz or 1 MHz steps."))
pnlset.append(rs)
# End _do_panel_settings
def _do_panel_buttons(self, pnlcfg): # - - - Current Panel Config
_settings = self._memobj.settings
rs = RadioSetting("pnl_cs", "C.S. Function",
RadioSettingValueList(self.FUNC_LIST,
current_index=_settings.pnl_cs))
rs.set_doc(_("Assigns one of the radio's functions to the front panel "
"C.S/VOICE button, for example MONI to listen to your "
"own transmission, SWR to transmit a 10 W carrier and "
"measure the antenna SWR, or PLAY1 to send CW beacon "
"message 1. Choices named after a front panel button act "
"like that button."))
pnlcfg.append(rs)
rs = RadioSetting("nb", "Noise blanker",
RadioSettingValueBoolean(_settings.nb))
rs.set_doc(_("Turns on the IF noise blanker, which reduces short "
"pulses of noise such as those from vehicle ignition "
"systems."))
pnlcfg.append(rs)
# The old FT450 uses different codes for the AGC setting, and only uses the
# last 3 bits of the AGC definition. The two upper bits appear to always be x"11"
# We will mask this in the routine that processes the agc value
if self.FT450:
options = ["Auto", "Fast", "Slow", "UNK3", "UNK4", "UNK5", "OFF", "UNK7"]
# mask off the two upper bits of the agc settings value
FT450agc = _settings.agc & int('0x7',16)
rs = RadioSetting("agc", "AGC",
RadioSettingValueList(options,
current_index=FT450agc))
else:
options = ["Auto", "Fast", "Slow", "Auto/Fast", "Auto/Slow", "?5?"]
rs = RadioSetting("agc", "AGC",
RadioSettingValueList(options,
current_index=_settings.agc))
rs.set_doc(_("Selects how quickly the automatic gain control lets the "
"receiver recover after a strong signal. Fast suits CW "
"and data, Slow suits SSB and AM, and Auto picks Slow "
"for SSB and AM and Fast for the other modes. With AGC "
"off the S-meter no longer moves and strong signals are "
"likely to distort."))
pnlcfg.append(rs)
rs = RadioSetting("keyer", "Keyer",
RadioSettingValueBoolean(_settings.keyer))
rs.set_doc(_("Turns on the built-in electronic CW keyer, for a keyer "
"paddle plugged into the KEY jack. Turn it off to use a "
"straight key or an external keyer."))
pnlcfg.append(rs)
rs = RadioSetting("fast", "Fast step",
RadioSettingValueBoolean(_settings.fast))
rs.set_doc(_("Speeds up tuning: the MAIN DIAL knob tunes ten times "
"faster and the DSP/SEL knob twice as fast."))
pnlcfg.append(rs)
rs = RadioSetting("lock", "Lock (per Lock Mode)",
RadioSettingValueBoolean(_settings.lock))
rs.set_doc(_("Locks the controls selected by Lock Mode against "
"accidental changes, as the LOCK button does."))
pnlcfg.append(rs)
options = ["PO", "ALC", "SWR"]
rs = RadioSetting("mtr_mode", "S-Meter mode",
RadioSettingValueList(options,
current_index=_settings.mtr_mode))
rs.set_doc(_("Selects what the meter shows while transmitting: PO the "
"average power output, ALC the relative ALC voltage, or "
"SWR the standing wave ratio of the antenna. While "
"receiving it always shows the signal strength."))
pnlcfg.append(rs)
# End _do_panel_Buttons
def _do_vox_settings(self, voxdat): # - - VOX and DATA Settings
_settings = self._memobj.settings
rs = RadioSetting("vox_dly", "VOX delay (x 100 ms)",
RadioSettingValueInteger(1, 30, _settings.vox_dly))
rs.set_doc(_("Sets how long the transmitter stays on after you stop "
"speaking in VOX operation, in steps of 100 ms, so 5 "
"means 500 ms. A longer delay keeps short pauses in your "
"speech from switching the radio back to receive."))
voxdat.append(rs)
rs = RadioSetting("vox_gain", "VOX Gain",
RadioSettingValueInteger(0, 100,
_settings.vox_gain))
rs.set_doc(_("Sets the VOX sensitivity. Too sensitive a setting keys "
"the transmitter on the noise around the radio, too "
"insensitive a setting fails to pick up your voice. 1 is "
"the least sensitive setting and 100 the most."))
voxdat.append(rs)
rs = RadioSetting("dig_vox", "Digital VOX Gain",
RadioSettingValueInteger(0, 100,
_settings.dig_vox))
rs.set_doc(_("Sets the VOX sensitivity used in DIG mode, so that the "
"audio from the data equipment switches the transmitter "
"on without a PTT line. 0 turns VOX off in DIG mode."))
voxdat.append(rs)
rs = RadioSetting("d_disp", "User-L/U freq offset (Hz)",
RadioSettingValueInteger(-3000, 30000,
_settings.d_disp, 10))
rs.set_doc(_("Shifts the frequency shown on the display in the USER-L "
"and USER-U data modes by this many hertz. The radio "
"accepts -3000 to +3000 Hz."))
voxdat.append(rs)
options = ["170 Hz", "200 Hz", "425 Hz", "850 Hz"]
rs = RadioSetting("rty_sft", "RTTY FSK Freq Shift",
RadioSettingValueList(options,
current_index=_settings.rty_sft))
rs.set_doc(_("Sets the frequency shift between the mark and space "
"tones in FSK RTTY. Most amateur RTTY uses 170 Hz."))
voxdat.append(rs)
options = ["1275 Hz", "2125 Hz"]
rs = RadioSetting("rty_ton", "RTTY FSK Mark tone",
RadioSettingValueList(options,
current_index=_settings.rty_ton))
rs.set_doc(_("Selects the mark tone for RTTY: 2125 Hz for high tone "
"operation or 1275 Hz for low tone operation."))
voxdat.append(rs)
options = ["Normal", "Reverse"]
rs = RadioSetting("rtyrpol", "RTTY Mark/Space RX polarity",
RadioSettingValueList(options,
current_index=_settings.rtyrpol))
rs.set_doc(_("Swaps the mark and space tones when receiving RTTY. Try "
"Reverse if a station with a good signal cannot be "
"decoded."))
voxdat.append(rs)
rs = RadioSetting("rtytpol", "RTTY Mark/Space TX polarity",
RadioSettingValueList(options,
current_index=_settings.rtytpol))
rs.set_doc(_("Swaps the mark and space tones of your transmitted RTTY "
"signal."))
voxdat.append(rs)
# End _do_vox_settings
def _do_mic_settings(self, mic): # - - MIC Settings
_settings = self._memobj.settings
rs = RadioSetting("mic_eq", "Mic Equalizer",
RadioSettingValueInteger(0, 9, _settings.mic_eq))
rs.set_doc(_("Selects one of ten DSP equalizer curves for your "
"transmitted voice. 0 is flat, 1 to 3 reduce the low, "
"middle or high frequencies, and 4 to 6 boost the high, "
"middle or low frequencies. 7 boosts the lows and "
"reduces the highs, 8 reduces the lows and boosts the "
"middle, and 9 reduces the lows and boosts the highs."))
mic.append(rs)
options = ["Low", "Normal", "High"]
rs = RadioSetting("micgain", "Mic Gain",
RadioSettingValueList(options,
current_index=_settings.micgain))
rs.set_doc(_("Sets the microphone gain for SSB, AM and FM. Use Low "
"with a microphone that has a high output, Normal with "
"the supplied hand microphone and High with a microphone "
"that has a low output. The speech processor is off at "
"Low and on at the other settings."))
mic.append(rs)
rs = RadioSetting("micscan", "Mic scan enabled",
RadioSettingValueBoolean(_settings.micscan))
rs.set_doc(_("When on, holding the microphone UP or DWN key for about "
"a second starts a scan that continues after the key is "
"released. When off, the radio only scans while the key "
"is held down."))
mic.append(rs)
rs = RadioSetting("pm_dwn", "Mic Down button function",
RadioSettingValueList(self.FUNC_LIST,
current_index=_settings.pm_dwn))
rs.set_doc(_("Assigns one of the radio's functions to the DWN key of "
"the microphone. DOWN, its normal function, steps the "
"frequency or memory channel down."))
mic.append(rs)
rs = RadioSetting("pm_fst", "Mic Fast button function",
RadioSettingValueList(self.FUNC_LIST,
current_index=_settings.pm_fst))
rs.set_doc(_("Assigns one of the radio's functions to the FST key of "
"the microphone. FAST, its normal function, speeds up "
"tuning like the front panel FAST button."))
mic.append(rs)
rs = RadioSetting("pm_up", "Mic Up button function",
RadioSettingValueList(self.FUNC_LIST,
current_index=_settings.pm_up))
rs.set_doc(_("Assigns one of the radio's functions to the UP key of "
"the microphone. UP, its normal function, steps the "
"frequency or memory channel up."))
mic.append(rs)
# End _do_mic_settings
def _do_mymodes_settings(self, mymodes): # - - MYMODES
_settings = self._memobj.settings # Inverted Logic requires callback
doc = _("Includes %s in the modes the MODE buttons step through. Turn "
"off the modes you never use to reach the others with fewer "
"presses.")
bx = not _settings.mym_lsb
rs = RadioSetting("mym_lsb", "LSB", RadioSettingValueBoolean(bx))
rs.set_apply_callback(self._invert_me, _settings, "mym_lsb")
rs.set_doc(doc % "LSB")
mymodes.append(rs)
bx = not _settings.mym_usb
rs = RadioSetting("mym_usb", "USB", RadioSettingValueBoolean(bx))
rs.set_apply_callback(self._invert_me, _settings, "mym_usb")
rs.set_doc(doc % "USB")
mymodes.append(rs)
bx = not _settings.mym_cw
rs = RadioSetting("mym_cw", "CW", RadioSettingValueBoolean(bx))
rs.set_apply_callback(self._invert_me, _settings, "mym_cw")
rs.set_doc(doc % "CW")
mymodes.append(rs)
bx = not _settings.mym_am
rs = RadioSetting("mym_am", "AM", RadioSettingValueBoolean(bx))
rs.set_apply_callback(self._invert_me, _settings, "mym_am")
rs.set_doc(doc % "AM")
mymodes.append(rs)
bx = not _settings.mym_fm
rs = RadioSetting("mym_fm", "FM", RadioSettingValueBoolean(bx))
rs.set_apply_callback(self._invert_me, _settings, "mym_fm")
rs.set_doc(doc % "FM")
mymodes.append(rs)
bx = not _settings.mym_data
rs = RadioSetting("mym_data", "DATA", RadioSettingValueBoolean(bx))
rs.set_apply_callback(self._invert_me, _settings, "mym_data")
rs.set_doc(doc % "DATA")
mymodes.append(rs)
# End _do_mymodes_settings
def _do_mybands_settings(self, mybands): # - - MYBANDS Settings
_settings = self._memobj.settings # Inverted Logic requires callback
doc = _("Includes the %s band in the bands the BAND buttons step "
"through. Turn off the bands you never use to reach the "
"others with fewer presses.")
bx = not _settings.myb_1_8
rs = RadioSetting("myb_1_8", "1.8 MHz", RadioSettingValueBoolean(bx))
rs.set_apply_callback(self._invert_me, _settings, "myb_1_8")
rs.set_doc(doc % "1.8 MHz")
mybands.append(rs)
bx = not _settings.myb_3_5
rs = RadioSetting("myb_3_5", "3.5 MHz", RadioSettingValueBoolean(bx))
rs.set_apply_callback(self._invert_me, _settings, "myb_3_5")
rs.set_doc(doc % "3.5 MHz")
mybands.append(rs)
bx = not _settings.myb_7
rs = RadioSetting("myb_7", "7 MHz", RadioSettingValueBoolean(bx))
rs.set_apply_callback(self._invert_me, _settings, "myb_7")
rs.set_doc(doc % "7 MHz")
mybands.append(rs)
bx = not _settings.myb_10
rs = RadioSetting("myb_10", "10 MHz", RadioSettingValueBoolean(bx))
rs.set_apply_callback(self._invert_me, _settings, "myb_10")
rs.set_doc(doc % "10 MHz")
mybands.append(rs)
bx = not _settings.myb_14
rs = RadioSetting("myb_14", "14 MHz", RadioSettingValueBoolean(bx))
rs.set_apply_callback(self._invert_me, _settings, "myb_14")
rs.set_doc(doc % "14 MHz")
mybands.append(rs)
bx = not _settings.myb_18
rs = RadioSetting("myb_18", "18 MHz", RadioSettingValueBoolean(bx))
rs.set_apply_callback(self._invert_me, _settings, "myb_18")
rs.set_doc(doc % "18 MHz")
mybands.append(rs)
bx = not _settings.myb_21
rs = RadioSetting("myb_21", "21 MHz", RadioSettingValueBoolean(bx))
rs.set_apply_callback(self._invert_me, _settings, "myb_21")
rs.set_doc(doc % "21 MHz")
mybands.append(rs)
bx = not _settings.myb_24
rs = RadioSetting("myb_24", "24 MHz", RadioSettingValueBoolean(bx))
rs.set_apply_callback(self._invert_me, _settings, "myb_24")
rs.set_doc(doc % "24 MHz")
mybands.append(rs)
bx = not _settings.myb_28
rs = RadioSetting("myb_28", "28 MHz", RadioSettingValueBoolean(bx))
rs.set_apply_callback(self._invert_me, _settings, "myb_28")
rs.set_doc(doc % "28 MHz")
mybands.append(rs)
bx = not _settings.myb_50
rs = RadioSetting("myb_50", "50 MHz", RadioSettingValueBoolean(bx))
rs.set_apply_callback(self._invert_me, _settings, "myb_50")
rs.set_doc(doc % "50 MHz")
mybands.append(rs)
# End _do_mybands_settings
def set_settings(self, settings):
_settings = self._memobj.settings
_mem = self._memobj
for element in settings:
if not isinstance(element, RadioSetting):
self.set_settings(element)
continue
else:
try:
name = element.get_name()
if "." in name:
bits = name.split(".")
obj = self._memobj
for bit in bits[: -1]:
if "/" in bit:
bit, index = bit.split("/", 1)
index = int(index)
obj = getattr(obj, bit)[index]
else:
obj = getattr(obj, bit)
setting = bits[-1]
else:
obj = _settings
setting = element.get_name()
if element.has_apply_callback():
LOG.debug("Using apply callback")
element.run_apply_callback()
elif element.value.get_mutable():
LOG.debug("Setting %s = %s" % (setting, element.value))
setattr(obj, setting, element.value)
except Exception:
LOG.debug(element.get_name())
raise
def validate_memory(self, mem):
msgs = super().validate_memory(mem)
if 'bpfilter' in mem.extra:
_, options = filters_for_mode(mem.mode)
bpfilter = str(mem.extra['bpfilter'].value)
if bpfilter not in options:
msgs.append(chirp_common.ValidationWarning(
('Filter width %s is not allowed for mode %s - '
'the closest suitable option %s will be used') % (
bpfilter, mem.mode,
closest_filter(options, bpfilter))))
return msgs
@directory.register
class FT450Radio(FT450DRadio):
"""Yaesu FT-450"""
FT450 = True
MODEL = "FT-450"
@classmethod
def match_model(cls, filedata, filename):
return False
chirp-20261030/chirp/drivers/ft50.py 0000644 0001750 0001750 00000051141 15154752252 015757 0 ustar hibby hibby # Copyright 2011 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 time
import logging
import re
from chirp.drivers import yaesu_clone
from chirp import chirp_common, directory, errors, bitwise, util
from chirp.settings import RadioSetting, RadioSettingGroup, \
RadioSettingValueInteger, RadioSettingValueList, \
RadioSettingValueBoolean, RadioSettingValueString, \
RadioSettings
LOG = logging.getLogger(__name__)
MEM_FORMAT = """
struct flag_struct {
u8 unknown1f:5,
skip:1,
mask:1,
used:1;
};
struct mem_struct {
u8 showname:1,
unknown1:3,
unknown2:2,
unknown3:2;
u8 ishighpower:1,
power:2,
unknown4:1,
tuning_step:4;
u8 codememno:4,
codeorpage:2,
duplex:2;
u8 tmode:2,
tone:6;
u8 unknown5:1,
dtcs:7;
u8 unknown6:6,
mode:2;
bbcd freq[3];
bbcd offset[3];
u8 name[4];
};
#seekto 0x000C;
struct {
u8 extendedrx_flg; // Seems to be set to 03 when extended rx is enabled
u8 extendedrx; // Seems to be set to 01 when extended rx is enabled
} extendedrx_struct; // UNFINISHED!!
#seekto 0x001A;
struct flag_struct flag[100];
#seekto 0x079C;
struct flag_struct flag_repeat[100];
#seekto 0x00AA;
struct mem_struct memory[100];
struct mem_struct special[11];
#seekto 0x08C7;
struct {
u8 sub_display;
u8 unknown1s;
u8 apo;
u8 timeout;
u8 lock;
u8 rxsave;
u8 lamp;
u8 bell;
u8 cwid[16];
u8 unknown2s;
u8 artsmode;
u8 artsbeep;
u8 unknown3s;
u8 unknown4s;
struct {
u8 header[3];
u8 mem_num;
u8 digits[16];
} autodial[8];
struct {
u8 header[3];
u8 mem_num;
u8 digits[32];
} autodial9_ro;
bbcd pagingcodec_ro[2];
bbcd pagingcodep[2];
struct {
bbcd digits[2];
} pagingcode[6];
u8 code_dec_c_en:1,
code_dec_p_en:1,
code_dec_1_en:1,
code_dec_2_en:1,
code_dec_3_en:1,
code_dec_4_en:1,
code_dec_5_en:1,
code_dec_6_en:1;
u8 pagingspeed;
u8 pagingdelay;
u8 pagingbell;
u8 paginganswer;
#seekto 0x0E30;
u8 squelch; // squelch
u8 unknown0c;
u8 rptl:1, // repeater input tracking
amod:1, // auto mode
scnl:1, // scan lamp
resm:1, // scan resume mode 0=5sec, 1=carr
ars:1, // automatic repeater shift
keybeep:1, // keypad beep
lck:1, // lock
unknown1c:1;
u8 lgt:1,
pageamsg:1,
unknown2c:1,
bclo:1, // Busy channel lock out
unknown3c:2,
cwid_en:1, // CWID off/on
tsav:1; // TX save
u8 unknown4c:4,
artssped:1, // ARTS/SPED: 0=15s, 1=25s
unknown5c:1,
rvhm:1, // RVHM: 0=home, 1=rev
mon:1; // MON: 0=mon, 1=tcal
} settings;
#seekto 0x080E;
struct mem_struct vfo_mem[10];
"""
# 10 VFO memories: A145, A220, A380, A430, A800,
# B145, B220, B380, B430, B800
DUPLEX = ["", "-", "+"]
MODES = ["FM", "AM", "WFM"]
SKIP_VALUES = ["", "S"]
TMODES = ["", "Tone", "TSQL", "DTCS"]
TUNING_STEPS = [5.0, 10.0, 12.5, 15.0, 20.0, 25.0, 50.0]
TONES = list(chirp_common.OLD_TONES)
# CHARSET = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ ()+-=*/???|0123456789"
# the = displays as an underscored dash on radio
# the first ? is an uppercase delta - \xA7
# the second ? is an uppercase gamma - \xD1
# the third ? is an uppercase sigma - \xCF
NUMERIC_CHARSET = list("0123456789")
CHARSET = [str(x) for x in range(0, 10)] + \
[chr(x) for x in range(ord("A"), ord("Z")+1)] + \
list(" ()+-=*/" + ("\x00" * 3) + "|") + NUMERIC_CHARSET
DTMFCHARSET = NUMERIC_CHARSET + list("ABCD*#")
POWER_LEVELS = [chirp_common.PowerLevel("Hi", watts=5.0),
chirp_common.PowerLevel("L3", watts=2.5),
chirp_common.PowerLevel("L2", watts=1.0),
chirp_common.PowerLevel("L1", watts=0.1)]
SPECIALS = ["L1", "U1", "L2", "U2", "L3", "U3", "L4", "U4", "L5", "U5", "UNK"]
@directory.register
class FT50Radio(yaesu_clone.YaesuCloneModeRadio):
"""Yaesu FT-50"""
BAUD_RATE = 9600
VENDOR = "Yaesu"
MODEL = "FT-50"
_model = ""
_memsize = 3723
_block_lengths = [10, 16, 112, 16, 16, 1776, 1776, 1]
# _block_delay = 0.15
_block_size = 8
@classmethod
def get_prompts(cls):
rp = chirp_common.RadioPrompts()
rp.pre_download = _(
"1. Turn radio off.\n"
"2. Connect cable to MIC/SP jack.\n"
"3. Press and hold [PTT] & Knob while turning the\n"
" radio on.\n"
"4. After clicking OK , press the [PTT] switch to send"
" image.\n")
rp.pre_upload = _(
"1. Turn radio off.\n"
"2. Connect cable to MIC/SP jack.\n"
"3. Press and hold [PTT] & Knob while turning the\n"
" radio on.\n"
"4. Press the [MONI] switch (\"WAIT\" will appear on the LCD).\n"
"5. Press OK.\n")
return rp
def get_features(self):
rf = chirp_common.RadioFeatures()
rf.memory_bounds = (1, 100)
rf.valid_duplexes = DUPLEX
rf.valid_tmodes = TMODES
rf.valid_power_levels = POWER_LEVELS
rf.valid_tuning_steps = TUNING_STEPS
rf.valid_power_levels = POWER_LEVELS
rf.valid_characters = "".join(CHARSET)
rf.valid_name_length = 4
rf.valid_modes = MODES
# Specials not yet implemented
# rf.valid_special_chans = SPECIALS
rf.valid_bands = [(76000000, 200000000),
(300000000, 540000000),
(590000000, 999000000)]
# rf.can_odd_split = True
rf.has_ctone = False
rf.has_bank = False
rf.has_settings = True
rf.has_dtcs_polarity = False
return rf
def _checksums(self):
return [yaesu_clone.YaesuChecksum(0x0000, 0xE89)]
def process_mmap(self):
self._memobj = bitwise.parse(MEM_FORMAT, self._mmap)
def get_raw_memory(self, number):
return repr(self._memobj.memory[number - 1])
def get_memory(self, number):
mem = chirp_common.Memory()
_mem = self._memobj.memory[number-1]
_flg = self._memobj.flag[number-1]
mem.number = number
# if not _flg.visible:
# mem.empty = True
if not _flg.used:
mem.empty = True
return mem
for i in _mem.name:
mem.name += CHARSET[i & 0x7F]
mem.name = mem.name.rstrip()
mem.freq = chirp_common.fix_rounded_step(int(_mem.freq) * 1000)
mem.duplex = DUPLEX[_mem.duplex]
mem.offset = chirp_common.fix_rounded_step(int(_mem.offset) * 1000)
mem.rtone = mem.ctone = TONES[_mem.tone]
mem.tmode = TMODES[_mem.tmode]
mem.mode = MODES[_mem.mode]
mem.tuning_step = TUNING_STEPS[_mem.tuning_step]
mem.dtcs = chirp_common.DTCS_CODES[_mem.dtcs]
# Power is stored as 2 bits to describe the 3 low power levels
# High power is determined by a different bit.
if not _mem.ishighpower:
mem.power = POWER_LEVELS[3 - _mem.power]
else:
mem.power = POWER_LEVELS[0]
mem.skip = SKIP_VALUES[_flg.skip]
return mem
def set_memory(self, mem):
_mem = self._memobj.memory[mem.number-1]
_flg = self._memobj.flag[mem.number-1]
_flg_repeat = self._memobj.flag_repeat[mem.number-1]
if mem.empty:
_flg.used = False
return
if (len(mem.name) == 0):
_mem.name = [0x24] * 4
_mem.showname = 0
else:
_mem.showname = 1
for i in range(0, 4):
_mem.name[i] = CHARSET.index(mem.name.ljust(4)[i])
_mem.freq = int(mem.freq / 1000)
_mem.duplex = DUPLEX.index(mem.duplex)
_mem.offset = int(mem.offset / 1000)
_mem.mode = MODES.index(mem.mode)
_mem.tuning_step = TUNING_STEPS.index(mem.tuning_step)
if mem.power:
if (mem.power == POWER_LEVELS[0]):
# low power level is not changed when high power is selected
_mem.ishighpower = 0x01
if (_mem.power == 3):
# Set low power to L3 (0x02) if it is
# set to 3 (new object default)
LOG.debug("SETTING DEFAULT?")
_mem.power = 0x02
else:
_mem.ishighpower = 0x00
_mem.power = 3 - POWER_LEVELS.index(mem.power)
else:
_mem.ishighpower = 0x01
_mem.power = 0x02
_mem.tmode = TMODES.index(mem.tmode)
try:
_mem.tone = TONES.index(mem.rtone)
except ValueError:
raise errors.UnsupportedToneError(
("This radio does not support tone %s" % mem.rtone))
_mem.dtcs = chirp_common.DTCS_CODES.index(mem.dtcs)
_flg.skip = SKIP_VALUES.index(mem.skip)
# initialize new channel to safe defaults
if not mem.empty and not _flg.used:
_flg.used = True
_flg.mask = True # Mask = True to be visible on radio
_mem.unknown1 = 0x00
_mem.unknown2 = 0x00
_mem.unknown3 = 0x00
_mem.unknown4 = 0x00
_mem.unknown5 = 0x00
_mem.unknown6 = 0x00
_mem.codememno = 0x02 # Not implemented in chirp
_mem.codeorpage = 0x00 # Not implemented in chirp
# Duplicate flags to repeated part in memory
_flg_repeat.skip = _flg.skip
_flg_repeat.mask = _flg.mask
_flg_repeat.used = _flg.used
def _decode_cwid(self, inarr):
LOG.debug("@_decode_chars, type: %s" % type(inarr))
LOG.debug(inarr)
outstr = ""
for i in inarr:
if i == 0xFF:
break
outstr += CHARSET[i & 0x7F]
LOG.debug(outstr)
return outstr.rstrip()
def _encode_cwid(self, instr, length=16):
LOG.debug("@_encode_chars, type: %s" % type(instr))
LOG.debug(instr)
outarr = []
instr = str(instr)
for i in range(0, length):
if i < len(instr):
outarr.append(CHARSET.index(instr[i]))
else:
outarr.append(0xFF)
return outarr
def get_settings(self):
_settings = self._memobj.settings
basic = RadioSettingGroup("basic", "Basic")
dtmf = RadioSettingGroup("dtmf", "DTMF Code & Paging")
arts = RadioSettingGroup("arts", "ARTS")
autodial = RadioSettingGroup("autodial", "AutoDial")
top = RadioSettings(basic, autodial, arts, dtmf)
rs = RadioSetting(
"squelch", "Squelch",
RadioSettingValueInteger(0, 15, _settings.squelch))
basic.append(rs)
rs = RadioSetting(
"keybeep", "Keypad Beep",
RadioSettingValueBoolean(_settings.keybeep))
basic.append(rs)
rs = RadioSetting(
"scnl", "Scan Lamp",
RadioSettingValueBoolean(_settings.scnl))
basic.append(rs)
options = ["off", "30m", "1h", "3h", "5h", "8h"]
rs = RadioSetting(
"apo", "APO time (hrs)",
RadioSettingValueList(options, current_index=_settings.apo))
basic.append(rs)
options = ["off", "1m", "2.5m", "5m", "10m"]
rs = RadioSetting(
"timeout", "Time Out Timer",
RadioSettingValueList(
options, current_index=_settings.timeout))
basic.append(rs)
options = ["key", "dial", "key+dial", "ptt",
"key+ptt", "dial+ptt", "all"]
rs = RadioSetting(
"lock", "Lock mode",
RadioSettingValueList(options, current_index=_settings.lock))
basic.append(rs)
options = ["off", "0.2", "0.3", "0.5", "1.0", "2.0"]
rs = RadioSetting(
"rxsave", "RX Save (sec)",
RadioSettingValueList(options, current_index=_settings.rxsave))
basic.append(rs)
options = ["5sec", "key", "tgl"]
rs = RadioSetting(
"lamp", "Lamp mode",
RadioSettingValueList(options, current_index=_settings.lamp))
basic.append(rs)
options = ["off", "1", "3", "5", "8", "rpt"]
rs = RadioSetting(
"bell", "Bell Repetitions",
RadioSettingValueList(options, current_index=_settings.bell))
basic.append(rs)
rs = RadioSetting(
"cwid_en", "CWID Enable",
RadioSettingValueBoolean(_settings.cwid_en))
arts.append(rs)
cwid = RadioSettingValueString(
0, 16, self._decode_cwid(_settings.cwid.get_value()))
cwid.set_charset(CHARSET)
rs = RadioSetting("cwid", "CWID", cwid)
arts.append(rs)
options = ["off", "rx", "tx", "trx"]
rs = RadioSetting(
"artsmode", "ARTS Mode",
RadioSettingValueList(
options, current_index=_settings.artsmode))
arts.append(rs)
options = ["off", "in range", "always"]
rs = RadioSetting(
"artsbeep", "ARTS Beep",
RadioSettingValueList(
options, current_index=_settings.artsbeep))
arts.append(rs)
for i in range(0, 8):
dialsettings = _settings.autodial[i]
dialstr = ""
for c in dialsettings.digits:
if c < len(DTMFCHARSET):
dialstr += DTMFCHARSET[c]
dialentry = RadioSettingValueString(0, 16, dialstr)
dialentry.set_charset(DTMFCHARSET + list(" "))
rs = RadioSetting("autodial" + str(i+1),
"AutoDial " + str(i+1), dialentry)
autodial.append(rs)
dialstr = ""
for c in _settings.autodial9_ro.digits:
if c < len(DTMFCHARSET):
dialstr += DTMFCHARSET[c]
dialentry = RadioSettingValueString(0, 32, dialstr)
dialentry.set_mutable(False)
rs = RadioSetting("autodial9_ro", "AutoDial 9 (read only)", dialentry)
autodial.append(rs)
options = ["50ms", "100ms"]
rs = RadioSetting(
"pagingspeed", "Paging Speed",
RadioSettingValueList(
options, current_index=_settings.pagingspeed))
dtmf.append(rs)
options = ["250ms", "450ms", "750ms", "1000ms"]
rs = RadioSetting(
"pagingdelay", "Paging Delay",
RadioSettingValueList(
options, current_index=_settings.pagingdelay))
dtmf.append(rs)
options = ["off", "1", "3", "5", "8", "rpt"]
rs = RadioSetting(
"pagingbell", "Paging Bell Repetitions",
RadioSettingValueList(
options, current_index=_settings.pagingbell))
dtmf.append(rs)
options = ["off", "ans", "for"]
rs = RadioSetting(
"paginganswer", "Paging Answerback",
RadioSettingValueList(options,
current_index=_settings.paginganswer))
dtmf.append(rs)
rs = RadioSetting(
"code_dec_c_en", "Paging Code C Decode Enable",
RadioSettingValueBoolean(_settings.code_dec_c_en))
dtmf.append(rs)
_str = str(_settings.pagingcodec_ro)
code = RadioSettingValueString(0, 3, _str)
code.set_charset(NUMERIC_CHARSET + list(" "))
code.set_mutable(False)
rs = RadioSetting("pagingcodec_ro", "Paging Code C (read only)", code)
dtmf.append(rs)
rs = RadioSetting(
"code_dec_p_en", "Paging Code P Decode Enable",
RadioSettingValueBoolean(_settings.code_dec_p_en))
dtmf.append(rs)
_str = str(_settings.pagingcodep)
code = RadioSettingValueString(0, 3, _str)
code.set_charset(NUMERIC_CHARSET + list(" "))
rs = RadioSetting("pagingcodep", "Paging Code P", code)
dtmf.append(rs)
for i in range(0, 6):
num = str(i+1)
name = "code_dec_" + num + "_en"
rs = RadioSetting(
name, "Paging Code " + num + " Decode Enable",
RadioSettingValueBoolean(getattr(_settings, name)))
dtmf.append(rs)
_str = str(_settings.pagingcode[i].digits)
code = RadioSettingValueString(0, 3, _str)
code.set_charset(NUMERIC_CHARSET + list(" "))
rs = RadioSetting("pagingcode" + num, "Paging Code " + num, code)
dtmf.append(rs)
return top
def set_settings(self, uisettings):
for element in uisettings:
if not isinstance(element, RadioSetting):
self.set_settings(element)
continue
if not element.changed():
continue
try:
setting = element.get_name()
_settings = self._memobj.settings
if re.match(r'autodial\d', setting):
# set autodial fields
dtmfstr = str(element.value).strip()
newval = []
for i in range(0, 16):
if i < len(dtmfstr):
newval.append(DTMFCHARSET.index(dtmfstr[i]))
else:
newval.append(0xFF)
LOG.debug(newval)
idx = int(setting[-1:]) - 1
_settings = self._memobj.settings.autodial[idx]
_settings.digits = newval
continue
if (setting == "pagingcodep"):
_settings.pagingcodep = int(element.value)
continue
if re.match(r'pagingcode\d', setting):
idx = int(setting[-1:]) - 1
_settings.pagingcode[idx].digits = int(element.value)
continue
newval = element.value
oldval = getattr(_settings, setting)
if setting == "cwid":
newval = self._encode_cwid(newval)
LOG.debug("Setting %s(%s) <= %s" % (setting, oldval, newval))
setattr(_settings, setting, newval)
except Exception:
LOG.debug(element.get_name())
raise
@classmethod
def match_model(cls, filedata, filename):
return len(filedata) == cls._memsize
def sync_out(self):
self.update_checksums()
return _clone_out(self)
def _clone_out(radio):
try:
return __clone_out(radio)
except Exception as e:
raise errors.RadioError("Failed to communicate with the radio: %s" % e)
def __clone_out(radio):
pipe = radio.pipe
pipe.timeout = 1
block_lengths = radio._block_lengths
total_written = 0
def _status():
status = chirp_common.Status()
status.msg = "Cloning to radio"
status.max = sum(block_lengths)
status.cur = total_written
radio.status_fn(status)
start = time.time()
blocks = 0
pos = 0
for block in radio._block_lengths:
blocks += 1
data = radio.get_mmap()[pos:pos + block]
# LOG.debug("Sending block: %s" % util.hexprint(data))
recvd = b""
# Radio echos every block received
for byte in data:
time.sleep(0.01)
pipe.write(bytes([byte]))
# flush & sleep so don't loose ack
pipe.flush()
time.sleep(0.015)
recvd += pipe.read(1) # chew the echo
# LOG.debug("Echo was %s" % util.hexprint(recvd))
LOG.debug("Bytes sent: %i" % len(data))
# Radio does not ack last block
if (blocks < 8):
buf = pipe.read(block)
LOG.debug("ACK attempt: " + util.hexprint(buf))
if buf and buf[0] != yaesu_clone.CMD_ACK:
buf = pipe.read(block)
if not buf or buf[-1] != yaesu_clone.CMD_ACK:
raise errors.RadioError("Radio did not ack block %i" % blocks)
total_written += len(data)
_status()
pos += block
pipe.read(pos) # Chew the echo if using a 2-pin cable
LOG.debug("Clone completed in %i seconds" % (time.time() - start))
chirp-20261030/chirp/drivers/ft60.py 0000644 0001750 0001750 00000065212 15172631605 015762 0 ustar hibby hibby # Copyright 2011 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 time
import logging
from chirp.drivers import yaesu_clone
from chirp import chirp_common, memmap, bitwise, directory, errors
from chirp.settings import RadioSetting, RadioSettingGroup, \
RadioSettingValueList, RadioSettingValueString, \
RadioSettings
LOG = logging.getLogger(__name__)
ACK = b"\x06"
def _send(pipe, data):
pipe.write(data)
echo = pipe.read(len(data))
if not echo:
raise errors.RadioError("No echo from cable")
if echo != data:
raise errors.RadioError("Error reading echo (Bad cable?)")
def _download(radio):
data = b""
for i in range(0, 10):
chunk = radio.pipe.read(8)
if len(chunk) == 8:
data += chunk
break
elif chunk:
raise Exception("Received invalid response from radio")
time.sleep(1)
LOG.info("Trying again...")
if not data:
raise errors.RadioNoResponse()
_send(radio.pipe, ACK)
for i in range(0, 448):
chunk = radio.pipe.read(64)
data += chunk
_send(radio.pipe, ACK)
if len(chunk) == 1 and i == 447:
break
elif len(chunk) != 64:
raise Exception("Reading block %i was short (%i)" %
(i, len(chunk)))
if radio.status_fn:
status = chirp_common.Status()
status.cur = i * 64
status.max = radio.get_memsize()
status.msg = "Cloning from radio"
radio.status_fn(status)
return memmap.MemoryMapBytes(data)
def _upload(radio):
_send(radio.pipe, radio.get_mmap()[0:8])
ack = radio.pipe.read(1)
if not ack:
raise errors.RadioNoResponse()
if ack != ACK:
raise Exception("Unexpected response from radio")
for i in range(0, 448):
offset = 8 + (i * 64)
_send(radio.pipe, radio.get_mmap()[offset:offset + 64])
ack = radio.pipe.read(1)
if not ack:
raise errors.RadioError(_("Radio did not ack block %i") % i)
if ack != ACK:
raise Exception(_("Radio did not ack block %i") % i)
if radio.status_fn:
status = chirp_common.Status()
status.cur = offset + 64
status.max = radio.get_memsize()
status.msg = "Cloning to radio"
radio.status_fn(status)
def _decode_freq(freqraw):
freq = int(freqraw) * 10000
if freq > 8000000000:
freq = (freq - 8000000000) + 5000
if freq > 4000000000:
freq -= 4000000000
for i in range(0, 3):
freq += 2500
if chirp_common.required_step(freq) == 12.5:
break
return freq
def _encode_freq(freq):
freqraw = freq / 10000
flags = 0x00
if ((freq / 1000) % 10) >= 5:
flags += 0x80
if chirp_common.is_fractional_step(freq):
flags += 0x40
return freqraw, flags
def _decode_name(mem):
name = ""
for i in mem:
if i == 0xFF:
break
try:
name += CHARSET[i]
except IndexError:
LOG.error("Unknown char index: %i " % (i))
return name
def _encode_name(mem):
name = [None] * 6
for i in range(0, 6):
try:
name[i] = CHARSET.index(mem[i])
except IndexError:
name[i] = CHARSET.index(" ")
return name
MEM_FORMAT = """
#seekto 0x0024;
struct {
u8 apo;
u8 x25:3,
tot:5;
u8 x26;
u8 x27;
u8 x28:4,
rf_sql:4;
u8 x29:4,
int_cd:4;
u8 x2A:4,
int_mr:4;
u8 x2B:5,
lock:3;
u8 x2C:5,
dt_dly:3;
u8 x2D:7,
dt_spd:1;
u8 ar_bep;
u8 x2F:6,
lamp:2;
u8 x30:5,
bell:3;
u8 x31:5,
rxsave:3;
u8 x32;
u8 x33;
u8 x34;
u8 x35;
u8 x36;
u8 x37;
u8 wx_alt:1,
x38_1:3,
ar_int:1,
x38_5:3;
u8 x39:3,
ars:1,
vfo_bnd:1,
dcs_nr:2,
ssrch:1;
u8 pri_rvt:1,
x3A_1:1,
beep_sc:1,
edg_bep:1,
beep_key:1,
inet:2,
x3A_7:1;
u8 x3B_0:5,
scn_md:1,
x3B_6:2;
u8 x3C_0:2,
rev_hm:1,
mt_cl:1,
resume:2,
txsave:1,
pag_abk:1;
u8 x3D_0:1,
scn_lmp:1,
x3D_2:1,
bsy_led:1,
x3D_4:1,
tx_led:1,
x3D_6:2;
u8 x3E_0:2,
bclo:1,
x3E_3:5;
} settings;
#seekto 0x09E;
ul16 mbs;
#seekto 0x0C8;
struct {
u8 memory[16];
} dtmf[9];
struct mem {
u8 used:1,
unknown1:1,
isnarrow:1,
isam:1,
duplex:4;
bbcd freq[3];
u8 unknown2:1,
step:3,
unknown2_1:1,
tmode:3;
bbcd tx_freq[3];
u8 power:2,
tone:6;
u8 unknown4:1,
dtcs:7;
u8 unknown5;
u16 unknown5_1:1,
offset:15;
u8 unknown6[3];
};
#seekto 0x0248;
struct mem memory[1000];
#seekto 0x40c8;
struct mem pms[100];
#seekto 0x6EC8;
// skips:2 for Memory M in [1, 1000] is in flags[(M-1)/4].skip((M-1)%4).
// Interpret with SKIPS[].
// PMS memories L0 - U50 aka memory 1001 - 1100 don't have skip flags.
struct {
u8 skip3:2,
skip2:2,
skip1:2,
skip0:2;
} flags[250];
#seekto 0x4708;
struct {
u8 name[6];
u8 use_name:1,
unknown1:7;
u8 valid:1,
unknown2:7;
} names[1000];
#seekto 0x69C8;
struct {
bbcd memory[128];
} banks[10];
#seekto 0x6FC8;
u8 checksum;
"""
DUPLEX = ["", "", "-", "+", "split", "off"]
TMODES = ["", "Tone", "TSQL", "TSQL-R", "DTCS",
"DTCS->", "Tone->DTCS", "DTCS->Tone"]
POWER_LEVELS = [chirp_common.PowerLevel("High", watts=5.0),
chirp_common.PowerLevel("Mid", watts=2.0),
chirp_common.PowerLevel("Low", watts=0.5)]
STEPS = [5.0, 10.0, 12.5, 15.0, 20.0, 25.0, 50.0, 100.0]
SKIPS = ["", "S", "P"]
DTMF_CHARS = list("0123456789ABCD*#")
CHARSET = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ !`o$%&'()*+,-./|;/=>?@[u]^_"
SPECIALS = ["%s%d" % (c, i + 1) for i in range(0, 50) for c in ('L', 'U')]
class FT60BankModel(chirp_common.BankModel):
def get_num_mappings(self):
return 10
def get_mappings(self):
banks = []
for i in range(0, self.get_num_mappings()):
bank = chirp_common.Bank(self, "%i" % (i + 1), "Bank %i" % (i + 1))
bank.index = i
banks.append(bank)
return banks
def add_memory_to_mapping(self, memory, bank):
number = (memory.number - 1) / 8
mask = 1 << ((memory.number - 1) & 7)
self._radio._memobj.banks[bank.index].memory[number].set_bits(mask)
def remove_memory_from_mapping(self, memory, bank):
number = (memory.number - 1) / 8
mask = 1 << ((memory.number - 1) & 7)
m = self._radio._memobj.banks[bank.index].memory[number]
if m.get_bits(mask) != mask:
raise Exception("Memory %i is not in bank %s." %
(memory.number, bank))
self._radio._memobj.banks[bank.index].memory[number].clr_bits(mask)
def get_mapping_memories(self, bank):
memories = []
for i in range(*self._radio.get_features().memory_bounds):
number = (i - 1) / 8
mask = 1 << ((i - 1) & 7)
m = self._radio._memobj.banks[bank.index].memory[number]
if m.get_bits(mask) == mask:
memories.append(self._radio.get_memory(i))
return memories
def get_memory_mappings(self, memory):
banks = []
for bank in self.get_mappings():
number = (memory.number - 1) / 8
mask = 1 << ((memory.number - 1) & 7)
m = self._radio._memobj.banks[bank.index].memory[number]
if m.get_bits(mask) == mask:
banks.append(bank)
return banks
@directory.register
class FT60Radio(yaesu_clone.YaesuCloneModeRadio):
"""Yaesu FT-60"""
BAUD_RATE = 9600
VENDOR = "Yaesu"
MODEL = "FT-60"
_model = b"AH017"
_memsize = 28617
@classmethod
def get_prompts(cls):
rp = chirp_common.RadioPrompts()
rp.pre_download = _(
"1. Turn radio off.\n"
"2. Connect cable to MIC/SP jack.\n"
"3. Press and hold in the [MONI] switch while turning the\n"
" radio on.\n"
"4. Rotate the DIAL job to select \"F8 CLONE\".\n"
"5. Press the [F/W] key momentarily.\n"
"6. After clicking OK , hold the [PTT] switch\n"
" for one second to send image.\n")
rp.pre_upload = _(
"1. Turn radio off.\n"
"2. Connect cable to MIC/SP jack.\n"
"3. Press and hold in the [MONI] switch while turning the\n"
" radio on.\n"
"4. Rotate the DIAL job to select \"F8 CLONE\".\n"
"5. Press the [F/W] key momentarily.\n"
"6. Press the [MONI] switch (\"--RX--\" will appear on the"
" LCD).\n")
return rp
def get_features(self):
rf = chirp_common.RadioFeatures()
rf.memory_bounds = (1, 1000)
rf.valid_duplexes = DUPLEX[1:]
rf.valid_tmodes = [x for x in TMODES if '->' not in x] + ['Cross']
rf.valid_cross_modes = ['Tone->DTCS', 'DTCS->Tone', 'DTCS->']
rf.valid_power_levels = POWER_LEVELS
rf.valid_tuning_steps = STEPS
rf.valid_skips = SKIPS
rf.valid_special_chans = SPECIALS
rf.valid_characters = CHARSET
rf.valid_name_length = 6
rf.valid_modes = ["FM", "NFM", "AM"]
rf.valid_bands = [(108000000, 520000000), (700000000, 999990000)]
rf.can_odd_split = True
rf.has_ctone = False
rf.has_bank = True
rf.has_settings = True
rf.has_dtcs_polarity = False
return rf
def get_bank_model(self):
return FT60BankModel(self)
def _checksums(self):
return [yaesu_clone.YaesuChecksum(0x0000, 0x6FC7)]
def sync_in(self):
try:
self._mmap = _download(self)
except errors.RadioError:
raise
except Exception as e:
raise errors.RadioError("Failed to communicate with radio: %s" % e)
self.process_mmap()
self.check_checksums()
def sync_out(self):
self.update_checksums()
try:
_upload(self)
except errors.RadioError:
raise
except Exception as e:
raise errors.RadioError("Failed to communicate with radio: %s" % e)
def process_mmap(self):
self._memobj = bitwise.parse(MEM_FORMAT, self._mmap)
def get_settings(self):
_settings = self._memobj.settings
repeater = RadioSettingGroup("repeater", "Repeater Settings")
ctcss = RadioSettingGroup("ctcss", "CTCSS/DCS/DTMF Settings")
arts = RadioSettingGroup("arts", "ARTS Settings")
scan = RadioSettingGroup("scan", "Scan Settings")
power = RadioSettingGroup("power", "Power Saver Settings")
wires = RadioSettingGroup("wires", "WiRES(tm) Settings")
eai = RadioSettingGroup("eai", "EAI/EPCS Settings")
switch = RadioSettingGroup("switch", "Switch/Knob Settings")
misc = RadioSettingGroup("misc", "Miscellaneous Settings")
mbls = RadioSettingGroup("banks", "Memory Bank Link Scan")
setmode = RadioSettings(repeater, ctcss, arts, scan, power,
wires, eai, switch, misc, mbls)
# APO
opts = ["OFF"] + ["%0.1f" % (x * 0.5) for x in range(1, 24 + 1)]
misc.append(
RadioSetting(
"apo", "Automatic Power Off",
RadioSettingValueList(opts, current_index=_settings.apo)))
# AR.BEP
opts = ["OFF", "INRANG", "ALWAYS"]
arts.append(
RadioSetting(
"ar_bep", "ARTS Beep",
RadioSettingValueList(opts, current_index=_settings.ar_bep)))
# AR.INT
opts = ["25 SEC", "15 SEC"]
arts.append(
RadioSetting(
"ar_int", "ARTS Polling Interval",
RadioSettingValueList(opts, current_index=_settings.ar_int)))
# ARS
opts = ["OFF", "ON"]
repeater.append(
RadioSetting(
"ars", "Automatic Repeater Shift",
RadioSettingValueList(opts, current_index=_settings.ars)))
# BCLO
opts = ["OFF", "ON"]
misc.append(RadioSetting(
"bclo", "Busy Channel Lock-Out",
RadioSettingValueList(opts, current_index=_settings.bclo)))
# BEEP
opts = ["OFF", "KEY", "KEY+SC"]
rs = RadioSetting(
"beep_key", "Enable the Beeper",
RadioSettingValueList(
opts, current_index=_settings.beep_key + _settings.beep_sc))
def apply_beep(s, obj):
setattr(obj, "beep_key",
(int(s.value) & 1) or ((int(s.value) >> 1) & 1))
setattr(obj, "beep_sc", (int(s.value) >> 1) & 1)
rs.set_apply_callback(apply_beep, self._memobj.settings)
switch.append(rs)
# BELL
opts = ["OFF", "1T", "3T", "5T", "8T", "CONT"]
ctcss.append(
RadioSetting(
"bell", "Bell Repetitions",
RadioSettingValueList(
opts, current_index=_settings.bell)))
# BSY.LED
opts = ["ON", "OFF"]
misc.append(
RadioSetting(
"bsy_led", "Busy LED",
RadioSettingValueList(
opts, current_index=_settings.bsy_led)))
# DCS.NR
opts = ["TR/X N", "RX R", "TX R", "T/RX R"]
ctcss.append(
RadioSetting(
"dcs_nr", "\"Inverted\" DCS Code Decoding",
RadioSettingValueList(
opts, current_index=_settings.dcs_nr)))
# DT.DLY
opts = ["50 ms", "100 ms", "250 ms", "450 ms", "750 ms", "1000 ms"]
ctcss.append(
RadioSetting(
"dt_dly", "DTMF Autodialer Delay Time",
RadioSettingValueList(
opts, current_index=_settings.dt_dly)))
# DT.SPD
opts = ["50 ms", "100 ms"]
ctcss.append(
RadioSetting(
"dt_spd", "DTMF Autodialer Sending Speed",
RadioSettingValueList(
opts, current_index=_settings.dt_spd)))
# DT.WRT
for i in range(0, 9):
dtmf = self._memobj.dtmf[i]
str = ""
for c in dtmf.memory:
if c == 0xFF:
break
if c < len(DTMF_CHARS):
str += DTMF_CHARS[c]
val = RadioSettingValueString(0, 16, str, False)
val.set_charset(DTMF_CHARS + list("abcd"))
rs = RadioSetting("dtmf_%i" % i,
"DTMF Autodialer Memory %i" % (i + 1), val)
def apply_dtmf(s, obj):
str = s.value.get_value().upper().rstrip()
val = [DTMF_CHARS.index(x) for x in str]
for x in range(len(val), 16):
val.append(0xFF)
obj.memory = val
rs.set_apply_callback(apply_dtmf, dtmf)
ctcss.append(rs)
# EDG.BEP
opts = ["OFF", "ON"]
misc.append(
RadioSetting(
"edg_bep", "Band Edge Beeper",
RadioSettingValueList(
opts, current_index=_settings.edg_bep)))
# I.NET
opts = ["OFF", "COD", "MEM"]
rs = RadioSetting("inet", "Internet Link Connection",
RadioSettingValueList(
opts, current_index=_settings.inet - 1))
def apply_inet(s, obj):
setattr(obj, s.get_name(), int(s.value) + 1)
rs.set_apply_callback(apply_inet, self._memobj.settings)
wires.append(rs)
# INT.CD
opts = ["CODE 0", "CODE 1", "CODE 2", "CODE 3", "CODE 4",
"CODE 5", "CODE 6", "CODE 7", "CODE 8", "CODE 9",
"CODE A", "CODE B", "CODE C", "CODE D", "CODE E", "CODE F"]
wires.append(
RadioSetting(
"int_cd", "Access Number for WiRES(TM)",
RadioSettingValueList(
opts, current_index=_settings.int_cd)))
# INT.MR
opts = ["d1", "d2", "d3", "d4", "d5", "d6", "d7", "d8", "d9"]
wires.append(
RadioSetting(
"int_mr", "Access Number (DTMF) for Non-WiRES(TM)",
RadioSettingValueList(
opts, current_index=_settings.int_mr)))
# LAMP
opts = ["KEY", "5SEC", "TOGGLE"]
switch.append(
RadioSetting(
"lamp", "Lamp Mode",
RadioSettingValueList(
opts, current_index=_settings.lamp)))
# LOCK
opts = ["LK KEY", "LKDIAL", "LK K+D", "LK PTT",
"LK P+K", "LK P+D", "LK ALL"]
rs = RadioSetting("lock", "Control Locking",
RadioSettingValueList(
opts, current_index=_settings.lock - 1))
def apply_lock(s, obj):
setattr(obj, s.get_name(), int(s.value) + 1)
rs.set_apply_callback(apply_lock, self._memobj.settings)
switch.append(rs)
# M/T-CL
opts = ["MONI", "T-CALL"]
switch.append(
RadioSetting(
"mt_cl", "MONI Switch Function",
RadioSettingValueList(
opts, current_index=_settings.mt_cl)))
# PAG.ABK
opts = ["OFF", "ON"]
eai.append(
RadioSetting(
"pag_abk", "Paging Answer Back",
RadioSettingValueList(
opts, current_index=_settings.pag_abk)))
# RESUME
opts = ["TIME", "HOLD", "BUSY"]
scan.append(
RadioSetting(
"resume", "Scan Resume Mode",
RadioSettingValueList(
opts, current_index=_settings.resume)))
# REV/HM
opts = ["REV", "HOME"]
switch.append(
RadioSetting(
"rev_hm", "HM/RV Key Function",
RadioSettingValueList(
opts, current_index=_settings.rev_hm)))
# RF.SQL
opts = ["OFF", "S-1", "S-2", "S-3", "S-4", "S-5", "S-6",
"S-7", "S-8", "S-FULL"]
misc.append(
RadioSetting(
"rf_sql", "RF Squelch Threshold",
RadioSettingValueList(
opts, current_index=_settings.rf_sql)))
# PRI.RVT
opts = ["OFF", "ON"]
scan.append(
RadioSetting(
"pri_rvt", "Priority Revert",
RadioSettingValueList(
opts, current_index=_settings.pri_rvt)))
# RXSAVE
opts = ["OFF", "200 ms", "300 ms", "500 ms", "1 s", "2 s"]
power.append(
RadioSetting(
"rxsave", "Receive Mode Battery Savery Interval",
RadioSettingValueList(
opts, current_index=_settings.rxsave)))
# S.SRCH
opts = ["SINGLE", "CONT"]
misc.append(
RadioSetting(
"ssrch", "Smart Search Sweep Mode",
RadioSettingValueList(
opts, current_index=_settings.ssrch)))
# SCN.MD
opts = ["MEM", "ONLY"]
scan.append(
RadioSetting(
"scn_md", "Memory Scan Channel Selection Mode",
RadioSettingValueList(
opts, current_index=_settings.scn_md)))
# SCN.LMP
opts = ["OFF", "ON"]
scan.append(
RadioSetting(
"scn_lmp", "Scan Lamp",
RadioSettingValueList(
opts, current_index=_settings.scn_lmp)))
# TOT
opts = ["OFF"] + ["%dMIN" % (x) for x in range(1, 30 + 1)]
misc.append(
RadioSetting(
"tot", "Timeout Timer",
RadioSettingValueList(
opts, current_index=_settings.tot)))
# TX.LED
opts = ["ON", "OFF"]
misc.append(
RadioSetting(
"tx_led", "TX LED",
RadioSettingValueList(
opts, current_index=_settings.tx_led)))
# TXSAVE
opts = ["OFF", "ON"]
power.append(
RadioSetting(
"txsave", "Transmitter Battery Saver",
RadioSettingValueList(
opts, current_index=_settings.txsave)))
# VFO.BND
opts = ["BAND", "ALL"]
misc.append(
RadioSetting(
"vfo_bnd", "VFO Band Edge Limiting",
RadioSettingValueList(
opts, current_index=_settings.vfo_bnd)))
# WX.ALT
opts = ["OFF", "ON"]
scan.append(
RadioSetting(
"wx_alt", "Weather Alert Scan",
RadioSettingValueList(
opts, current_index=_settings.wx_alt)))
# MBS
for i in range(0, 10):
opts = ["OFF", "ON"]
mbs = (self._memobj.mbs >> i) & 1
rs = RadioSetting("mbs%i" % i, "Bank %s Scan" % (i + 1),
RadioSettingValueList(opts, current_index=mbs))
def apply_mbs(s, index):
if int(s.value):
self._memobj.mbs |= (1 << index)
else:
self._memobj.mbs &= ~(1 << index)
rs.set_apply_callback(apply_mbs, i)
mbls.append(rs)
return setmode
def set_settings(self, uisettings):
_settings = self._memobj.settings
for element in uisettings:
if not isinstance(element, RadioSetting):
self.set_settings(element)
continue
if not element.changed():
continue
try:
name = element.get_name()
value = element.value
if element.has_apply_callback():
LOG.debug("Using apply callback")
element.run_apply_callback()
else:
obj = getattr(_settings, name)
setattr(_settings, name, value)
LOG.debug("Setting %s: %s" % (name, value))
except Exception:
LOG.debug(element.get_name())
raise
def get_raw_memory(self, number):
return repr(self._memobj.memory[number - 1]) + \
repr(self._memobj.flags[(number - 1) / 4]) + \
repr(self._memobj.names[number - 1])
def get_memory(self, number):
mem = chirp_common.Memory()
if isinstance(number, str):
# pms channel
mem.number = 1001 + SPECIALS.index(number)
mem.extd_number = number
mem.immutable = ["number", "extd_number", "name", "skip"]
_mem = self._memobj.pms[mem.number - 1001]
_nam = _skp = None
elif number > 1000:
# pms channel
mem.number = number
mem.extd_number = SPECIALS[number - 1001]
mem.immutable = ["number", "extd_number", "name", "skip"]
_mem = self._memobj.pms[mem.number - 1001]
_nam = _skp = None
else:
mem.number = number
_mem = self._memobj.memory[mem.number - 1]
_nam = self._memobj.names[mem.number - 1]
_skp = self._memobj.flags[(mem.number - 1) / 4]
if not _mem.used:
mem.empty = True
return mem
mem.freq = _decode_freq(_mem.freq)
mem.offset = int(_mem.offset) * 50000
mem.duplex = DUPLEX[_mem.duplex]
if mem.duplex == "split":
if int(_mem.tx_freq) == 0:
mem.duplex = "off"
else:
mem.offset = _decode_freq(_mem.tx_freq)
tmode = TMODES[_mem.tmode]
if '->' in tmode:
mem.tmode = 'Cross'
mem.cross_mode = tmode
else:
mem.tmode = tmode
mem.rtone = chirp_common.TONES[_mem.tone]
mem.dtcs = chirp_common.DTCS_CODES[_mem.dtcs]
mem.power = POWER_LEVELS[_mem.power]
mem.mode = _mem.isam and "AM" or _mem.isnarrow and "NFM" or "FM"
mem.tuning_step = STEPS[_mem.step]
if _skp is not None:
skip = _skp["skip%i" % ((mem.number - 1) % 4)]
mem.skip = SKIPS[skip]
if _nam is not None:
if _nam.use_name and _nam.valid:
mem.name = _decode_name(_nam.name).rstrip()
return mem
def set_memory(self, mem):
if mem.number > 1000:
# pms channel
_mem = self._memobj.pms[mem.number - 1001]
_nam = _skp = None
else:
_mem = self._memobj.memory[mem.number - 1]
_nam = self._memobj.names[mem.number - 1]
_skp = self._memobj.flags[(mem.number - 1) / 4]
assert (_mem)
if mem.empty:
_mem.used = False
return
if not _mem.used:
_mem.set_raw("\x00" * 16)
_mem.used = 1
_mem.freq, flags = _encode_freq(mem.freq)
_mem.freq[0].set_bits(flags)
if mem.duplex == "split":
_mem.tx_freq, flags = _encode_freq(mem.offset)
_mem.tx_freq[0].set_bits(flags)
_mem.offset = 0
elif mem.duplex == "off":
_mem.tx_freq = 0
_mem.offset = 0
else:
_mem.tx_freq = 0
_mem.offset = mem.offset / 50000
_mem.duplex = DUPLEX.index(mem.duplex)
if mem.tmode == 'Cross':
_mem.tmode = TMODES.index(mem.cross_mode)
else:
_mem.tmode = TMODES.index(mem.tmode)
_mem.tone = chirp_common.TONES.index(mem.rtone)
_mem.dtcs = chirp_common.DTCS_CODES.index(mem.dtcs)
_mem.power = mem.power and POWER_LEVELS.index(mem.power) or 0
_mem.isnarrow = mem.mode == "NFM"
_mem.isam = mem.mode == "AM"
_mem.step = STEPS.index(mem.tuning_step)
if _skp is not None:
_skp["skip%i" % ((mem.number - 1) % 4)] = SKIPS.index(mem.skip)
if _nam is not None:
_nam.name = _encode_name(mem.name)
_nam.use_name = mem.name.strip() and True or False
_nam.valid = _nam.use_name
chirp-20261030/chirp/drivers/ft70.py 0000644 0001750 0001750 00000132416 14722311450 015755 0 ustar hibby hibby # Copyright 2010 Dan Smith
# Copyright 2017 Nicolas Pike
#
# 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.drivers import yaesu_clone
from chirp import chirp_common, directory, bitwise
from chirp import errors
from chirp import memmap
from chirp.settings import RadioSettingGroup, RadioSetting, RadioSettings, \
RadioSettingValueString, RadioSettingValueList, \
InvalidValueError
from chirp import util
import string
LOG = logging.getLogger(__name__)
# Testing
# 37 PAG.ABK Turn the pager answer back Function ON/OFF
# 38 PAG.CDR Specify a personal code (receive)
# 39 PAG.CDT Specify a personal code (transmit)
# 47 RX.MOD Select the receive mode. Auto FM AM
MEM_SETTINGS_FORMAT = """
#seekto 0x047e;
struct {
u8 unknown1;
u8 flag;
u16 unknown2;
struct {
char padded_string[6]; // 36 OPN.MSG Select MSG then key vm to edit it
} message;
} opening_message; // 36 OPN.MSG Select the Opening Message when transceiver is ON. OFF/MSG/DC
// FT-70DE New Model #5329
//
// Communications Mode ? AMS,FM DN,DW TX vs RX?
// Mode not currently correctly stored in memories ? - ALL show as FM in memories
// SKIP test/where stored
// Check storage of steps
// Pager settings ?
// Triple check/ understand _memsize and _block_lengths
// Bank name label name size display 6 store 16? padded with 0xFF same for MYCALL and message
// CHIRP mode DIG not supported - is there a CHIRP Fusion mode? Auto?
// Check character set
// Supported Modes ?
// Supported Bands ?
// rf.has_dtcs_polarity = False - think radio supports DTCS polarity
// rf.memory_bounds = (1, 900) - should this be 0? as zero displays as blank
// RPT offsets (stored per band) not included.
// 59 Display radio firmware version info and Radio ID?
// Front Panel settings power etc?
// Banks and VFO?
// Features Required
// Default AMS and Memory name (in mem extras) to enabled.
// Bugs
// MYCALL and Opening Message errors if not 10 characters
// Values greater than one sometimes stored as whole bytes, these need to be refactored into bit fields
// to prevent accidental overwriting of adjacent values
// Bank Name length not checked on gui input - but first 6 characters are saved correctly.
// Extended characters entered as bank names on radio are corrupted in Chirp
// Missing
// 50 SCV.WTH Set the VFO scan frequency range. BAND / ALL - NOT FOUND
// 49 SCM.WTH Set the memory scan frequency range. ALL / BAND - NOT FOUND
// Radio Questions
// Temp unit C/F not saved by radio, always goes back to C ?
// 44 RF SQL Adjusts the RF Squelch threshold level. OFF / S1 - S9? Default is OFF - Based on RF strength - for AM? How
// is this different from F, Monitor, Dial Squelch?
// Password setting on radio allows letters (DTMF), but letters cannot be entered at the radio's password prompt?
// 49 SCM.WTH Set the memory scan frequency range. ALL / BAND Defaults to ALL Not Band as stated in the manual.
#seekto 0x049a;
struct {
u8 unknown0:4,
squelch:4; // Squelch F, Monitor, Dial Adjust the Squelch level
} squelch_settings;
#seekto 0x04ba;
struct {
u8 unknown:3,
scan_resume:5; // 52 SCN.RSM Configure the scan stop mode settings. 2.0 S - 5.0 S - 10.0 S / BUSY / HOLD
u8 unknown1:3,
dw_resume_interval:5; // 22 DW RSM Configure the scan stop mode settings for Dual Receive. 2.0S-10.0S/BUSY/HOLD
u8 unknown2;
u8 unknown3:3,
apo:5; // 02 APO Set the length of time until the transceiver turns off automatically.
u8 unknown4:6,
gm_ring:2; // 24 GM RNG Select the beep option while receiving digital GM info. OFF/IN RNG/ALWAYS
u8 temp_cf; // Placeholder as not found
} first_settings;
//#seekto 0x04c0;
struct {
u8 unknown1:5,
beep_level:3; // 05 BEP.LVL Beep volume setting LEVEL1 - LEVEL4 - LEVEL7
u8 unknown2:6,
beep_select:2; // 04 BEEP Sets the beep sound function OFF / KEY+SC / KEY
} beep_settings;
#seekto 0x04ce;
struct {
u8 lcd_dimmer; // 14 DIMMER LCD Dimmer
u8 dtmf_delay; // 18 DT DLY DTMF delay
u8 unknown0[3];
u8 unknown1:4,
unknown1_2:4;
u8 lamp; // 28 LAMP Set the duration time of the backlight and keys to be lit
u8 lock; // 30 LOCK Configure the lock mode setting. KEY/DIAL/K+D/PTT/K+P/D+P/ALL
u8 unknown2_1;
u8 mic_gain; // 31 MCGAIN Adjust the microphone gain level
u8 unknown2_3;
u8 dw_interval; // 21 DW INT Set the priority memory ch mon int during Dual RX 0.1S-5.0S-10.0S
u8 ptt_delay; // 42 PTT.DLY Set the PTT delay time. OFF / 20 ms / 50 ms / 100 ms / 200 ms
u8 rx_save; // 48 RX.SAVE Set the battery save time. OFF / 0.2 s - 60.0 s
u8 scan_restart; // 53 SCN.STR Set the scanning restart time. 0.1 s - 2.0 s - 10.0 s
u8 unknown2_5;
u8 unknown2_6;
u8 unknown4[5];
u8 tot; // 56 TOT Set the transmission timeout timer
u8 unknown5[3]; // 26
u8 vfo_mode:1, // 60 VFO.MOD Set freq setting range in the VFO mode by DIAL knob. ALL / BAND
unknown7:1,
scan_lamp:1, // 51 SCN.LMP Set the scan lamp ON or OFF when scanning stops On/Off
unknown8:1,
ars:1, // 45 RPT.ARS Turn the ARS function on/off.
dtmf_speed:1, // 20 DT SPD Set DTMF speed
unknown8_1:1,
dtmf_mode:1; // DTMF Mode set from front panel
u8 busy_led:1, // Not Supported ?
unknown8_2:1,
unknown8_3:1,
bclo:1, // 03 BCLO Turns the busy channel lockout function on/off.
beep_edge:1, // 06 BEP.Edg Sets the beep sound ON or OFF when a band edge is encountered.
unknown8_6:1,
unknown8_7:1,
unknown8_8:1; // 28
u8 unknown9_1:1,
unknown9_2:1,
unknown9_3:1,
unknown9_4:1,
unknown9_5:1,
password:1, // Placeholder location
home_rev:1, // 26 HOME/REV Select the function of the [HOME/REV] key.
moni:1; // 32 Mon/T-Call Select the function of the [MONI/T-CALL] switch.
u8 gm_interval:4, // 30 // 25 GM INT Set tx interval of digital GM information. OFF / NORMAL / LONG
unknown10:4;
u8 unknown11;
u8 unknown12:1,
unknown12_2:1,
unknown12_3:1,
unknown12_4:1,
home_vfo:1, // 27 HOME->VFO Turn transfer VFO to the Home channel ON or OFF.
unknown12_6:1,
unknown12_7:1,
dw_rt:1; // 32 // 23 DW RVT Turn "Priority Channel Revert" feature ON or OFF during Dual Rx.
u8 unknown33;
u8 unknown34;
u8 unknown35;
u8 unknown36;
u8 unknown37;
u8 unknown38;
u8 unknown39;
u8 unknown40;
u8 unknown41;
u8 unknown42;
u8 unknown43;
u8 unknown44;
u8 unknown45;
u8 prog_key1; // P1 Set Mode Items to the Programmable Key
u8 prog_key2; // P2 Set Mode Items to the Programmable Key
} scan_settings;
#seekto 0x54a;
struct {
u16 in_use;
} bank_used[24];
#seekto 0x064b;
struct {
u8 unknown1:1,
unknown2:1,
unknown3:1,
unknown4:1,
vfo_scan_width:1, // Placeholder as not found - 50 SCV.WTH Set the VFO scan frequency range. BAND / ALL
memory_scan_width:1, // Placeholder as not found - 49 SCM.WTH Set the memory scan frequency range. ALL / BAND
unknown7:1,
unknown8:1;
} scan_settings_1;
struct bank_aux {
u8 unknown1;
u8 max_bank;
u8 unknown2;
u8 unknown3;
};
#seekto 0x654;
struct bank_aux bank_aux1;
#seekto 0x6D4;
struct bank_aux bank_aux2;
#seekto 0x06B6;
struct {
u8 unknown1:3,
volume:5; // # VOL and Dial Adjust the volume level
} scan_settings_2;
//#seekto 0x0690; // Memory or VFO Settings Map?
//struct {
//u8 unknown[48]; // Array cannot be 64 elements!
//u8 unknown1[16]; // Exception: Not implemented for chirp.bitwise.structDataElement
//} vfo_info_1;
//#seekto 0x0710; // Backup Memory or VFO Settings Map?
//struct {
//u8 unknown[48];
//u8 unknown1[16];
//} vfo_backup_info_1;
#seekto 0x094a; // DTMF Memories
struct {
u8 memory[16];
} dtmf[10];
#seekto 0x0EFE;
struct {
u8 unknown[2];
u8 name[6];
u8 unknown1[10];
} bank_info[24];
#seekto 0x154a;
struct {
u16 channel[100];
} bank_members[24];
"""
MEM_SETTINGS_FORMAT_HI = """
#seekto 0xCF30;
struct {
u8 unknown0;
u8 unknown1;
u8 unknown2;
u8 unknown3;
u8 unknown4;
u8 unknown5;
u8 unknown6;
u8 digital_popup; // 15 DIG.POP Call sign display pop up time
} digital_settings_more;
#seekto 0xCF7C;
struct {
u8 unknown0:6,
ams_tx_mode:2; // AMS TX Mode Short Press AMS button AMS TX Mode
u8 unknown1;
u8 unknown2:7,
standby_beep:1; // 07 BEP.STB Standby Beep in the digital C4FM mode. On/Off
u8 unknown3;
u8 unknown4:6,
gm_ring:2; // 24 GM RNG Select beep option while rx digital GM info. OFF/IN RNG/ALWAYS
u8 unknown5;
u8 rx_dg_id; // RX DG-ID Long Press Mode Key, Mode Key to select, Dial
u8 tx_dg_id; // TX DG-ID Long Press Mode Key, Dial
u8 unknown6:7,
vw_mode:1; // 16 DIG VW Turn the VW mode selection ON or OFF
u8 unknown7;
} digital_settings;
// ^^^ All above referenced U8's have been refactored to minimum number of bits.
"""
MEM_FORMAT = """
//#seekto 0x280A;
struct {
u8 nosubvfo:1,
unknown:3,
pskip:1, // PSkip (Select?)
skip:1, // Skip memory during scan
used:1, // Memory used
valid:1; // Always 1?
} flag[%d];
#seekto 0x2D4A;
struct { // 32 Bytes per memory entry
u8 display_tag:1, // 0 Display Freq, 1 Display Name
unknown0:1, // Mode if AMS not selected????????
deviation:1, // 0 Full deviation (Wide), 1 Half deviation (Narrow)
clock_shift:1, // 0 None, 1 CPU clock shifted
unknown1:4; // 1
u8 mode:2, // FM,AM,WFM only? - check
duplex:2, // Works
tune_step:4; // Works - check all steps? 7 = Auto // 1
bbcd freq[3]; // Works // 3
u8 power:2, // Works
unknown2:1, // 0 FM, 1 Digital - If AMS off
ams:1, // 0 AMS off, 1 AMS on ?
tone_mode:4; // Works // 1
u8 charsetbits[2]; // 2
char label[6]; // Works - Can only input 6 on screen // 6
char unknown7[10]; // Rest of label ??? // 10
bbcd offset[3]; // Works // 3
u8 unknown5:2,
tone:6; // Works // 1
u8 unknown6:1,
dcs:7; // Works // 1
u8 unknown9;
u8 ams_on_dn_vw_fm:2, // AMS DN, AMS VW, AMS FM
unknown8_3:1,
unknown8_4:1,
smeter:4;
u8 unknown10:2,
att:1,
auto_step:1,
auto_mode:1,
unknown11:2,
bell:1;
} memory[%d]; // DN, VW, FM, AM
// AMS DN, AMS VW, AMS FM
"""
MEM_CALLSIGN_FORMAT = """
#seekto 0x0ced0;
struct {
char callsign[10]; // 63 MYCALL Set the call sign. (up to 10 characters)
u16 charset; // character set ID
} my_call;
"""
MEM_CHECKSUM_FORMAT = """
#seekto 0xFECA;
u8 checksum;
"""
TMODES = ["", "Tone", "TSQL", "DTCS"]
DUPLEX = ["", "-", "+", "split"]
MODES = ["FM", "AM", "NFM"]
STEPS = [5, 6.25, 0, 10, 12.5, 15, 20, 25, 50, 100] # 0 = auto?
RFSQUELCH = ["OFF", "S1", "S2", "S3", "S4", "S5", "S6", "S7", "S8"]
SKIPS = ["", "S", "P"]
FT70_DTMF_CHARS = list("0123456789ABCDEF-")
CHARSET = ["%i" % int(x) for x in range(0, 10)] + \
[chr(x) for x in range(ord("A"), ord("Z") + 1)] + \
[" ", ] + \
[chr(x) for x in range(ord("a"), ord("z") + 1)] + \
list(".,:;*#_-/&()@!?^ ") + list("\x00" * 100)
POWER_LEVELS = [chirp_common.PowerLevel("Hi", watts=5.00),
chirp_common.PowerLevel("Mid", watts=2.00),
chirp_common.PowerLevel("Low", watts=.50)]
class FT70Bank(chirp_common.NamedBank):
"""A FT70 bank"""
def get_name(self):
_bank = self._model._radio._memobj.bank_info[self.index]
name = ""
for i in _bank.name:
if i == 0xff:
break
name += chr(i & 0xFF)
return name.rstrip()
def set_name(self, name):
_bank = self._model._radio._memobj.bank_info[self.index]
_bank.name = [ord(x) for x in name.ljust(6, chr(0xFF))[:6]]
class FT70BankModel(chirp_common.BankModel):
"""A FT70 bank model"""
def __init__(self, radio, name='Banks'):
super(FT70BankModel, self).__init__(radio, name)
_banks = self._radio._memobj.bank_info
self._bank_mappings = []
for index, _bank in enumerate(_banks):
bank = FT70Bank(self, "%i" % index, "BANK-%i" % index)
bank.index = index
self._bank_mappings.append(bank)
def get_num_mappings(self):
return len(self._bank_mappings)
def get_mappings(self):
return self._bank_mappings
def _channel_numbers_in_bank(self, bank):
_bank_used = self._radio._memobj.bank_used[bank.index]
if _bank_used.in_use == 0xFFFF:
return set()
_members = self._radio._memobj.bank_members[bank.index]
return set([int(ch) + 1 for ch in _members.channel if ch != 0xFFFF])
def _update_bank_with_channel_numbers(self, bank, channels_in_bank):
_members = self._radio._memobj.bank_members[bank.index]
if len(channels_in_bank) > len(_members.channel):
raise Exception("Too many entries in bank %d" % bank.index)
empty = 0
for index, channel_number in enumerate(sorted(channels_in_bank)):
_members.channel[index] = channel_number - 1
empty = index + 1
for index in range(empty, len(_members.channel)):
_members.channel[index] = 0xFFFF
max_bank = -1
for index, bank in enumerate(self._radio._memobj.bank_used):
if int(bank.in_use) != 0xFFFF:
max_bank = max(max_bank, index)
LOG.debug('Determined max bank to be %i', max_bank)
if max_bank == -1:
self._radio._memobj.bank_aux1.fill_raw(b'\xFF')
self._radio._memobj.bank_aux2.fill_raw(b'\xFF')
else:
self._radio._memobj.bank_aux1.fill_raw(b'\x00')
self._radio._memobj.bank_aux2.fill_raw(b'\x00')
self._radio._memobj.bank_aux1.max_bank = max_bank
self._radio._memobj.bank_aux2.max_bank = max_bank
def add_memory_to_mapping(self, memory, bank):
channels_in_bank = self._channel_numbers_in_bank(bank)
channels_in_bank.add(memory.number)
_bank_used = self._radio._memobj.bank_used[bank.index]
_bank_used.in_use = 0x06
self._update_bank_with_channel_numbers(bank, channels_in_bank)
# ~0x655 changes? Highest zero-origin bank used?
# First bank used 0x654-0x657 goes FF -> 00
# Also change 0x6d5 to match?
def remove_memory_from_mapping(self, memory, bank):
channels_in_bank = self._channel_numbers_in_bank(bank)
try:
channels_in_bank.remove(memory.number)
except KeyError:
raise Exception("Memory %i is not in bank %s. Cannot remove" %
(memory.number, bank))
if not channels_in_bank:
_bank_used = self._radio._memobj.bank_used[bank.index]
_bank_used.in_use = 0xFFFF
self._update_bank_with_channel_numbers(bank, channels_in_bank)
def get_mapping_memories(self, bank):
memories = []
for channel in self._channel_numbers_in_bank(bank):
memories.append(self._radio.get_memory(channel))
return memories
def get_memory_mappings(self, memory):
banks = []
for bank in self.get_mappings():
if memory.number in self._channel_numbers_in_bank(bank):
banks.append(bank)
return banks
@directory.register
class FT70Radio(yaesu_clone.YaesuCloneModeRadio):
"""Yaesu FT-70DE"""
BAUD_RATE = 38400
VENDOR = "Yaesu"
MODEL = "FT-70D"
FORMATS = [directory.register_format('FT-70D ADMS-10', '*.ft70d')]
_model = b"AH51G"
_adms_ext = '.ft70d'
_memsize = 65227 # 65227 read from dump
_block_lengths = [10, 65217]
_block_size = 32
_mem_params = (900, # size of memories array
900, # size of flags array
)
_has_vibrate = False
_has_af_dual = True
_BEEP_SELECT = ("Off", "Key+Scan", "Key")
_OPENING_MESSAGE = ("Off", "DC", "Message")
_MIC_GAIN = ("Level 1", "Level 2", "Level 3", "Level 4", "Level 5", "Level 6", "Level 7", "Level 8", "Level 9")
_AMS_TX_MODE = ("TX Auto", "TX DIGITAL", "TX FM")
_VW_MODE = ("On", "Off")
_DIG_POP_UP = ("Off", "2sec", "4sec", "6sec", "8sec", "10sec", "20sec", "30sec", "60sec", "Continuous")
_STANDBY_BEEP = ("On", "Off")
_SCAN_RESUME = ["%.1fs" % (0.5 * x) for x in range(4, 21)] + \
["Busy", "Hold"]
_SCAN_RESTART = ["%.1fs" % (0.1 * x) for x in range(1, 10)] + \
["%.1fs" % (0.5 * x) for x in range(2, 21)]
_LAMP_KEY = ["Key %d sec" % x
for x in range(2, 11)] + ["Continuous", "OFF"]
_LCD_DIMMER = ["Level %d" % x for x in range(1, 7)]
_TOT_TIME = ["Off"] + ["%.1f min" % (0.5 * x) for x in range(1, 21)]
_OFF_ON = ("Off", "On")
_ON_OFF = ("On", "Off")
_DTMF_MODE = ("Manual", "Auto")
_DTMF_SPEED = ("50ms", "100ms")
_DTMF_DELAY = ("50ms", "250ms", "450ms", "750ms", "1000ms")
_TEMP_CF = ("Centigrade", "Fahrenheit")
_APO_SELECT = ("Off", "0.5H", "1.0H", "1.5H", "2.0H", "2.5H", "3.0H", "3.5H", "4.0H", "4.5H", "5.0H",
"5.5H", "6.0H", "6.5H", "7.0H", "7.5H", "8.0H", "8.5H", "9.0H", "9.5H", "10.0H", "10.5H",
"11.0H", "11.5H", "12.0H")
_MONI_TCALL = ("Monitor", "Tone-CALL")
_HOME_REV = ("Home", "Reverse")
_LOCK = ("KEY", "DIAL", "Key+Dial", "PTT", "Key+PTT", "Dial+PTT", "ALL")
_PTT_DELAY = ("Off", "20 ms", "50 ms", "100 ms", "200 ms")
_BEEP_LEVEL = ("Level 1", "Level 2", "Level 3", "Level 4", "Level 5", "Level 6", "Level 7")
_SET_MODE = ("Level 1", "Level 2", "Level 3", "Level 4", "Level 5", "Level 6", "Level 7")
_RX_SAVE = ("OFF", "0.2s", ".3s", ".4s", ".5s", ".6s", ".7s", ".8s", ".9s", "1.0s", "1.5s",
"2.0s", "2.5s", "3.0s", "3.5s", "4.0s", "4.5s", "5.0s", "5.5s", "6.0s", "6.5s", "7.0s",
"7.5s", "8.0s", "8.5s", "9.0s", "10.0s", "15s", "20s", "25s", "30s", "35s", "40s", "45s", "50s", "55s",
"60s")
_VFO_MODE = ("ALL", "BAND")
_VFO_SCAN_MODE = ("BAND", "ALL")
_MEMORY_SCAN_MODE = ("BAND", "ALL")
_VOLUME = ["Level %d" % x for x in range(0, 32)]
_SQUELCH = ["Level %d" % x for x in range(0, 16)]
_DG_ID = ["%d" % x for x in range(0, 100)]
_GM_RING = ("OFF", "IN RING", "AlWAYS")
_GM_INTERVAL = ("LONG", "NORMAL", "OFF")
_MYCALL_CHR_SET = list(string.ascii_uppercase) + list(string.digits) + ['-', '/']
@classmethod
def match_model(cls, filedata, filename):
if filename.endswith(cls._adms_ext):
return True
else:
return super().match_model(filedata, filename)
@classmethod
def get_prompts(cls):
rp = chirp_common.RadioPrompts()
rp.pre_download = _(
"1. Turn radio on.\n"
"2. Connect cable to DATA terminal.\n"
"3. Unclip battery.\n"
"4. Press and hold in the [AMS] key and power key while clipping"
" \n in back battery the"
"(\"ADMS\" will appear on the display).\n"
"5. After clicking OK , press the [BAND] key.\n")
rp.pre_upload = _(
"1. Turn radio on.\n"
"2. Connect cable to DATA terminal.\n"
"3. Unclip battery.\n"
"4. Press and hold in the [AMS] key and power key while clipping"
" \n in back battery the "
"(\"ADMS\" will appear on the display).\n"
"5. Press the [MODE] key (\"-WAIT-\" will appear on the LCD).\n"
"Then click OK ")
return rp
def process_mmap(self):
mem_format = (MEM_SETTINGS_FORMAT + MEM_FORMAT + MEM_CALLSIGN_FORMAT +
MEM_SETTINGS_FORMAT_HI + MEM_CHECKSUM_FORMAT)
self._memobj = bitwise.parse(mem_format % self._mem_params, self._mmap)
def get_features(self):
rf = chirp_common.RadioFeatures()
rf.has_dtcs_polarity = False
rf.valid_modes = list(MODES)
rf.valid_tmodes = list(TMODES)
rf.valid_duplexes = list(DUPLEX)
rf.valid_tuning_steps = [x for x in STEPS if x]
rf.valid_bands = [(500000, 999900000)]
rf.valid_skips = SKIPS
rf.valid_power_levels = POWER_LEVELS
rf.valid_characters = "".join(CHARSET).upper()
rf.valid_name_length = 6
rf.memory_bounds = (1, 900)
rf.can_odd_split = True
rf.has_ctone = False
rf.has_bank_names = True
rf.has_settings = True
return rf
def get_raw_memory(self, number):
return "\n".join([repr(self._memobj.memory[number - 1]),
repr(self._memobj.flag[number - 1])])
def _checksums(self):
return [yaesu_clone.YaesuChecksum(0x0000, 0xFEC9)] # The whole file -2 bytes
@staticmethod
def _add_ff_pad(val, length):
return val.ljust(length, "\xFF")[:length]
@classmethod
def _strip_ff_pads(cls, messages):
result = []
for msg_text in messages:
result.append(str(msg_text).rstrip("\xFF"))
return result
def get_memory(self, number):
flag = self._memobj.flag[number - 1]
_mem = self._memobj.memory[number - 1]
mem = chirp_common.Memory()
mem.number = number
if not flag.used:
mem.empty = True
if not flag.valid:
mem.empty = True
return mem
mem.freq = chirp_common.fix_rounded_step(int(_mem.freq) * 1000)
mem.offset = int(_mem.offset) * 1000
mem.rtone = mem.ctone = chirp_common.TONES[_mem.tone]
self._get_tmode(mem, _mem)
mem.duplex = DUPLEX[_mem.duplex]
if mem.duplex == "split":
mem.offset = chirp_common.fix_rounded_step(mem.offset)
mem.mode = self._decode_mode(_mem)
mem.dtcs = chirp_common.DTCS_CODES[_mem.dcs]
mem.tuning_step = STEPS[_mem.tune_step]
mem.power = self._decode_power_level(_mem)
mem.skip = flag.pskip and "P" or flag.skip and "S" or ""
mem.name = self._decode_label(_mem)
return mem
def _decode_label(self, mem):
return str(mem.label).rstrip("\xFF")
def _encode_label(self, mem):
return self._add_ff_pad(mem.name.rstrip(), 6)
def _encode_charsetbits(self, mem):
# We only speak English here in chirpville
return [0x00, 0x00]
def _decode_power_level(self, mem): # 3 High 2 Mid 1 Low
return POWER_LEVELS[3 - mem.power]
def _encode_power_level(self, mem):
return 3 - POWER_LEVELS.index(mem.power)
def _decode_mode(self, mem):
mode = MODES[mem.mode]
if mode == 'FM' and int(mem.deviation):
return 'NFM'
else:
return mode
def _encode_mode(self, mem):
mode = mem.mode
if mode == 'NFM':
# Narrow is handled by a separate flag
mode = 'FM'
return MODES.index(mode)
def _get_tmode(self, mem, _mem):
mem.tmode = TMODES[_mem.tone_mode]
def _set_tmode(self, _mem, mem):
_mem.tone_mode = TMODES.index(mem.tmode)
def _set_mode(self, _mem, mem):
_mem.deviation = mem.mode == 'NFM'
_mem.mode = self._encode_mode(mem)
def _debank(self, mem):
bm = self.get_bank_model()
for bank in bm.get_memory_mappings(mem):
bm.remove_memory_from_mapping(mem, bank)
def set_memory(self, mem):
_mem = self._memobj.memory[mem.number - 1]
flag = self._memobj.flag[mem.number - 1]
self._debank(mem)
if not mem.empty and not flag.valid:
self._wipe_memory(_mem)
if mem.empty and flag.valid and not flag.used:
flag.valid = False
return
flag.used = not mem.empty
flag.valid = flag.used
if mem.empty:
return
if mem.freq < 30000000 or \
(mem.freq > 88000000 and mem.freq < 108000000) or \
mem.freq > 580000000:
flag.nosubvfo = True # Masked from VFO B
else:
flag.nosubvfo = False # Available in both VFOs
_mem.freq = int(mem.freq / 1000)
_mem.offset = int(mem.offset / 1000)
_mem.tone = chirp_common.TONES.index(mem.rtone)
self._set_tmode(_mem, mem)
_mem.duplex = DUPLEX.index(mem.duplex)
self._set_mode(_mem, mem)
_mem.dcs = chirp_common.DTCS_CODES.index(mem.dtcs)
_mem.tune_step = STEPS.index(mem.tuning_step)
if mem.power:
_mem.power = self._encode_power_level(mem)
else:
_mem.power = 3 # Set 3 - High power as the default
_mem.label = self._encode_label(mem)
charsetbits = self._encode_charsetbits(mem)
_mem.charsetbits[0], _mem.charsetbits[1] = charsetbits
flag.skip = mem.skip == "S"
flag.pskip = mem.skip == "P"
_mem.display_tag = 1 # Always Display Memory Name (For the moment..)
@classmethod
def _wipe_memory(cls, mem):
mem.set_raw("\x00" * (mem.size() // 8))
mem.unknown1 = 0x05
def get_bank_model(self):
return FT70BankModel(self)
def _get_dtmf_settings(self):
menu = RadioSettingGroup("dtmf_settings", "DTMF")
dtmf = self._memobj.scan_settings
val = RadioSettingValueList(
self._DTMF_MODE,
current_index=dtmf.dtmf_mode)
rs = RadioSetting("scan_settings.dtmf_mode", "DTMF Mode", val)
menu.append(rs)
val = RadioSettingValueList(
self._DTMF_DELAY,
current_index=dtmf.dtmf_delay)
rs = RadioSetting(
"scan_settings.dtmf_delay", "DTMF Delay", val)
menu.append(rs)
val = RadioSettingValueList(
self._DTMF_SPEED,
current_index=dtmf.dtmf_speed)
rs = RadioSetting(
"scan_settings.dtmf_speed", "DTMF Speed", val)
menu.append(rs)
for i in range(10):
name = "dtmf_%02d" % (i + 1)
if i == 9:
name = "dtmf_%02d" % 0
dtmfsetting = self._memobj.dtmf[i]
dtmfstr = ""
for c in dtmfsetting.memory:
if c == 0xFF:
break
if c < len(FT70_DTMF_CHARS):
dtmfstr += FT70_DTMF_CHARS[c]
dtmfentry = RadioSettingValueString(0, 16, dtmfstr)
dtmfentry.set_charset(
FT70_DTMF_CHARS + list("abcdef ")) # Allow input in lowercase, space ? validation fails otherwise
rs = RadioSetting(name, name.upper(), dtmfentry)
rs.set_apply_callback(self.apply_dtmf, i)
menu.append(rs)
return menu
def _get_display_settings(self):
menu = RadioSettingGroup("display_settings", "Display")
scan_settings = self._memobj.scan_settings
val = RadioSettingValueList(
self._LAMP_KEY,
current_index=scan_settings.lamp)
rs = RadioSetting("scan_settings.lamp", "Lamp", val)
menu.append(rs)
val = RadioSettingValueList(
self._LCD_DIMMER,
current_index=scan_settings.lcd_dimmer)
rs = RadioSetting("scan_settings.lcd_dimmer", "LCD Dimmer", val)
menu.append(rs)
opening_message = self._memobj.opening_message
val = RadioSettingValueList(
self._OPENING_MESSAGE,
current_index=opening_message.flag)
rs = RadioSetting("opening_message.flag", "Opening Msg Mode", val)
menu.append(rs)
return menu
def _get_config_settings(self):
menu = RadioSettingGroup("config_settings", "Config")
scan_settings = self._memobj.scan_settings
# 02 APO Set the length of time until the transceiver turns off automatically.
first_settings = self._memobj.first_settings
val = RadioSettingValueList(
self._APO_SELECT,
current_index=first_settings.apo)
rs = RadioSetting("first_settings.apo", "APO", val)
menu.append(rs)
# 03 BCLO Turns the busy channel lockout function on/off.
val = RadioSettingValueList(
self._OFF_ON,
current_index=scan_settings.bclo)
rs = RadioSetting("scan_settings.bclo", "Busy Channel Lockout", val)
menu.append(rs)
# 04 BEEP Sets the beep sound function.
beep_settings = self._memobj.beep_settings
val = RadioSettingValueList(
self._BEEP_SELECT,
current_index=beep_settings.beep_select)
rs = RadioSetting("beep_settings.beep_select", "Beep", val)
menu.append(rs)
# 05 BEP.LVL Beep volume setting LEVEL1 - LEVEL4 - LEVEL7
val = RadioSettingValueList(
self._BEEP_LEVEL,
current_index=beep_settings.beep_level)
rs = RadioSetting("beep_settings", "Beep Level", val)
menu.append(rs)
# 06 BEP.Edg Sets the beep sound ON or OFF when a band edge is encountered.
val = RadioSettingValueList(
self._OFF_ON,
current_index=scan_settings.beep_edge)
rs = RadioSetting("scan_settings.beep_edge", "Beep Band Edge", val)
menu.append(rs)
# 10 Bsy.LED Turn the MODE/STATUS Indicator ON or OFF while receiving signals.
val = RadioSettingValueList(
self._ON_OFF,
current_index=scan_settings.busy_led)
rs = RadioSetting("scan_settings.busy_led", "Busy LED", val)
menu.append(rs)
# 26 HOME/REV Select the function of the [HOME/REV] key.
val = RadioSettingValueList(
self._HOME_REV,
current_index=scan_settings.home_rev)
rs = RadioSetting("scan_settings.home_rev", "HOME/REV", val)
menu.append(rs)
# 27 HOME->VFO Turn transfer VFO to the Home channel ON or OFF.
val = RadioSettingValueList(
self._OFF_ON,
current_index=scan_settings.home_vfo)
rs = RadioSetting("scan_settings.home_vfo", "Home->VFO", val)
menu.append(rs)
# 30 LOCK Configure the lock mode setting. KEY / DIAL / K+D / PTT / K+P / D+P / ALL
val = RadioSettingValueList(
self._LOCK,
current_index=scan_settings.lock)
rs = RadioSetting("scan_settings.lock", "Lock Mode", val)
menu.append(rs)
# 32 Mon/T-Call Select the function of the [MONI/T-CALL] switch.
val = RadioSettingValueList(
self._MONI_TCALL,
current_index=scan_settings.moni)
rs = RadioSetting("scan_settings.moni", "MONI/T-CALL", val)
menu.append(rs)
# 42 PTT.DLY Set the PTT delay time. OFF / 20 ms / 50 ms / 100 ms / 200 ms
val = RadioSettingValueList(
self._PTT_DELAY,
current_index=scan_settings.ptt_delay)
rs = RadioSetting("scan_settings.ptt_delay", "PTT Delay", val)
menu.append(rs)
# 45 RPT.ARS Turn the ARS function on/off.
val = RadioSettingValueList(
self._OFF_ON,
current_index=scan_settings.ars)
rs = RadioSetting("scan_settings.ars", "ARS", val)
menu.append(rs)
# 48 RX.SAVE Set the battery save time. OFF / 0.2 s - 60.0 s
val = RadioSettingValueList(
self._RX_SAVE,
current_index=scan_settings.rx_save)
rs = RadioSetting("scan_settings.rx_save", "RX SAVE", val)
menu.append(rs)
# 60 VFO.MOD Set the frequency setting range in the VFO mode by DIAL knob. ALL / BAND
val = RadioSettingValueList(
self._VFO_MODE,
current_index=scan_settings.vfo_mode)
rs = RadioSetting("scan_settings.vfo_mode", "VFO MODE", val)
menu.append(rs)
# 56 TOT Set the timeout timer.
val = RadioSettingValueList(
self._TOT_TIME,
current_index=scan_settings.tot)
rs = RadioSetting("scan_settings.tot", "Transmit Timeout (TOT)", val)
menu.append(rs)
# 31 MCGAIN Adjust the microphone gain level
val = RadioSettingValueList(
self._MIC_GAIN,
current_index=scan_settings.mic_gain)
rs = RadioSetting("scan_settings.mic_gain", "Mic Gain", val)
menu.append(rs)
# VOLUME Adjust the volume level
scan_settings_2 = self._memobj.scan_settings_2
val = RadioSettingValueList(
self._VOLUME,
current_index=scan_settings_2.volume)
rs = RadioSetting("scan_settings_2.volume", "Volume", val)
menu.append(rs)
# Squelch F key, Hold Monitor, Dial to adjust squelch level
squelch_settings = self._memobj.squelch_settings
val = RadioSettingValueList(
self._SQUELCH,
current_index=squelch_settings.squelch)
rs = RadioSetting("squelch_settings.squelch", "Squelch", val)
menu.append(rs)
return menu
def _get_digital_settings(self):
menu = RadioSettingGroup("digital_settings", "Digital")
# MYCALL
mycall = self._memobj.my_call
mycallstr = str(mycall.callsign).rstrip("\xFF")
mycallentry = RadioSettingValueString(0, 10, mycallstr, False, charset=self._MYCALL_CHR_SET)
rs = RadioSetting('mycall.callsign', 'MYCALL', mycallentry)
rs.set_apply_callback(self.apply_mycall, mycall)
menu.append(rs)
# Short Press AMS button AMS TX Mode
digital_settings = self._memobj.digital_settings
val = RadioSettingValueList(
self._AMS_TX_MODE,
current_index=digital_settings.ams_tx_mode)
rs = RadioSetting("digital_settings.ams_tx_mode", "AMS TX Mode", val)
menu.append(rs)
# 16 DIG VW Turn the VW mode selection ON or OFF.
val = RadioSettingValueList(
self._VW_MODE,
current_index=digital_settings.vw_mode)
rs = RadioSetting("digital_settings.vw_mode", "VW Mode", val)
menu.append(rs)
# TX DG-ID Long Press Mode Key, Dial
val = RadioSettingValueList(
self._DG_ID,
current_index=digital_settings.tx_dg_id)
rs = RadioSetting("digital_settings.tx_dg_id", "TX DG-ID", val)
menu.append(rs)
# RX DG-ID Long Press Mode Key, Mode Key to select, Dial
val = RadioSettingValueList(
self._DG_ID,
current_index=digital_settings.rx_dg_id)
rs = RadioSetting("digital_settings.rx_dg_id", "RX DG-ID", val)
menu.append(rs)
# 15 DIG.POP Call sign display pop up time
# 00 OFF 00
# 0A 2s 10
# 0B 4s 11
# 0C 6s 12
# 0D 8s 13
# 0E 10s 14
# 0F 20s 15
# 10 30s 16
# 11 60s 17
# 12 CONT 18
digital_settings_more = self._memobj.digital_settings_more
val = RadioSettingValueList(
self._DIG_POP_UP,
current_index=0 if digital_settings_more.digital_popup == 0 else digital_settings_more.digital_popup - 9)
rs = RadioSetting("digital_settings_more.digital_popup", "Digital Popup", val)
rs.set_apply_callback(self.apply_digital_popup, digital_settings_more)
menu.append(rs)
# 07 BEP.STB Standby Beep in the digital C4FM mode. On/Off
val = RadioSettingValueList(
self._STANDBY_BEEP,
current_index=digital_settings.standby_beep)
rs = RadioSetting("digital_settings.standby_beep", "Standby Beep", val)
menu.append(rs)
return menu
def _get_gm_settings(self):
menu = RadioSettingGroup("first_settings", "Group Monitor")
# 24 GM RNG Select the beep option while receiving digital GM information. OFF / IN RNG /ALWAYS
first_settings = self._memobj.first_settings
val = RadioSettingValueList(
self._GM_RING,
current_index=first_settings.gm_ring)
rs = RadioSetting("first_settings.gm_ring", "GM Ring", val)
menu.append(rs)
# 25 GM INT Set the transmission interval of digital GM information. OFF / NORMAL / LONG
scan_settings = self._memobj.scan_settings
val = RadioSettingValueList(
self._GM_INTERVAL,
current_index=scan_settings.gm_interval)
rs = RadioSetting("scan_settings.gm_interval", "GM Interval", val)
menu.append(rs)
return menu
def _get_scan_settings(self):
menu = RadioSettingGroup("scan_settings", "Scan")
scan_settings = self._memobj.scan_settings
# 23 DW RVT Turn the "Priority Channel Revert" feature ON or OFF during Dual Receive.
val = RadioSettingValueList(
self._OFF_ON,
current_index=scan_settings.dw_rt)
rs = RadioSetting("scan_settings.dw_rt", "Dual Watch Priority Channel Revert", val)
menu.append(rs)
# 21 DW INT Set the priority memory channel monitoring interval during Dual Receive. 0.1S - 5.0S - 10.0S
val = RadioSettingValueList(
self._SCAN_RESTART,
current_index=scan_settings.dw_interval)
rs = RadioSetting("scan_settings.dw_interval", "Dual Watch Interval", val)
menu.append(rs)
# 22 DW RSM Configure the scan stop mode settings for Dual Receive. 2.0S - 10.0 S / BUSY / HOLD
first_settings = self._memobj.first_settings
val = RadioSettingValueList(
self._SCAN_RESUME,
current_index=first_settings.dw_resume_interval)
rs = RadioSetting("first_settings.dw_resume_interval", "Dual Watch Resume Interval", val)
menu.append(rs)
# 51 SCN.LMP Set the scan lamp ON or OFF when scanning stops. OFF / ON
val = RadioSettingValueList(
self._OFF_ON,
current_index=scan_settings.scan_lamp)
rs = RadioSetting("scan_settings.scan_lamp", "Scan Lamp", val)
menu.append(rs)
# 53 SCN.STR Set the scanning restart time. 0.1 S - 2.0 S - 10.0 S
val = RadioSettingValueList(
self._SCAN_RESTART,
current_index=scan_settings.scan_restart)
rs = RadioSetting("scan_settings.scan_restart", "Scan Restart", val)
menu.append(rs)
# Scan Width Section
# 50 SCV.WTH Set the VFO scan frequency range. BAND / ALL - NOT FOUND!
# Scan Resume Section
# 52 SCN.RSM Configure the scan stop mode settings. 2.0 S - 5.0 S - 10.0 S / BUSY / HOLD
first_settings = self._memobj.first_settings
val = RadioSettingValueList(
self._SCAN_RESUME,
current_index=first_settings.scan_resume)
rs = RadioSetting("first_settings.scan_resume", "Scan Resume", val)
menu.append(rs)
return menu
def _get_settings(self):
top = RadioSettings(
self._get_config_settings(),
self._get_digital_settings(),
self._get_display_settings(),
self._get_dtmf_settings(),
self._get_gm_settings(),
self._get_scan_settings()
)
return top
def get_settings(self):
try:
return self._get_settings()
except:
import traceback
LOG.error("Failed to parse settings: %s", traceback.format_exc())
return None
@classmethod
def apply_ff_padded_string(cls, setting, obj):
setattr(obj, "padded_string", cls._add_ff_pad(setting.value.get_value().rstrip(), 6))
def set_settings(self, settings):
_mem = self._memobj
for element in settings:
if not isinstance(element, RadioSetting):
self.set_settings(element)
continue
if not element.changed():
continue
try:
if element.has_apply_callback():
LOG.debug("Using apply callback")
try:
element.run_apply_callback()
except NotImplementedError as e:
LOG.error(e)
continue
# Find the object containing setting.
obj = _mem
bits = element.get_name().split(".")
setting = bits[-1]
for name in bits[:-1]:
if name.endswith("]"):
name, index = name.split("[")
index = int(index[:-1])
obj = getattr(obj, name)[index]
else:
obj = getattr(obj, name)
try:
old_val = getattr(obj, setting)
LOG.debug("Setting %s(%r) <= %s" % (
element.get_name(), old_val, element.value))
setattr(obj, setting, element.value)
except AttributeError as e:
LOG.error("Setting %s is not in the memory map: %s" %
(element.get_name(), e))
except Exception:
LOG.debug(element.get_name())
raise
def apply_volume(cls, setting, vfo):
val = setting.value.get_value()
cls._memobj.vfo_info[(vfo * 2)].volume = val
cls._memobj.vfo_info[(vfo * 2) + 1].volume = val
def apply_dtmf(cls, setting, i):
rawval = setting.value.get_value().upper().rstrip()
val = [FT70_DTMF_CHARS.index(x) for x in rawval]
for x in range(len(val), 16):
val.append(0xFF)
cls._memobj.dtmf[i].memory = val
def apply_digital_popup(cls, setting, obj):
rawval = setting.value.get_value()
val = 0 if cls._DIG_POP_UP.index(rawval) == 0 else cls._DIG_POP_UP.index(rawval) + 9
obj.digital_popup = val
def apply_mycall(cls, setting, obj):
cs = setting.value.get_value()
if cs[0] in ('-', '/'):
raise InvalidValueError("First character of call sign can't be - or /: {0:s}".format(cs))
else:
obj.callsign = cls._add_ff_pad(cs.rstrip(), 10)
def load_mmap(self, filename):
if filename.lower().endswith(self._adms_ext):
with open(filename, 'rb') as f:
self._adms_header = f.read(0xED)
if b'=ADMS10, Version=1.0.1.0' not in self._adms_header:
raise errors.ImageDetectFailed(
'Unsupported version found in ADMS file')
LOG.debug('ADMS Header:\n%s',
util.hexprint(self._adms_header))
self._mmap = memmap.MemoryMapBytes(self._model + f.read())
LOG.info('Loaded ADMS file')
self.process_mmap()
else:
chirp_common.CloneModeRadio.load_mmap(self, filename)
def save_mmap(self, filename):
if filename.lower().endswith(self._adms_ext):
if not hasattr(self, '_adms_header'):
raise Exception('Unable to save .img to %s' % self._adms_ext)
with open(filename, 'wb') as f:
f.write(self._adms_header)
f.write(self._mmap.get_packed()[5:])
LOG.info('Wrote file')
else:
chirp_common.CloneModeRadio.save_mmap(self, filename)
chirp-20261030/chirp/drivers/ft7100.py 0000644 0001750 0001750 00000115732 15266622247 016135 0 ustar hibby hibby # Copyright 2011 Dan Smith
#
# FT-2900-specific modifications by Richard Cochran,
# Initial work on settings by Chris Fosnight,
# FT-7100-specific modifications by Bruno Maire,
#
# 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 time
import logging
from chirp import util, memmap, chirp_common, bitwise, directory, errors
from chirp.drivers.yaesu_clone import YaesuCloneModeRadio
from chirp.settings import RadioSetting, RadioSettingGroup, \
RadioSettingValueList, RadioSettingValueString, RadioSettings, \
RadioSettingValueInteger, RadioSettingValueBoolean
LOG = logging.getLogger(__name__)
ACK = b"\x06"
NB_OF_BLOCKS = 248
BLOCK_LEN = 32
def _send(pipe, data):
time.sleep(0.035) # Same delay as "FT7100 Programmer" from RT Systems
# pipe.write(data) --> It seems, that the single bytes are sent too fast
# so send character per character with a delay
for ch in data:
pipe.write(bytes([ch]))
time.sleep(0.0012) # 0.0011 is to short. No ACK after a few packets
echo = pipe.read(len(data))
if data == b"":
raise Exception("Failed to read echo."
" Maybe serial hardware not connected."
" Maybe radio not powered or not in receiving mode.")
if data != echo:
LOG.debug("expecting echo\n%s\n", util.hexprint(data))
LOG.debug("got echo\n%s\n", util.hexprint(echo))
raise Exception("Got false echo. Expected: %r, got: %r.",
data, echo)
def _send_ack(pipe):
time.sleep(0.01) # Wait for radio input buffer ready
# time.sleep(0.0003) is the absolute working minimum.
# This delay is not critical for the transfer as there are not many ACKs.
_send(pipe, ACK)
def _wait_for_ack(pipe):
echo = pipe.read(1)
if echo == b"":
raise errors.RadioNoResponse()
if echo != ACK:
raise Exception("Failed to read ACK. Expected: %r, got: %r.",
ACK, echo)
def _download(radio):
LOG.debug("in _download\n")
data = b""
for _i in range(0, 60):
chunk = radio.pipe.read(BLOCK_LEN)
data += chunk
LOG.debug("Header:\n%s", util.hexprint(data))
if data == radio.IDBLOCK:
break
if len(data) > len(radio.IDBLOCK):
break
if data == b"":
raise Exception("Got no data from radio.")
if data != radio.IDBLOCK:
raise Exception("Got false header. Expected: %r, got: %r." % (
radio.IDBLOCK, data))
_send_ack(radio.pipe)
# read 16 Byte block
# and ignore it because it is constant. This might be a bug.
# It was built in at the very beginning and discovered very late that the
# data might be necessary later to write to the radio.
# Now the data is hardcoded in _upload(radio)
data = radio.pipe.read(16)
_send_ack(radio.pipe)
LOG.debug('Magic 16-byte chunk:\n%s' % util.hexprint(data))
# initialize data, the big var that holds all memory
data = b""
for block_nr in range(NB_OF_BLOCKS):
chunk = radio.pipe.read(BLOCK_LEN)
if len(chunk) != BLOCK_LEN:
LOG.debug("Block %i ", block_nr)
LOG.debug("Got: %i:\n%s", len(chunk), util.hexprint(chunk))
LOG.debug("len chunk is %i\n", len(chunk))
raise Exception("Failed to get full data block")
else:
data += chunk
_send_ack(radio.pipe)
if radio.status_fn:
status = chirp_common.Status()
status.max = NB_OF_BLOCKS * BLOCK_LEN
status.cur = len(data)
status.msg = "Cloning from radio"
radio.status_fn(status)
LOG.debug("Total: %i", len(data))
_send_ack(radio.pipe)
# for debugging purposes, dump the channels, in hex.
for _i in range(0, (NB_OF_BLOCKS * BLOCK_LEN) // 26):
_start_data = 4 + 26 * _i
chunk = data[_start_data:_start_data + 26]
LOG.debug("channel %i:\n%s", _i-21, util.hexprint(chunk))
return memmap.MemoryMapBytes(data)
def _upload(radio):
data = radio.pipe.read(256) # Clear buffer
_send(radio.pipe, radio.IDBLOCK)
_wait_for_ack(radio.pipe)
# write 16 Byte block
# If there should be a problem, see remarks in _download(radio)
_send(radio.pipe, b"\xEE\x77\x01\x00\x0E\x07\x0E\x07"
b"\x00\x00\x00\x00\x00\x02\x00\x00")
_wait_for_ack(radio.pipe)
for block_nr in range(NB_OF_BLOCKS):
data = radio.get_mmap()[block_nr * BLOCK_LEN:
(block_nr + 1) * BLOCK_LEN]
LOG.debug("Writing block_nr %i:\n%s", block_nr, util.hexprint(data))
_send(radio.pipe, data)
_wait_for_ack(radio.pipe)
if radio.status_fn:
status = chirp_common.Status()
status.max = NB_OF_BLOCKS * BLOCK_LEN
status.cur = block_nr * BLOCK_LEN
status.msg = "Cloning to radio"
radio.status_fn(status)
block_nr += 1
MEM_FORMAT = """
struct mem {
u8 is_used:1,
is_masked:1,
is_skip:1,
unknown11:3,
show_name:1,
is_split:1;
u8 unknown2;
ul32 freq_rx_Hz;
ul32 freq_tx_Hz;
ul16 offset_10khz;
u8 unknown_dependent_of_band_144_b0000_430_b0101:4,
tuning_step_index_1_2:4;
u8 unknown51:2,
is_offset_minus:1,
is_offset_plus:1,
unknown52:1,
tone_mode_index:3;
u8 tone_index;
u8 dtcs_index;
u8 is_mode_am:1,
unknown71:2,
is_packet96:1,
unknown72:2,
power_index:2;
u8 unknown81:2,
tuning_step_index_2_2:4,
unknown82:2;
char name[6];
u8 unknown9;
u8 unknownA;
};
// Settings are often present multiple times.
// The memories which is written to are mapped here
struct
{
#seekto 0x41;
u8 current_band;
#seekto 0xa1;
u8 apo;
#seekto 0xa2;
u8 ars_vhf;
#seekto 0xe2;
u8 ars_uhf;
#seekto 0xa3;
u8 arts_vhf;
#seekto 0xa3;
u8 arts_uhf;
#seekto 0xa4;
u8 beep;
#seekto 0xa5;
u8 cwid;
#seekto 0x80;
char cwidw[6];
#seekto 0xa7;
u8 dim;
#seekto 0xaa;
u8 dcsnr_vhf;
#seekto 0xea;
u8 dcsnr_uhf;
#seekto 0xab;
u8 disp;
#seekto 0xac;
u8 dtmfd;
#seekto 0xad;
u8 dtmfs;
#seekto 0xae;
u8 dtmfw;
#seekto 0xb0;
u8 lockt;
#seekto 0xb1;
u8 mic;
#seekto 0xb2;
u8 mute;
#seekto 0xb4;
u8 button[4];
#seekto 0xb8;
u8 rf_sql_vhf;
#seekto 0xf8;
u8 rf_sql_uhf;
#seekto 0xb9;
u8 scan_vhf;
#seekto 0xf9;
u8 scan_uhf;
#seekto 0xbc;
u8 speaker_cnt;
#seekto 0xff;
u8 tot;
#seekto 0xc0;
u8 txnar_vhf;
#seekto 0x100;
u8 txnar_uhf;
#seekto 0xc1;
u8 vfotr;
#seekto 0xc2;
u8 am;
} overlay;
// All known memories
#seekto 0x20;
u8 nb_mem_used_vhf;
#seekto 0x22;
u8 nb_mem_used_vhf_and_limits;
#seekto 0x24;
u8 nb_mem_used_uhf;
#seekto 0x26;
u8 nb_mem_used_uhf_and_limits;
#seekto 0x41;
u8 current_band;
#seekto 0x42;
u8 current_nb_mem_used_vhf_maybe_not;
#seekto 0x4c;
u8 priority_channel_maybe_1; // not_implemented
u8 priority_channel_maybe_2; // not_implemented
u8 priority_channel; // not_implemented
#seekto 0x87;
u8 opt_01_apo_1_4;
#seekto 0xa1;
u8 opt_01_apo_2_4;
#seekto 0xc5;
u8 opt_01_apo_3_4;
#seekto 0xe1;
u8 opt_01_apo_4_4;
#seekto 0x88;
u8 opt_02_ars_vhf_1_2;
#seekto 0xa2;
u8 opt_02_ars_vhf_2_2;
#seekto 0xc6;
u8 opt_02_ars_uhf_1_2;
#seekto 0xe2;
u8 opt_02_ars_uhf_2_2;
#seekto 0x89;
u8 opt_03_arts_mode_vhf_1_2;
#seekto 0xa3;
u8 opt_03_arts_mode_vhf_2_2;
#seekto 0xc7;
u8 opt_03_arts_mode_uhf_1_2;
#seekto 0xa3;
u8 opt_03_arts_mode_uhf_2_2;
#seekto 0x8a;
u8 opt_04_beep_1_2;
#seekto 0xa4;
u8 opt_04_beep_2_2;
#seekto 0x8b;
u8 opt_05_cwid_on_1_4;
#seekto 0xa5;
u8 opt_05_cwid_on_2_4;
#seekto 0xc9;
u8 opt_05_cwid_on_3_4;
#seekto 0xe5;
u8 opt_05_cwid_on_4_4;
#seekto 0x80;
char opt_06_cwidw[6];
#seekto 0x8d;
u8 opt_07_dim_1_4;
#seekto 0xa7;
u8 opt_07_dim_2_4;
#seekto 0xcb;
u8 opt_07_dim_3_4;
#seekto 0xe7;
u8 opt_07_dim_4_4;
#seekto 0x90;
u8 opt_10_dcsnr_vhf_1_2;
#seekto 0xaa;
u8 opt_10_dcsnr_vhf_2_2;
#seekto 0xce;
u8 opt_10_dcsnr_uhf_1_2;
#seekto 0xea;
u8 opt_10_dcsnr_uhf_2_2;
#seekto 0x91;
u8 opt_11_disp_1_4;
#seekto 0xab;
u8 opt_11_disp_2_4;
#seekto 0xcf;
u8 opt_11_disp_3_4;
#seekto 0xeb;
u8 opt_11_disp_4_4;
#seekto 0x92;
u8 opt_12_dtmf_delay_1_4;
#seekto 0xac;
u8 opt_12_dtmf_delay_2_4;
#seekto 0xd0;
u8 opt_12_dtmf_delay_3_4;
#seekto 0xec;
u8 opt_12_dtmf_delay_4_4;
#seekto 0x93;
u8 opt_13_dtmf_speed_1_4;
#seekto 0xad;
u8 opt_13_dtmf_speed_2_4;
#seekto 0xd1;
u8 opt_13_dtmf_speed_3_4;
#seekto 0xed;
u8 opt_13_dtmf_speed_4_4;
#seekto 0x94;
u8 opt_14_dtmfw_index_1_4;
#seekto 0xae;
u8 opt_14_dtmfw_index_2_4;
#seekto 0xd2;
u8 opt_14_dtmfw_index_3_4;
#seekto 0xee;
u8 opt_14_dtmfw_index_4_4;
#seekto 0x96;
u8 opt_16_lockt_1_4;
#seekto 0xb0;
u8 opt_16_lockt_2_4;
#seekto 0xd4;
u8 opt_16_lockt_3_4;
#seekto 0xf0;
u8 opt_16_lockt_4_4;
#seekto 0x97;
u8 opt_17_mic_MH48_1_4;
#seekto 0xb1;
u8 opt_17_mic_MH48_2_4;
#seekto 0xd5;
u8 opt_17_mic_MH48_3_4;
#seekto 0xf1;
u8 opt_17_mic_MH48_4_4;
#seekto 0x98;
u8 opt_18_mute_1_4;
#seekto 0xb2;
u8 opt_18_mute_2_4;
#seekto 0xd6;
u8 opt_18_mute_3_4;
#seekto 0xf2;
u8 opt_18_mute_4_4;
#seekto 0x9a;
u8 opt_20_pg_p_1_4[4];
#seekto 0xb4;
u8 opt_20_pg_p_2_4[4];
#seekto 0xd8;
u8 opt_20_pg_p_3_4[4];
#seekto 0xf4;
u8 opt_20_pg_p_4_4[4];
#seekto 0x9e;
u8 opt_24_rf_sql_vhf_1_2;
#seekto 0xb8;
u8 opt_24_rf_sql_vhf_2_2;
#seekto 0xdc;
u8 opt_24_rf_sql_uhf_1_2;
#seekto 0xf8;
u8 opt_24_rf_sql_uhf_2_2;
#seekto 0x9f;
u8 opt_25_scan_resume_vhf_1_2;
#seekto 0xb9;
u8 opt_25_scan_resume_vhf_2_2;
#seekto 0xdd;
u8 opt_25_scan_resume_uhf_1_2;
#seekto 0xf9;
u8 opt_25_scan_resume_uhf_2_2;
#seekto 0xbc;
u8 opt_28_speaker_cnt_1_2;
#seekto 0xfc;
u8 opt_28_speaker_cnt_2_2;
#seekto 0xbf;
u8 opt_31_tot_1_2;
#seekto 0xff;
u8 opt_31_tot_2_2;
#seekto 0xc0;
u8 opt_32_tx_nar_vhf;
#seekto 0x100;
u8 opt_32_tx_nar_uhf;
#seekto 0xc1;
u8 opt_33_vfo_tr;
#seekto 0xc2;
u8 opt_34_am_1_2;
#seekto 0x102;
u8 opt_34_am_2_2;
#seekto 260;
struct {
struct mem mem_struct;
char fill_ff[2];
} unknown00;
struct {
struct mem mem_struct;
char fill_00[6];
} current_vfo_vhf_uhf[2];
struct {
struct mem mem_struct;
char fill_ff[6];
} current_mem_vhf_uhf[2];
struct {
struct mem mem_struct;
char fill_ff[6];
} home_vhf_uhf[2];
struct {
struct mem mem_struct;
char fill_010003000000[6];
} vhome;
struct {
struct mem mem_struct;
char fill_010001000000[6];
} unknown01;
struct {
char name[32];
} Vertex_Standard_AH003M_Backup_DT;
struct mem
memory[260];
struct {
char name[24];
} Vertex_Standard_AH003M;
struct {
u8 dtmf[16];
} dtmf_mem[16];
"""
MODES_VHF = ["FM", "AM"]
MODES_UHF = ["FM"] # AM can be set but is ignored by the radio
DUPLEX = ["", "-", "+", "split"]
TONE_MODES_RADIO = ["", "Tone", "TSQL", "CTCSS Bell", "DTCS"]
TONE_MODES = ["", "Tone", "TSQL", "DTCS"]
POWER_LEVELS = [
chirp_common.PowerLevel("Low", watts=5),
chirp_common.PowerLevel("Low2", watts=10),
chirp_common.PowerLevel("Low3", watts=20),
chirp_common.PowerLevel("High", watts=35),
]
TUNING_STEPS = [5.0, 10.0, 12.5, 15.0, 20.0, 25.0, 50.0]
SKIP_VALUES = ["", "S"]
CHARSET = r"!\"#$%&'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\]^ _"
DTMF_CHARSET = "0123456789*# "
SPECIAL_CHANS = ['VFO-VHF', 'VFO-UHF', 'Home-VHF', 'Home-UHF', 'VFO', 'Home']
SCAN_LIMITS = ["L1", "U1", "L2", "U2", "L3", "U3", "L4", "U4", "L5", "U5"]
def do_download(radio):
"""This is your download function"""
return _download(radio)
@directory.register
class FT7100Radio(YaesuCloneModeRadio):
"""Yaesu FT-7100M"""
MODEL = "FT-7100M"
VARIANT = ""
IDBLOCK = b"Vartex Standard AH003M M-Map V04"
BAUD_RATE = 9600
@classmethod
def match_model(cls, filedata, filename):
return filedata[0x1ec0:0x1ec0 + len(cls.IDBLOCK)] == cls.IDBLOCK
# Return information about this radio's features, including
# how many memories it has, what bands it supports, etc
def get_features(self):
LOG.debug("get_features")
rf = chirp_common.RadioFeatures()
rf.has_bank = False
rf.memory_bounds = (0, 240)
# This radio supports 120 + 10 + 120 + 10 = 260 memories
# These are zero based for chirpc
rf.valid_bands = [
(108000000, 180000001), # Supports 2-meters tx
(320000000, 999990000), # Supports 70-centimeters tx
]
rf.can_odd_split = True
rf.has_ctone = True
rf.has_rx_dtcs = True
rf.has_cross = False
rf.has_dtcs_polarity = False
rf.has_bank = False
rf.has_bank_names = False
rf.has_settings = True
rf.has_sub_devices = True
rf.valid_tuning_steps = TUNING_STEPS
rf.valid_modes = MODES_VHF
rf.valid_tmodes = TONE_MODES
rf.valid_power_levels = POWER_LEVELS
rf.valid_duplexes = DUPLEX
rf.valid_skips = SKIP_VALUES
rf.valid_name_length = 6
rf.valid_characters = CHARSET
rf.valid_special_chans = SPECIAL_CHANS
return rf
def sync_in(self):
start = time.time()
try:
self._mmap = _download(self)
except errors.RadioError:
raise
except Exception as e:
raise errors.RadioError("Failed to communicate with radio: %s" % e)
LOG.info("Downloaded in %.2f sec", (time.time() - start))
self.process_mmap()
def sync_out(self):
self.pipe.timeout = 1
start = time.time()
try:
_upload(self)
except errors.RadioError:
raise
except Exception as e:
raise errors.RadioError("Failed to communicate with radio: %s" % e)
LOG.info("Uploaded in %.2f sec", (time.time() - start))
def process_mmap(self):
self._memobj = bitwise.parse(MEM_FORMAT, self._mmap)
def get_raw_memory(self, number):
return repr(self._memobj.memory[number])
def get_memory(self, number):
LOG.debug("get_memory Number: %r", number)
# Create a high-level memory object to return to the UI
mem = chirp_common.Memory()
# Get a low-level memory object mapped to the image
if isinstance(number, int) and number < 0:
number = SPECIAL_CHANS[number + 10]
if isinstance(number, str):
mem.number = -10 + SPECIAL_CHANS.index(number)
mem.extd_number = number
band = 0
if self._memobj.overlay.current_band != 0:
band = 1
if number == 'VFO-VHF':
_mem = self._memobj.current_vfo_vhf_uhf[0].mem_struct
elif number == 'VFO-UHF':
_mem = self._memobj.current_vfo_vhf_uhf[1].mem_struct
elif number == 'Home-VHF':
_mem = self._memobj.home_vhf_uhf[0].mem_struct
elif number == 'Home-UHF':
_mem = self._memobj.home_vhf_uhf[1].mem_struct
elif number == 'VFO':
_mem = self._memobj.current_vfo_vhf_uhf[band].mem_struct
elif number == 'Home':
_mem = self._memobj.home_vhf_uhf[band].mem_struct
_mem.is_used = True
else:
mem.number = number # Set the memory number
_mem = self._memobj.memory[number]
upper_channel = self._memobj.nb_mem_used_vhf
upper_limit = self._memobj.nb_mem_used_vhf_and_limits
if number >= upper_channel and number < upper_limit:
i = number - upper_channel
mem.extd_number = SCAN_LIMITS[i]
if number >= 260-10:
i = number - (260-10)
mem.extd_number = SCAN_LIMITS[i]
# Convert your low-level frequency to Hertz
mem.freq = int(_mem.freq_rx_Hz)
mem.name = str(_mem.name).rstrip() # Set the alpha tag
mem.rtone = chirp_common.TONES[_mem.tone_index]
mem.ctone = chirp_common.TONES[_mem.tone_index]
mem.dtcs = chirp_common.DTCS_CODES[_mem.dtcs_index]
mem.rx_dtcs = chirp_common.DTCS_CODES[_mem.dtcs_index]
tmode_radio = TONE_MODES_RADIO[_mem.tone_mode_index]
# CTCSS Bell is TSQL plus a flag in the extra setting
is_ctcss_bell = tmode_radio == "CTCSS Bell"
if is_ctcss_bell:
mem.tmode = "TSQL"
else:
mem.tmode = tmode_radio
mem.duplex = ""
if _mem.is_offset_plus:
mem.duplex = "+"
elif _mem.is_offset_minus:
mem.duplex = "-"
elif _mem.is_split:
mem.duplex = "split"
if _mem.is_split:
mem.offset = int(_mem.freq_tx_Hz)
else:
mem.offset = int(_mem.offset_10khz)*10000 # 10 kHz to Hz
if _mem.is_mode_am:
mem.mode = "AM"
else:
mem.mode = "FM"
mem.power = POWER_LEVELS[_mem.power_index]
mem.tuning_step = TUNING_STEPS[_mem.tuning_step_index_1_2]
if _mem.is_skip:
mem.skip = "S"
else:
mem.skip = ""
# mem.comment = ""
mem.empty = not _mem.is_used
mem.extra = RadioSettingGroup("Extra", "extra")
rs = RadioSetting("show_name", "Show Name",
RadioSettingValueBoolean(_mem.show_name))
mem.extra.append(rs)
rs = RadioSetting("is_masked", "Is Masked",
RadioSettingValueBoolean(_mem.is_masked))
mem.extra.append(rs)
rs = RadioSetting("is_packet96", "Packet 9600",
RadioSettingValueBoolean(_mem.is_packet96))
mem.extra.append(rs)
rs = RadioSetting("is_ctcss_bell", "CTCSS Bell",
RadioSettingValueBoolean(is_ctcss_bell))
mem.extra.append(rs)
return mem
def set_memory(self, mem):
LOG.debug("set_memory Number: %r", mem.number)
if mem.number < 0:
number = SPECIAL_CHANS[mem.number+10]
if number == 'VFO-VHF':
_mem = self._memobj.current_vfo_vhf_uhf[0].mem_struct
elif number == 'VFO-UHF':
_mem = self._memobj.current_vfo_vhf_uhf[1].mem_struct
elif number == 'Home-VHF':
_mem = self._memobj.home_vhf_uhf[0].mem_struct
elif number == 'Home-UHF':
_mem = self._memobj.home_vhf_uhf[1].mem_struct
else:
raise errors.RadioError("Unexpected Memory Number: %r",
mem.number)
else:
_mem = self._memobj.memory[mem.number]
_mem.name = mem.name.ljust(6)
_mem.tone_index = chirp_common.TONES.index(mem.rtone)
_mem.dtcs_index = chirp_common.DTCS_CODES.index(mem.dtcs)
if mem.tmode == "TSQL":
_mem.tone_index = chirp_common.TONES.index(mem.ctone)
if mem.tmode == "DTSC-R":
_mem.dtcs_index = chirp_common.DTCS_CODES.index(mem.rx_dtcs)
_mem.is_offset_plus = 0
_mem.is_offset_minus = 0
_mem.is_split = 0
_mem.freq_rx_Hz = mem.freq
_mem.freq_tx_Hz = mem.freq
if mem.duplex == "+":
_mem.is_offset_plus = 1
_mem.freq_tx_Hz = mem.freq + mem.offset
_mem.offset_10khz = int(mem.offset/10000)
elif mem.duplex == "-":
_mem.is_offset_minus = 1
_mem.freq_tx_Hz = mem.freq - mem.offset
_mem.offset_10khz = int(mem.offset/10000)
elif mem.duplex == "split":
_mem.is_split = 1
_mem.freq_tx_Hz = mem.offset
# No change to _mem.offset_10khz
_mem.is_mode_am = mem.mode == "AM"
if mem.power:
_mem.power_index = POWER_LEVELS.index(mem.power)
_mem.tuning_step_index_1_2 = TUNING_STEPS.index(mem.tuning_step)
_mem.is_skip = mem.skip == "S"
_mem.is_used = not mem.empty
# if name not empty: show name
_mem.show_name = mem.name.strip() != ""
# In testmode there is no setting in mem.extra
# This is why the following line is not located in the else part of
# the if structure below
_mem.tone_mode_index = TONE_MODES_RADIO.index(mem.tmode)
for setting in mem.extra:
if setting.get_name() == "is_ctcss_bell":
if mem.tmode == "TSQL" and setting.value:
_mem.tone_mode_index = TONE_MODES_RADIO.index("CTCSS Bell")
else:
setattr(_mem, setting.get_name(), setting.value)
LOG.debug("encoded mem\n%s\n",
(util.hexprint(_mem.get_raw(asbytes=False)[0:25])))
LOG.debug(repr(_mem))
def get_settings(self):
common = RadioSettingGroup("common", "Common Settings")
band = RadioSettingGroup("band", "Band dependent Settings")
arts = RadioSettingGroup("arts",
"Auto Range Transponder System (ARTS)")
dtmf = RadioSettingGroup("dtmf", "DTMF Settings")
mic_button = RadioSettingGroup("mic_button", "Microphone Buttons")
setmode = RadioSettings(common, band, arts, dtmf, mic_button)
_overlay = self._memobj.overlay
# numbers and names of settings refer to the way they're
# presented in the set menu, as well as the list starting on
# page 49 of the manual
# 1 Automatic Power Off
opts = [
"Off", "30 Min",
"1 Hour", "1.5 Hours",
"2 Hours", "2.5 Hours",
"3 Hours", "3.5 Hours",
"4 Hours", "4.5 Hours",
"5 Hours", "5.5 Hours",
"6 Hours", "6.5 Hours",
"7 Hours", "7.5 Hours",
"8 Hours", "8.5 Hours",
"9 Hours", "9.5 Hours",
"10 Hours", "10.5 Hours",
"11 Hours", "11.5 Hours",
"12 Hours",
]
common.append(
RadioSetting(
"apo", "Automatic Power Off",
RadioSettingValueList(opts, current_index=_overlay.apo)))
# 2 Automatic Repeater Shift function
opts = ["Off", "On"]
band.append(
RadioSetting(
"ars_vhf", "Automatic Repeater Shift VHF",
RadioSettingValueList(opts, current_index=_overlay.ars_vhf)))
band.append(
RadioSetting(
"ars_uhf", "Automatic Repeater Shift UHF",
RadioSettingValueList(opts, current_index=_overlay.ars_uhf)))
# 3 Selects the ARTS mode.
# -> Only useful to set it on the radio directly
# 4 Enables/disables the key/button beeper.
opts = ["Off", "On"]
common.append(
RadioSetting(
"beep", "Key/Button Beep",
RadioSettingValueList(opts, current_index=_overlay.beep)))
# 5 Enables/disables the CW IDer during ARTS operation.
opts = ["Off", "On"]
arts.append(
RadioSetting(
"cwid", "Enables/Disables the CW ID",
RadioSettingValueList(opts, current_index=_overlay.cwid)))
# 6 Callsign during ARTS operation.
cwidw = _overlay.cwidw.get_raw(asbytes=False)
cwidw = cwidw.rstrip('\x00')
val = RadioSettingValueString(0, 6, cwidw)
val.set_charset(CHARSET)
rs = RadioSetting("cwidw", "CW Identifier Callsign", val)
def apply_cwid(setting):
value_string = setting.value.get_value()
_overlay.cwidw.set_value(value_string)
rs.set_apply_callback(apply_cwid)
arts.append(rs)
# 7 Front panel display's illumination level.
opts = ["0: Off", "1: Max", "2", "3", "4", "5", "6", "7: Min"]
common.append(
RadioSetting(
"dim", "Display Illumination",
RadioSettingValueList(opts, current_index=_overlay.dim)))
# 8 Setting the DCS code number.
# Note: This Menu item can be set independently for each band,
# and independently in each memory.
# 9 Activates the DCS Code Search
# -> Only useful if set on radio itself
# 10 Selects 'Normal' or 'Inverted' DCS coding.
opts = ["TRX Normal", "RX Reversed", "TX Reversed", "TRX Reversed"]
band.append(
RadioSetting(
"dcsnr_vhf", "DCS coding VHF",
RadioSettingValueList(opts, current_index=_overlay.dcsnr_vhf)))
band.append(
RadioSetting(
"dcsnr_uhf", "DCS coding UHF",
RadioSettingValueList(opts, current_index=_overlay.dcsnr_uhf)))
# 11 Selects the 'sub' band display format
opts = ["Frequency", "Off / Sub Band disabled",
"DC Input Voltage", "CW ID"]
common.append(
RadioSetting(
"disp", "Sub Band Display Format",
RadioSettingValueList(opts, current_index=_overlay.disp)))
# 12 Setting the DTMF Autodialer delay time
opts = ["50 ms", "250 ms", "450 ms", "750 ms", "1 s"]
dtmf.append(
RadioSetting(
"dtmfd", "Autodialer delay time",
RadioSettingValueList(opts, current_index=_overlay.dtmfd)))
# 13 Setting the DTMF Autodialer sending speed
opts = ["50 ms", "75 ms", "100 ms"]
dtmf.append(
RadioSetting(
"dtmfs", "Autodialer sending speed",
RadioSettingValueList(opts, current_index=_overlay.dtmfs)))
# 14 Current DTMF Autodialer memory
rs = RadioSetting("dtmfw", "Current Autodialer memory",
RadioSettingValueInteger(1, 16, _overlay.dtmfw + 1))
def apply_dtmfw(setting):
_overlay.dtmfw = setting.value.get_value() - 1
rs.set_apply_callback(apply_dtmfw)
dtmf.append(rs)
# DTMF Memory
for i in range(16):
dtmf_string = ""
for j in range(16):
dtmf_char = ''
dtmf_int = int(self._memobj.dtmf_mem[i].dtmf[j])
if dtmf_int < 10:
dtmf_char = str(dtmf_int)
elif dtmf_int == 14:
dtmf_char = '*'
elif dtmf_int == 15:
dtmf_char = '#'
elif dtmf_int == 255:
break
dtmf_string += dtmf_char
radio_setting_value_string = RadioSettingValueString(0, 16,
dtmf_string)
radio_setting_value_string.set_charset(DTMF_CHARSET)
rs = RadioSetting("dtmf_%02i" % i,
"DTMF Mem " + str(i+1),
radio_setting_value_string)
def apply_dtmf(setting, index):
radio_setting_value_string = setting.value.get_value().rstrip()
j = 0
for dtmf_char in radio_setting_value_string:
dtmf_int = 255
if dtmf_char in "0123456789":
dtmf_int = int(dtmf_char)
elif dtmf_char == '*':
dtmf_int = 14
elif dtmf_char == '#':
dtmf_int = 15
if dtmf_int < 255:
self._memobj.dtmf_mem[index].dtmf[j] = dtmf_int
j += 1
if j < 16:
self._memobj.dtmf_mem[index].dtmf[j] = 255
rs.set_apply_callback(apply_dtmf, i)
dtmf.append(rs)
# 16 Enables/disables the PTT switch lock
opts = ["Off", "Band A", "Band B", "Both"]
common.append(
RadioSetting(
"lockt", "PTT switch lock",
RadioSettingValueList(opts, current_index=_overlay.lockt)))
# 17 Selects the Microphone type to be used
opts = ["MH-42", "MH-48"]
common.append(
RadioSetting(
"mic", "Microphone type",
RadioSettingValueList(opts, current_index=_overlay.mic)))
# 18 Reduces the audio level on the sub receiver when the
# main receiver is active
opts = ["Off", "On"]
common.append(
RadioSetting(
"mute", "Mute Sub Receiver",
RadioSettingValueList(opts, current_index=_overlay.mute)))
# 20 - 23 Programming the microphones button assignment
buttons = [
"ACC / P1",
"P / P2",
"P1 / P3",
"P2 / P4",
]
opts_button = ["Low", "Tone", "MHz", "Rev", "Home", "Band",
"VFO / Memory", "Sql Off", "1750 Hz Tone Call",
"Repeater", "Priority"]
for i, button in enumerate(buttons):
rs = RadioSetting(
"button" + str(i), button,
RadioSettingValueList(opts_button,
current_index=_overlay.button[i]))
def apply_button(setting, index):
value_string = setting.value.get_value()
value_int = opts_button.index(value_string)
_overlay.button[index] = value_int
rs.set_apply_callback(apply_button, i)
mic_button.append(rs)
# 24 Adjusts the RF SQL threshold level
opts = ["Off", "S-1", "S-5", "S-9", "S-FULL"]
band.append(
RadioSetting(
"rf_sql_vhf", "RF Sql VHF",
RadioSettingValueList(
opts, current_index=_overlay.rf_sql_vhf)))
band.append(
RadioSetting(
"rf_sql_uhf", "RF Sql UHF",
RadioSettingValueList(
opts, current_index=_overlay.rf_sql_uhf)))
# 25 Selects the Scan-Resume mode
opts = ["Busy", "Time"]
band.append(
RadioSetting(
"scan_vhf", "Scan-Resume VHF",
RadioSettingValueList(opts, current_index=_overlay.scan_vhf)))
band.append(
RadioSetting(
"scan_uhf", "Scan-Resume UHF",
RadioSettingValueList(opts, current_index=_overlay.scan_uhf)))
# 28 Defining the audio path to the external speaker
opts = ["Off", "Band A", "Band B", "Both"]
common.append(
RadioSetting(
"speaker_cnt", "External Speaker",
RadioSettingValueList(
opts, current_index=_overlay.speaker_cnt)))
# 31 Sets the Time-Out Timer
opts = ["Off", "Band A", "Band B", "Both"]
common.append(
RadioSetting(
"tot", "TX Time-Out [Min.] (0 = Off)",
RadioSettingValueInteger(0, 30, _overlay.tot)))
# 32 Reducing the MIC Gain (and Deviation)
opts = ["Off", "On"]
band.append(
RadioSetting(
"txnar_vhf", "TX Narrowband VHF",
RadioSettingValueList(opts, current_index=_overlay.txnar_vhf)))
band.append(
RadioSetting(
"txnar_uhf", "TX Narrowband UHF",
RadioSettingValueList(opts, current_index=_overlay.txnar_uhf)))
# 33 Enables/disables the VFO Tracking feature
opts = ["Off", "On"]
common.append(
RadioSetting(
"vfotr", "VFO Tracking",
RadioSettingValueList(opts, current_index=_overlay.vfotr)))
# 34 Selects the receiving mode on the VHF band
opts = ["Inhibit (only FM)", "AM", "Auto"]
common.append(
RadioSetting(
"am", "AM Mode",
RadioSettingValueList(opts, current_index=_overlay.am)))
# Current Band
opts = ["VHF", "UHF"]
common.append(
RadioSetting(
"current_band", "Current Band",
RadioSettingValueList(
opts, current_index=_overlay.current_band)))
# Show number of VHF and UHF channels
val = RadioSettingValueString(0, 7,
str(int(self._memobj.nb_mem_used_vhf)))
val.set_mutable(False)
rs = RadioSetting("num_chan_vhf", "Number of VHF channels", val)
common.append(rs)
val = RadioSettingValueString(0, 7,
str(int(self._memobj.nb_mem_used_uhf)))
val.set_mutable(False)
rs = RadioSetting("num_chan_uhf", "Number of UHF channels", val)
common.append(rs)
return setmode
def set_settings(self, uisettings):
_overlay = self._memobj.overlay
for element in uisettings:
if not isinstance(element, RadioSetting):
self.set_settings(element)
continue
if not element.changed():
continue
try:
name = element.get_name()
value = element.value
if element.has_apply_callback():
LOG.debug("Using apply callback")
element.run_apply_callback()
else:
setattr(_overlay, name, value)
LOG.debug("Setting %s: %s", name, value)
except Exception:
LOG.debug(element.get_name())
raise
def _get_upper_vhf_limit(self):
if self._memobj is None:
# test with tox has no _memobj
upper_vhf_limit = 120
else:
upper_vhf_limit = int(self._memobj.nb_mem_used_vhf)
return upper_vhf_limit
def _get_upper_uhf_limit(self):
if self._memobj is None:
# test with tox has no _memobj
upper_uhf_limit = 120
else:
upper_uhf_limit = int(self._memobj.nb_mem_used_uhf)
return upper_uhf_limit
@classmethod
def get_prompts(cls):
rp = chirp_common.RadioPrompts()
rp.pre_download = _(
"1. Turn Radio off.\n"
"2. Connect data cable.\n"
"3. While holding \"TONE\" and \"REV\" buttons, turn radio on.\n"
"4. After clicking OK , press \"TONE\" to send image.\n")
rp.pre_upload = _(
"1. Turn Radio off.\n"
"2. Connect data cable.\n"
"3. While holding \"TONE\" and \"REV\" buttons, turn radio on.\n"
"4. Press \"REV\" to receive image.\n"
"5. Make sure display says \"CLONE RX\" and green led is"
" blinking\n"
"6. Click OK to start transfer.\n")
return rp
def get_sub_devices(self):
if not self.VARIANT:
return [FT7100RadioVHF(self._mmap),
FT7100RadioUHF(self._mmap)]
else:
return []
class FT7100RadioVHF(FT7100Radio):
VARIANT = "VHF Band"
def get_features(self):
LOG.debug("VHF get_features")
upper_vhf_limit = self._get_upper_vhf_limit()
rf = FT7100Radio.get_features(self)
rf.memory_bounds = (1, upper_vhf_limit)
# Normally this band supports 120 + 10 memories. 1 based for chirpw
rf.valid_bands = [(108000000, 180000001)] # Supports 2-meters tx
rf.valid_modes = MODES_VHF
rf.valid_special_chans = ['VFO', 'Home']
rf.has_sub_devices = False
return rf
def get_memory(self, number):
LOG.debug("get_memory VHF Number: %s", number)
if isinstance(number, int):
if number >= 0:
mem = FT7100Radio.get_memory(self, number + 0 - 1)
else:
mem = FT7100Radio.get_memory(self, number)
mem.number = number
else:
mem = FT7100Radio.get_memory(self, number + '-VHF')
mem.extd_number = number
mem.immutable = ["number", "extd_number", "skip"]
return mem
def set_memory(self, mem):
LOG.debug("set_memory VHF Number: %s", mem.number)
# We modify memory, so dupe() it to avoid changing our caller's
# version
mem = mem.dupe()
if isinstance(mem.number, int):
if mem.number >= 0:
mem.number += -1
else:
mem.number += '-VHF'
super(FT7100RadioVHF, self).set_memory(mem)
return
class FT7100RadioUHF(FT7100Radio):
VARIANT = "UHF Band"
def get_features(self):
LOG.debug("UHF get_features")
upper_uhf_limit = self._get_upper_uhf_limit()
rf = FT7100Radio.get_features(self)
rf.memory_bounds = (1, upper_uhf_limit)
# Normally this band supports 120 + 10 memories. 1 based for chirpw
rf.valid_bands = [(320000000, 999990000)] # Supports 70-centimeters tx
# The radio can retain AM on UHF memories, although it ignores it.
# Keep it readable/valid so existing images remain round-trippable.
rf.valid_modes = MODES_UHF + ["AM"]
rf.valid_special_chans = ['VFO', 'Home']
rf.has_sub_devices = False
return rf
def get_memory(self, number):
LOG.debug("get_memory UHF Number: %s", number)
upper_vhf_limit = self._get_upper_vhf_limit()
if isinstance(number, int):
if number >= 0:
mem = FT7100Radio.get_memory(self, number + 10 +
upper_vhf_limit - 1)
else:
mem = FT7100Radio.get_memory(self, number)
mem.number = number
else:
mem = FT7100Radio.get_memory(self, number + '-UHF')
mem.extd_number = number
mem.immutable = ["number", "extd_number", "skip"]
return mem
def set_memory(self, mem):
LOG.debug("set_memory UHF Number: %s", mem.number)
# We modify memory, so dupe() it to avoid changing our caller's
# version
mem = mem.dupe()
upper_vhf_limit = self._get_upper_vhf_limit()
if isinstance(mem.number, int):
if mem.number >= 0:
mem.number += upper_vhf_limit - 1 + 10
else:
mem.number += '-UHF'
super(FT7100RadioUHF, self).set_memory(mem)
return
chirp-20261030/chirp/drivers/ft7800.py 0000644 0001750 0001750 00000121600 15172631605 016125 0 ustar hibby hibby # 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 time
import logging
import re
from chirp.drivers import yaesu_clone
from chirp import chirp_common, memmap, directory, bitwise, errors
from chirp.settings import RadioSetting, RadioSettingGroup, \
RadioSettingValueList, RadioSettingValueBoolean, \
RadioSettingValueString, RadioSettings, RadioSettingValueFloat
from collections import defaultdict
LOG = logging.getLogger(__name__)
ACK = b'\x06'
MEM_FORMAT = """
struct mem_struct {
u8 used:1,
unknown1:1,
mode:2,
unknown2:1,
duplex:3;
bbcd freq[3];
u8 clockshift:1,
tune_step:3,
unknown5:1, // TODO: tmode has extended settings, at least 4 bits
tmode:3;
bbcd split[3];
u8 power:2,
tone:6;
u8 unknown6:1,
dtcs:7;
u8 unknown7[2];
u8 offset;
u8 unknown9[3];
};
#seekto 0x002A;
u8 banks_unk2;
u8 current_channel;
u8 unk3;
u8 unk4;
u8 current_menu;
#seekto 0x003A;
struct {
u8 apo;
u8 tot;
u8 lock:3,
arts_interval:1,
unk1a:1,
prog_panel_acc:3;
u8 prog_p1;
u8 prog_p2;
u8 prog_p3;
u8 prog_p4;
u8 rf_sql;
u8 inet_dtmf_mem:4,
inet_dtmf_digit:4;
u8 arts_cwid_enable:1,
prog_tone_vm:1,
unk2a:1,
hyper_write:2,
memory_only:1,
dimmer:2;
u8 beep_scan:1,
beep_edge:1,
beep_key:1,
unk3a:1,
inet_mode:1,
unk3b:1,
dtmf_speed:2;
u8 dcs_polarity:2,
smart_search:1,
priority_revert:1,
unk4a:1,
dtmf_delay:3;
u8 unk5a:3,
microphone_type:1,
scan_resume:1,
unk5b:1,
arts_mode:2;
u8 unk6;
} settings;
#seekto 0x0048;
struct mem_struct vfos[5];
#seekto 0x00C8;
struct {
u8 memory[16];
} dtmf[16];
#seekto 0x01C8;
struct mem_struct homes[5];
#seekto 0x0218;
u8 arts_cwid[6];
#seekto 0x04C8;
struct mem_struct memory[1000];
#seekto 0x4988;
struct {
char name[6];
u8 enabled:1,
unknown1:7;
u8 used:1,
unknown2:7;
} names[1000];
#seekto 0x6c48;
struct {
u32 bitmap[32];
} bank_channels[20];
#seekto 0x7648;
struct {
u8 skip0:2,
skip1:2,
skip2:2,
skip3:2;
} flags[250];
#seekto 0x7B48;
u8 checksum;
"""
MODES = ["FM", "AM", "NFM"]
DUPLEX = ["", "", "-", "+", "split"]
STEPS = (5.0, 10.0, 12.5, 15.0, 20.0, 25.0, 50.0, 100.0)
STEPS_8800 = (5.0, 10.0, 12.5, 15.0, 20.0, 25.0, 50.0)
SKIPS = ["", "S", "P", ""]
CHARSET = ["%i" % int(x) for x in range(0, 10)] + \
[chr(x) for x in range(ord("A"), ord("Z")+1)] + \
list(" " * 10) + \
list("*+,- /| [ ] _") + \
list("\x00" * 100)
DTMFCHARSET = list("0123456789ABCD*#")
def _send(ser, data):
for i in data:
ser.write(bytes([i]))
time.sleep(0.002)
echo = ser.read(len(data))
if not echo:
raise errors.RadioError("No echo from cable")
if echo != data:
raise errors.RadioError("Error reading echo (Bad cable?)")
def _download(radio):
data = bytes(b"")
chunk = bytes(b"")
for i in range(0, 30):
chunk += radio.pipe.read(radio._block_lengths[0])
if chunk:
break
if len(chunk) != radio._block_lengths[0]:
if not chunk:
raise errors.RadioNoResponse()
raise Exception("Failed to read header (%i)" % len(chunk))
data += chunk
_send(radio.pipe, ACK)
for i in range(0, radio._block_lengths[1], radio._block_size):
chunk = radio.pipe.read(radio._block_size)
data += chunk
if len(chunk) != radio._block_size:
break
time.sleep(0.01)
_send(radio.pipe, ACK)
if radio.status_fn:
status = chirp_common.Status()
status.max = radio.get_memsize()
status.cur = i+len(chunk)
status.msg = "Cloning from radio"
radio.status_fn(status)
data += radio.pipe.read(1)
_send(radio.pipe, ACK)
return memmap.MemoryMapBytes(data)
def _upload(radio):
cur = 0
mmap = radio.get_mmap().get_byte_compatible()
first_exchange = True
for block in radio._block_lengths:
for _i in range(0, block, radio._block_size):
length = min(radio._block_size, block)
# LOG.debug("i=%i length=%i range: %i-%i" %
# (i, length, cur, cur+length))
_send(radio.pipe, mmap[cur:cur+length])
ack = radio.pipe.read(1)
if not ack:
if first_exchange:
raise errors.RadioNoResponse()
raise errors.RadioError("Radio did not ack block at %i" % cur)
if ack != ACK:
raise errors.RadioError("Radio did not ack block at %i" % cur)
first_exchange = False
cur += length
time.sleep(0.05)
if radio.status_fn:
status = chirp_common.Status()
status.cur = cur
status.max = radio.get_memsize()
status.msg = "Cloning to radio"
radio.status_fn(status)
def get_freq(rawfreq):
"""Decode a frequency that may include a fractional step flag"""
# Ugh. The 0x80 and 0x40 indicate values to add to get the
# real frequency. Gross.
if rawfreq > 8000000000:
rawfreq = (rawfreq - 8000000000) + 5000
if rawfreq > 4000000000:
rawfreq = (rawfreq - 4000000000) + 2500
return rawfreq
def set_freq(freq, obj, field):
"""Encode a frequency with any necessary fractional step flags"""
obj[field] = freq / 10000
if (freq % 1000) == 500:
obj[field][0].set_bits(0x40)
if (freq % 10000) >= 5000:
obj[field][0].set_bits(0x80)
return freq
def _decode_chars(inarr):
LOG.debug("@_decode_chars, type: %s" % type(inarr))
LOG.debug(inarr)
outstr = ""
for i in inarr:
if i == 0xFF:
break
outstr += CHARSET[i & 0x7F]
return outstr.rstrip()
def _encode_chars(instr, length=16):
LOG.debug("@_encode_chars, type: %s" % type(instr))
LOG.debug(instr)
outarr = []
instr = str(instr)
for i in range(length):
if i < len(instr):
outarr.append(CHARSET.index(instr[i]))
else:
outarr.append(0xFF)
return outarr
class FTx800Radio(yaesu_clone.YaesuCloneModeRadio):
"""Base class for FT-7800,7900,8800,8900 radios"""
BAUD_RATE = 9600
VENDOR = "Yaesu"
MODES = MODES
STEPS = STEPS
_block_size = 64
POWER_LEVELS_VHF = [chirp_common.PowerLevel("Hi", watts=50),
chirp_common.PowerLevel("Mid1", watts=20),
chirp_common.PowerLevel("Mid2", watts=10),
chirp_common.PowerLevel("Low", watts=5)]
POWER_LEVELS_UHF = [chirp_common.PowerLevel("Hi", watts=35),
chirp_common.PowerLevel("Mid1", watts=20),
chirp_common.PowerLevel("Mid2", watts=10),
chirp_common.PowerLevel("Low", watts=5)]
@classmethod
def get_prompts(cls):
rp = chirp_common.RadioPrompts()
rp.pre_download = _(
"1. Turn radio off.\n"
"2. Connect cable to DATA jack.\n"
"3. Press and hold in the [MHz(PRI)] key while turning the\n"
" radio on.\n"
"4. Rotate the DIAL job to select \"F-7 CLONE\".\n"
"5. Press and hold in the [BAND(SET)] key. The display\n"
" will disappear for a moment, then the \"CLONE\" notation\n"
" will appear.\n"
"6. After clicking OK , press the [V/M(MW)] key to send"
" image.\n")
rp.pre_upload = _(
"1. Turn radio off.\n"
"2. Connect cable to DATA jack.\n"
"3. Press and hold in the [MHz(PRI)] key while turning the\n"
" radio on.\n"
"4. Rotate the DIAL job to select \"F-7 CLONE\".\n"
"5. Press and hold in the [BAND(SET)] key. The display\n"
" will disappear for a moment, then the \"CLONE\" notation\n"
" will appear.\n"
"6. Press the [LOW(ACC)] key (\"--RX--\" will appear on the"
" display).\n")
return rp
def get_features(self):
rf = chirp_common.RadioFeatures()
rf.memory_bounds = (1, 999)
rf.has_bank = False
rf.has_ctone = False
rf.has_dtcs_polarity = False
rf.valid_modes = self.MODES
rf.valid_tmodes = self.TMODES
rf.valid_duplexes = ["", "-", "+", "split"]
rf.valid_tuning_steps = self.STEPS
rf.valid_bands = [(108000000, 520000000), (700000000, 990000000)]
rf.valid_skips = ["", "S", "P"]
rf.valid_power_levels = self.POWER_LEVELS_VHF
rf.valid_characters = "".join(CHARSET)
rf.valid_name_length = 6
rf.can_odd_split = True
return rf
def _checksums(self):
return [yaesu_clone.YaesuChecksum(0x0000, 0x7B47)]
def sync_in(self):
start = time.time()
try:
self._mmap = _download(self)
except errors.RadioError:
raise
except Exception as e:
raise errors.RadioError("Failed to communicate with radio: %s" % e)
LOG.info("Download finished in %i seconds" % (time.time() - start))
self.check_checksums()
self.process_mmap()
def process_mmap(self):
self._memobj = bitwise.parse(MEM_FORMAT, self._mmap)
def sync_out(self):
self.update_checksums()
start = time.time()
try:
_upload(self)
except errors.RadioError:
raise
except Exception as e:
raise errors.RadioError("Failed to communicate with radio: %s" % e)
LOG.info("Upload finished in %i seconds" % (time.time() - start))
def get_raw_memory(self, number):
return repr(self._memobj.memory[number-1])
def _get_mem_offset(self, mem, _mem):
if mem.duplex == "split":
return get_freq(int(_mem.split) * 10000)
else:
return (_mem.offset * 5) * 10000
def _set_mem_offset(self, mem, _mem):
if mem.duplex == "split":
set_freq(mem.offset, _mem, "split")
else:
_mem.offset = (int(mem.offset / 10000) / 5)
def _get_mem_name(self, mem, _mem):
_nam = self._memobj.names[mem.number - 1]
name = ""
if _nam.used:
for i in str(_nam.name):
name += CHARSET[ord(i)]
return name.rstrip()
def _set_mem_name(self, mem, _mem):
_nam = self._memobj.names[mem.number - 1]
if mem.name.rstrip():
name = [chr(CHARSET.index(x)) for x in mem.name.ljust(6)[:6]]
_nam.name = "".join(name)
_nam.used = 1
_nam.enabled = 1
else:
_nam.used = 0
_nam.enabled = 0
def _get_mem_skip(self, mem, _mem):
_flg = self._memobj.flags[(mem.number - 1) / 4]
flgidx = (mem.number - 1) % 4
return SKIPS[_flg["skip%i" % flgidx]]
def _set_mem_skip(self, mem, _mem):
_flg = self._memobj.flags[(mem.number - 1) / 4]
flgidx = (mem.number - 1) % 4
_flg["skip%i" % flgidx] = SKIPS.index(mem.skip)
def _get_mem_mode(self, mem, _mem):
mem.mode = self.MODES[_mem.mode]
def _set_mem_mode(self, mem, _mem):
_mem.mode = self.MODES.index(mem.mode)
def get_memory(self, number):
_mem = self._memobj.memory[number - 1]
mem = chirp_common.Memory()
mem.number = number
mem.empty = not _mem.used
if mem.empty:
return mem
mem.freq = get_freq(int(_mem.freq) * 10000)
mem.rtone = chirp_common.TONES[_mem.tone]
mem.tmode = self.TMODES[_mem.tmode]
self._get_mem_mode(mem, _mem)
mem.dtcs = chirp_common.DTCS_CODES[_mem.dtcs]
if self.get_features().has_tuning_step:
mem.tuning_step = self.STEPS[_mem.tune_step]
mem.duplex = DUPLEX[_mem.duplex]
mem.offset = self._get_mem_offset(mem, _mem)
mem.name = self._get_mem_name(mem, _mem)
if int(mem.freq / 100) == 4:
mem.power = self.POWER_LEVELS_UHF[_mem.power]
else:
mem.power = self.POWER_LEVELS_VHF[_mem.power]
mem.skip = self._get_mem_skip(mem, _mem)
return mem
def set_memory(self, mem):
_mem = self._memobj.memory[mem.number - 1]
_mem.used = int(not mem.empty)
if mem.empty:
return
set_freq(mem.freq, _mem, "freq")
_mem.tone = chirp_common.TONES.index(mem.rtone)
_mem.tmode = self.TMODES.index(mem.tmode)
self._set_mem_mode(mem, _mem)
_mem.dtcs = chirp_common.DTCS_CODES.index(mem.dtcs)
if self.get_features().has_tuning_step:
_mem.tune_step = self.STEPS.index(mem.tuning_step)
_mem.duplex = DUPLEX.index(mem.duplex)
_mem.split = mem.duplex == "split" and int(mem.offset / 10000) or 0
if mem.power:
_mem.power = self.POWER_LEVELS_VHF.index(mem.power)
else:
_mem.power = 0
_mem.unknown5 = 0 # Make sure we don't leave garbage here
# NB: Leave offset after mem name for the 8800!
self._set_mem_name(mem, _mem)
self._set_mem_offset(mem, _mem)
self._set_mem_skip(mem, _mem)
class FT7800BankModel(chirp_common.BankModel):
"""Yaesu FT-7800/7900 bank model"""
def __init__(self, radio):
super(FT7800BankModel, self).__init__(radio)
self.__b2m_cache = defaultdict(list)
self.__m2b_cache = defaultdict(list)
def __precache(self):
if self.__b2m_cache:
return
for bank in self.get_mappings():
self.__b2m_cache[bank.index] = self._get_bank_memories(bank)
for memnum in self.__b2m_cache[bank.index]:
self.__m2b_cache[memnum].append(bank.index)
def get_num_mappings(self):
return 20
def get_mappings(self):
banks = []
for i in range(0, self.get_num_mappings()):
bank = chirp_common.Bank(self, "%i" % i, "BANK-%i" % (i + 1))
bank.index = i
banks.append(bank)
return banks
def add_memory_to_mapping(self, memory, bank):
self.__precache()
index = memory.number - 1
_bitmap = self._radio._memobj.bank_channels[bank.index]
ishft = 31 - (index % 32)
_bitmap.bitmap[index // 32] |= (1 << ishft)
self.__m2b_cache[memory.number].append(bank.index)
self.__b2m_cache[bank.index].append(memory.number)
def remove_memory_from_mapping(self, memory, bank):
self.__precache()
index = memory.number - 1
_bitmap = self._radio._memobj.bank_channels[bank.index]
ishft = 31 - (index % 32)
if not (_bitmap.bitmap[index // 32] & (1 << ishft)):
raise Exception("Memory {num} is not in bank {bank}".format(
num=memory.number, bank=bank))
_bitmap.bitmap[index // 32] &= ~(1 << ishft)
self.__b2m_cache[bank.index].remove(memory.number)
self.__m2b_cache[memory.number].remove(bank.index)
def _get_bank_memories(self, bank):
memories = []
upper = self._radio.get_features().memory_bounds[1]
c = self._radio._memobj.bank_channels[bank.index]
for i in range(0, upper):
_bitmap = c.bitmap[i // 32]
ishft = 31 - (i % 32)
if _bitmap & (1 << ishft):
memories.append(i + 1)
return memories
def get_mapping_memories(self, bank):
self.__precache()
return [self._radio.get_memory(n)
for n in self.__b2m_cache[bank.index]]
def get_memory_mappings(self, memory):
self.__precache()
_banks = self.get_mappings()
return [_banks[b] for b in self.__m2b_cache[memory.number]]
@directory.register
class FT7800Radio(FTx800Radio):
"""Yaesu FT-7800"""
MODEL = "FT-7800/7900"
_model = b"AH016"
_memsize = 31561
_block_lengths = [8, 31552, 1]
TMODES = ["", "Tone", "TSQL", "TSQL-R", "DTCS"]
def get_bank_model(self):
if not hasattr(self, '_banks'):
self._banks = FT7800BankModel(self)
return self._banks
def get_features(self):
rf = FTx800Radio.get_features(self)
rf.has_bank = True
rf.has_settings = True
return rf
def set_memory(self, memory):
if memory.empty:
self._wipe_memory_banks(memory)
FTx800Radio.set_memory(self, memory)
def get_settings(self):
_settings = self._memobj.settings
basic = RadioSettingGroup("basic", "Basic")
dtmf = RadioSettingGroup("dtmf", "DTMF")
arts = RadioSettingGroup("arts", "ARTS")
prog = RadioSettingGroup("prog", "Programmable Buttons")
top = RadioSettings(basic, dtmf, arts, prog)
basic.append(RadioSetting(
"priority_revert", "Priority Revert",
RadioSettingValueBoolean(_settings.priority_revert)))
basic.append(RadioSetting(
"memory_only", "Memory Only mode",
RadioSettingValueBoolean(_settings.memory_only)))
opts = ["off"] + ["%0.1f" % (t / 60.0) for t in range(30, 750, 30)]
basic.append(RadioSetting(
"apo", "APO time (hrs)",
RadioSettingValueList(opts, current_index=_settings.apo)))
basic.append(RadioSetting(
"beep_scan", "Beep: Scan",
RadioSettingValueBoolean(_settings.beep_scan)))
basic.append(RadioSetting(
"beep_edge", "Beep: Edge",
RadioSettingValueBoolean(_settings.beep_edge)))
basic.append(RadioSetting(
"beep_key", "Beep: Key",
RadioSettingValueBoolean(_settings.beep_key)))
opts = ["T/RX Normal", "RX Reverse", "TX Reverse", "T/RX Reverse"]
basic.append(
RadioSetting(
"dcs_polarity", "DCS polarity",
RadioSettingValueList(
opts, current_index=_settings.dcs_polarity)))
opts = ["off", "dim 1", "dim 2", "dim 3"]
basic.append(RadioSetting(
"dimmer", "Dimmer",
RadioSettingValueList(opts, current_index=_settings.dimmer)))
opts = ["manual", "auto", "1-auto"]
basic.append(
RadioSetting(
"hyper_write", "Hyper Write",
RadioSettingValueList(
opts, current_index=_settings.hyper_write)))
opts = ["", "key", "dial", "key+dial", "ptt",
"ptt+key", "ptt+dial", "all"]
basic.append(RadioSetting(
"lock", "Lock mode",
RadioSettingValueList(opts, current_index=_settings.lock)))
opts = ["MH-42", "MH-48"]
basic.append(
RadioSetting(
"microphone_type", "Microphone Type",
RadioSettingValueList(
opts, current_index=_settings.microphone_type)))
opts = ["off"] + ["S-%d" % n for n in range(2, 10)] + ["S-Full"]
basic.append(RadioSetting(
"rf_sql", "RF Squelch",
RadioSettingValueList(opts, current_index=_settings.rf_sql)))
opts = ["time", "hold", "busy"]
basic.append(
RadioSetting(
"scan_resume", "Scan Resume",
RadioSettingValueList(
opts, current_index=_settings.scan_resume)))
opts = ["single", "continuous"]
basic.append(
RadioSetting(
"smart_search", "Smart Search",
RadioSettingValueList(
opts, current_index=_settings.smart_search)))
opts = ["off"] + ["%d" % t for t in range(1, 31)]
basic.append(RadioSetting(
"tot", "Time-out timer (mins)",
RadioSettingValueList(opts, current_index=_settings.tot)))
# dtmf tab
opts = ["50", "100", "250", "450", "750", "1000"]
dtmf.append(
RadioSetting(
"dtmf_delay", "DTMF delay (ms)",
RadioSettingValueList(
opts, current_index=_settings.dtmf_delay)))
opts = ["50", "75", "100"]
dtmf.append(
RadioSetting(
"dtmf_speed", "DTMF speed (ms)",
RadioSettingValueList(
opts, current_index=_settings.dtmf_speed)))
for i in range(16):
name = "dtmf%02d" % i
dtmfsetting = self._memobj.dtmf[i]
dtmfstr = ""
for c in dtmfsetting.memory:
if c == 0xFF:
break
if c < len(DTMFCHARSET):
dtmfstr += DTMFCHARSET[c]
LOG.debug(dtmfstr)
dtmfentry = RadioSettingValueString(0, 16, dtmfstr)
dtmfentry.set_charset(DTMFCHARSET + list(" "))
rs = RadioSetting(name, name.upper(), dtmfentry)
dtmf.append(rs)
# arts tab
opts = ["off", "in range", "always"]
arts.append(
RadioSetting(
"arts_mode", "ARTS beep",
RadioSettingValueList(
opts, current_index=_settings.arts_mode)))
opts = ["15", "25"]
arts.append(
RadioSetting(
"arts_interval", "ARTS interval",
RadioSettingValueList(
opts, current_index=_settings.arts_interval)))
arts.append(RadioSetting(
"arts_cwid_enable", "CW ID",
RadioSettingValueBoolean(_settings.arts_cwid_enable)))
_arts_cwid = self._memobj.arts_cwid
cwid = RadioSettingValueString(
0, 16, _decode_chars(_arts_cwid.get_value()))
cwid.set_charset(CHARSET)
arts.append(RadioSetting("arts_cwid", "CW ID", cwid))
# prog buttons
opts = ["WX", "Reverse", "Repeater", "SQL Off", "Lock", "Dimmer"]
prog.append(
RadioSetting(
"prog_panel_acc", "Prog Panel - Low(ACC)",
RadioSettingValueList(
opts, current_index=_settings.prog_panel_acc)))
opts = ["Reverse", "Home"]
prog.append(
RadioSetting(
"prog_tone_vm", "TONE | V/M",
RadioSettingValueList(
opts, current_index=_settings.prog_tone_vm)))
opts = ["" for n in range(26)] + \
["Priority", "Low", "Tone", "MHz", "Reverse", "Home", "Band",
"VFO/MR", "Scan", "Sql Off", "TCall", "SSCH", "ARTS", "Tone Freq",
"DCSC", "WX", "Repeater"]
prog.append(RadioSetting(
"prog_p1", "P1",
RadioSettingValueList(opts, current_index=_settings.prog_p1)))
prog.append(RadioSetting(
"prog_p2", "P2",
RadioSettingValueList(opts, current_index=_settings.prog_p2)))
prog.append(RadioSetting(
"prog_p3", "P3",
RadioSettingValueList(opts, current_index=_settings.prog_p3)))
prog.append(RadioSetting(
"prog_p4", "P4",
RadioSettingValueList(opts, current_index=_settings.prog_p4)))
return top
def set_settings(self, uisettings):
for element in uisettings:
if not isinstance(element, RadioSetting):
self.set_settings(element)
continue
if not element.changed():
continue
try:
_settings = self._memobj.settings
setting = element.get_name()
if re.match(r'dtmf\d', setting):
# set dtmf fields
dtmfstr = str(element.value).strip()
newval = []
for i in range(0, 16):
if i < len(dtmfstr):
newval.append(DTMFCHARSET.index(dtmfstr[i]))
else:
newval.append(0xFF)
LOG.debug(newval)
idx = int(setting[-2:])
_settings = self._memobj.dtmf[idx]
_settings.memory = newval
continue
if setting == "arts_cwid":
oldval = self._memobj.arts_cwid
newval = _encode_chars(newval.get_value(), 6)
self._memobj.arts_cwid = newval
continue
# normal settings
newval = element.value
oldval = getattr(_settings, setting)
LOG.debug("Setting %s(%s) <= %s" % (setting, oldval, newval))
setattr(_settings, setting, newval)
except Exception:
LOG.debug(element.get_name())
raise
MEM_FORMAT_8800_COMMON = """
#seekto 0x0009;
struct {
bbcd vfo_left[3];
} current_state;
#seekto 0x0032;
struct {
u8 ukn32;
u8 main:1,
ukn33_a:1,
ars:1,
ukn33_b:2,
vfo_band_edge:1,
ukn33_c:2;
u8 ukn34;
u8 ukn35;
u8 ukn36;
u8 ukn37;
u8 ukn38;
u8 ukn39;
u8 apo;
u8 ukn3b;
u8 ukn3c_a:3,
arts_mode:1,
ukn3c_b:2,
lockt:2;
u8 prog_p1;
u8 prog_p2;
u8 prog_p3;
u8 prog_p4;
u8 ukn41;
u8 ukn42;
u8 arts_cwid_enable:1,
lock:1,
ukn43:4,
backlight:2;
u8 ukn44a:2,
beep:1,
ukn44b:1,
key_mod:1,
ukn44c:3;
u8 ukn45a:3,
mute:2,
ukn45b:3;
u8 ukn46a:3,
mic:1,
ukn56b:4;
u8 ukn47;
u8 arts_cwid[6];
} settings;
"""
MEM_FORMAT_8800 = """
#seekto 0x%X;
struct {
u8 used:1,
unknown1:1,
wid_nar:1,
am:1,
unknown2:1,
duplex:3;
bbcd freq[3];
u8 unknown3:1,
tune_step:3,
power:2,
tmode:2;
bbcd split[3];
u8 nameused:1,
unknown5:1,
tone:6;
u8 namevalid:1,
dtcs:7;
u8 name[6];
} memory[512];
#seekto 0x%X;
struct {
u32 bitmap[16];
} bank_channels[10];
#seekto 0x51C8;
struct {
u8 skip0:2,
skip1:2,
skip2:2,
skip3:2;
} flags[256];
#seekto 0x7B48;
u8 checksum;
"""
class FT8800BankModel(FT7800BankModel):
def get_num_mappings(self):
return 10
@directory.register
class FT8800Radio(FTx800Radio):
"""Base class for Yaesu FT-8800"""
MODEL = "FT-8800"
_model = b"AH018"
_memsize = 22217
_block_lengths = [8, 22208, 1]
_memstart = 0x0000
TMODES = ["", "Tone", "TSQL", "DTCS"]
STEPS = STEPS_8800
@classmethod
def get_prompts(cls):
rp = chirp_common.RadioPrompts()
rp.pre_download = _(
"1. Turn radio off.\n"
"2. Connect cable to DATA jack.\n"
"3. Press and hold in the \"left\" [V/M] key while turning the\n"
" radio on.\n"
"4. Rotate the \"right\" DIAL knob to select \"CLONE START\".\n"
"5. Press the [SET] key. The display will disappear\n"
" for a moment, then the \"CLONE\" notation will appear.\n"
"6. After clicking OK , press the \"left\" [V/M] key to\n"
" send image.\n")
rp.pre_upload = _(
"1. Turn radio off.\n"
"2. Connect cable to DATA jack.\n"
"3. Press and hold in the \"left\" [V/M] key while turning the\n"
" radio on.\n"
"4. Rotate the \"right\" DIAL knob to select \"CLONE START\".\n"
"5. Press the [SET] key. The display will disappear\n"
" for a moment, then the \"CLONE\" notation will appear.\n"
"6. Press the \"left\" [LOW] key (\"CLONE -RX-\" will appear"
" on\n"
" the display).\n")
return rp
def get_features(self):
rf = FTx800Radio.get_features(self)
rf.has_sub_devices = self.VARIANT == ""
rf.has_bank = True
rf.memory_bounds = (1, 512)
rf.has_settings = True
return rf
def get_sub_devices(self):
return [FT8800RadioLeft(self._mmap), FT8800RadioRight(self._mmap)]
def get_bank_model(self):
if not hasattr(self, '_banks'):
self._banks = FT8800BankModel(self)
return self._banks
def _checksums(self):
return [yaesu_clone.YaesuChecksum(0x0000, 0x56C7)]
def process_mmap(self):
if not self._memstart:
self._memobj = bitwise.parse(MEM_FORMAT_8800_COMMON, self._mmap)
return
# Memory format for the sub devices
self._memobj = bitwise.parse(MEM_FORMAT_8800 % (self._memstart,
self._bankstart),
self._mmap)
def _get_mem_offset(self, mem, _mem):
if mem.duplex == "split":
return get_freq(int(_mem.split) * 10000)
# The offset is packed into the upper two bits of the last four
# bytes of the name (?!)
val = 0
for i in _mem.name[2:6]:
val <<= 2
val |= (i & 0xC0) >> 6
return (val * 5) * 10000
def _set_mem_offset(self, mem, _mem):
if mem.duplex == "split":
set_freq(mem.offset, _mem, "split")
return
val = int(mem.offset / 10000) // 5
for i in reversed(list(range(2, 6))):
_mem.name[i] = (_mem.name[i] & 0x3F) | ((val & 0x03) << 6)
val >>= 2
def _get_mem_name(self, mem, _mem):
name = ""
if _mem.namevalid:
for i in _mem.name:
index = int(i) & 0x3F
if index < len(CHARSET):
name += CHARSET[index]
return name.rstrip()
def _set_mem_name(self, mem, _mem):
_mem.name = [CHARSET.index(x) for x in mem.name.ljust(6)[:6]]
_mem.namevalid = 1
_mem.nameused = bool(mem.name.rstrip())
def _get_mem_mode(self, mem, _mem):
if _mem.am:
mem.mode = "AM"
else:
mem.mode = "NFM" if _mem.wid_nar else "FM"
def _set_mem_mode(self, mem, _mem):
if mem.mode == "AM":
_mem.am = True
_mem.wid_nar = False
else:
_mem.am = False
_mem.wid_nar = mem.mode == "NFM"
def get_settings(self):
_settings = self._memobj.settings
_cs = self._memobj.current_state
basic = RadioSettingGroup("basic", "Basic")
dtmf = RadioSettingGroup("dtmf", "DTMF")
arts = RadioSettingGroup("arts", "ARTS")
prog = RadioSettingGroup("prog", "Programmable Buttons")
top = RadioSettings(basic, dtmf, arts, prog)
basic.append(RadioSetting(
"apo", "APO time (hrs)",
RadioSettingValueList(
["off"] + ["%0.1f" % (t / 60.0)
for t in range(30, 750, 30)],
current_index=_settings.apo)))
basic.append(RadioSetting("beep", "Beep: Key",
RadioSettingValueBoolean(_settings.beep)))
basic.append(RadioSetting("main", "Main: active device",
RadioSettingValueList(
["Right", "Left"],
current_index=_settings.main)))
basic.append(RadioSetting("backlight", "Backlight",
RadioSettingValueList(
["off", "dim 1", "dim 2", "dim 3"],
current_index=_settings.backlight)))
basic.append(RadioSetting("vfo_band_edge",
"BAND: VFO remain within band",
RadioSettingValueList(
["BND. ON", "BND. OFF"],
current_index=_settings.vfo_band_edge)))
# 123.455 -> 0812345
# 999.9875 -> 1299998
# 155.2875 -> 1215528
# 108.0000 -> 0010800
basic.append(RadioSetting("vfo_left", "VFO: left",
RadioSettingValueFloat(
0, 9**20, int(_cs.vfo_left) / 100)))
basic.append(RadioSetting("key_mod",
("key functions for “right� band function "
"switches"),
RadioSettingValueList(
["KEY1", "KEY2"],
current_index=_settings.key_mod)))
basic.append(RadioSetting("lock", "Lock: key/button",
RadioSettingValueBoolean(_settings.lock)))
basic.append(RadioSetting("lockt", "Lock: PTT",
RadioSettingValueList(
["OFF", "BAND_R", "BAND_L", "BOTH"],
current_index=_settings.lockt)))
basic.append(RadioSetting("mute", "Audio Mute",
RadioSettingValueList(
["OFF", "TX", "RX", "TX/RX"],
current_index=_settings.mute)))
basic.append(RadioSetting("mic", "Mic: type",
RadioSettingValueList(
["MH-42", "MH-48"],
current_index=_settings.mic)))
# arts tab
arts.append(RadioSetting("arts_mode", "ARTS beep",
RadioSettingValueList(
["off", "in range", "always"],
current_index=_settings.arts_mode)))
arts.append(RadioSetting("arts_cwid_enable", "CW ID Enable",
RadioSettingValueBoolean(
_settings.arts_cwid_enable)))
cwid = RadioSettingValueString(0, 16,
_decode_chars(
_settings.arts_cwid.get_value()))
cwid.set_charset(CHARSET)
arts.append(RadioSetting("arts_cwid", "CW ID", cwid))
# Prog buttons
opts = [
"Repeater",
"Priority",
"Low",
"Tone",
"MHz",
"Reverse",
"Home",
"Band",
"VFO/MR",
"Scan",
"Sql Off",
"TCall (1750Hz)",
]
for n in range(1, 5):
prog.append(
RadioSetting(
f"prog_p{n}", f"P{n}",
RadioSettingValueList(
opts,
current_index=getattr(_settings, f"prog_p{n}") - 26)))
return top
def set_settings(self, uisettings):
for element in uisettings:
if not isinstance(element, RadioSetting):
self.set_settings(element)
continue
if not element.changed():
continue
try:
_settings = self._memobj.settings
_cs = self._memobj.current_state
setting = element.get_name()
newval = element.value
if setting.startswith("vfo_"):
setattr(_cs, setting, newval * 100)
continue
if setting == "arts_cwid":
oldval = _settings.arts_cwid
newval = _encode_chars(newval.get_value(), 6)
_settings.arts_cwid = newval
continue
if setting.startswith("prog_p"):
setattr(_settings, setting, newval + 26)
continue
# normal settings
oldval = getattr(_settings, setting)
LOG.debug("Setting %s(%s) <= %s" % (setting, oldval, newval))
setattr(_settings, setting, newval)
except Exception:
LOG.debug(element.get_name())
raise
class FT8800RadioLeft(FT8800Radio):
"""Yaesu FT-8800 Left VFO subdevice"""
VARIANT = "Left"
_memstart = 0x0948
_bankstart = 0x4BC8
def get_features(self):
rf = super().get_features()
rf.has_settings = False
return rf
class FT8800RadioRight(FT8800Radio):
"""Yaesu FT-8800 Right VFO subdevice"""
VARIANT = "Right"
_memstart = 0x2948
_bankstart = 0x4BC8
def get_features(self):
rf = super().get_features()
rf.has_settings = False
return rf
MEM_FORMAT_8900 = """
#seekto 0x0708;
struct {
u8 used:1,
skip:2,
sub_used:1,
unknown2:1,
duplex:3;
bbcd freq[3];
u8 mode:2,
nameused:1,
unknown4:1,
power:2,
tmode:2;
bbcd split[3];
u8 unknown5:2,
tone:6;
u8 namevalid:1,
dtcs:7;
u8 name[6];
} memory[799];
#seekto 0x51C8;
struct {
u8 skip0:2,
skip1:2,
skip2:2,
skip3:2;
} flags[400];
#seekto 0x7B48;
u8 checksum;
"""
@directory.register
class FT8900Radio(FTx800Radio):
"""Yaesu FT-8900"""
MODEL = "FT-8900"
_model = b"AH008"
_memsize = 14793
_block_lengths = [8, 14784, 1]
MODES = ["FM", "NFM", "AM"]
TMODES = ["", "Tone", "TSQL", "DTCS"]
STEPS = STEPS
@classmethod
def get_prompts(cls):
rp = chirp_common.RadioPrompts()
rp.pre_download = _(
"1. Turn radio off.\n"
"2. Connect cable to DATA jack.\n"
"3. Press and hold in the \"left\" [V/M] key while turning the\n"
" radio on.\n"
"4. Rotate the \"right\" DIAL knob to select \"CLONE START\".\n"
"5. Press the [SET] key. The display will disappear\n"
" for a moment, then the \"CLONE\" notation will appear.\n"
"6. After clicking OK , press the \"left\" [V/M] key to\n"
" send image.\n")
rp.pre_upload = _(
"1. Turn radio off.\n"
"2. Connect cable to DATA jack.\n"
"3. Press and hold in the \"left\" [V/M] key while turning the\n"
" radio on.\n"
"4. Rotate the \"right\" DIAL knob to select \"CLONE START\".\n"
"5. Press the [SET] key. The display will disappear\n"
" for a moment, then the \"CLONE\" notation will appear.\n"
"6. Press the \"left\" [LOW] key (\"CLONE -RX-\" will appear"
" on\n"
" the display).\n")
return rp
def process_mmap(self):
self._memobj = bitwise.parse(MEM_FORMAT_8900, self._mmap)
def get_features(self):
rf = FTx800Radio.get_features(self)
rf.valid_modes = self.MODES
rf.valid_bands = [(28000000, 29700000),
(50000000, 54000000),
(108000000, 180000000),
(320000000, 480000000),
(700000000, 985000000)]
rf.memory_bounds = (1, 799)
rf.has_tuning_step = False
return rf
def _checksums(self):
return [yaesu_clone.YaesuChecksum(0x0000, 0x39C7)]
def _get_mem_offset(self, mem, _mem):
if mem.duplex == "split":
return get_freq(int(_mem.split) * 10000)
# The offset is packed into the upper two bits of the last four
# bytes of the name (?!)
val = 0
for i in _mem.name[2:6]:
val <<= 2
val |= ((i & 0xC0) >> 6)
return (val * 5) * 10000
def _set_mem_offset(self, mem, _mem):
if mem.duplex == "split":
set_freq(mem.offset, _mem, "split")
return
val = int(mem.offset / 10000) // 5
for i in reversed(list(range(2, 6))):
_mem.name[i] = (_mem.name[i] & 0x3F) | ((val & 0x03) << 6)
val >>= 2
def _get_mem_name(self, mem, _mem):
name = ""
if _mem.namevalid:
for i in _mem.name:
index = int(i) & 0x3F
if index < len(CHARSET):
name += CHARSET[index]
return name.rstrip()
def _set_mem_name(self, mem, _mem):
_mem.name = [CHARSET.index(x) for x in mem.name.ljust(6)[:6]]
_mem.namevalid = 1
_mem.nameused = bool(mem.name.rstrip())
def _get_mem_skip(self, mem, _mem):
return SKIPS[_mem.skip]
def _set_mem_skip(self, mem, _mem):
_mem.skip = SKIPS.index(mem.skip)
def set_memory(self, mem):
FT8800Radio.set_memory(self, mem)
# The 8900 has a bit flag that tells the radio whether or not
# the memory should show up on the sub (right) band
_mem = self._memobj.memory[mem.number - 1]
if mem.freq < 108000000 or mem.freq > 480000000:
_mem.sub_used = 0
else:
_mem.sub_used = 1
chirp-20261030/chirp/drivers/ft8000r.py 0000644 0001750 0001750 00000057710 15270732464 016316 0 ustar hibby hibby # Copyright 2026 FT-8000 driver contributors
#
# 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 .
"""FT-8000 clone transport and memory editing.
Validated with a physical radio and CH340 cable: 1860 bytes, 9600 8N2,
individual byte writes with 10 ms requested gaps and cable echo. The generic
Yaesu ACK/block transport is not applicable. Unmapped settings stay opaque.
"""
import logging
import time
from chirp import bitwise, chirp_common, directory, errors, memmap
from chirp.settings import (MemSetting, RadioSetting, RadioSettingGroup,
RadioSettings, RadioSettingValueMap,
RadioSettingValueBoolean, RadioSettingValueList,
RadioSettingValueString)
LOG = logging.getLogger(__name__)
IMAGE_SIZE = 1860
HEADER = bytes.fromhex('00 01 26 21 00')
SENTINELS = {'Left': 145000000, 'Right': 433000000}
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, 162.2, 167.9, 173.8, 179.9, 186.2, 192.8, 203.5,
210.7, 218.1, 225.7, 233.6, 241.8, 250.3]
DUPLEXES = ['', '+', '-', 'split']
TMODES = ['', 'Tone', 'TSQL']
SPECIALS = ['1L', '1U', '2L', '2U', 'HOME', 'VFO']
RECORD_FORMAT = """
struct channel {
u8 masked:1,
skip:1,
duplex:2,
tx_step:2,
rx_step:2;
u8 tone_mode:2,
tone:6;
u24 freq;
u24 offset;
u8 power:2,
unknown1:3,
bell:1,
am:1,
unknown2:1;
};
"""
MEM_FORMAT = RECORD_FORMAT + """
struct controls {
u8 unknown1:3,
scan:1,
unknown2:2,
ars:1,
search:1;
};
struct autodial {
u8 count;
u8 digits[16];
};
struct band_state {
u8 memory_mode:1,
memory_tune:1,
home_mode:1,
unknown2:5;
u8 unknown3;
struct controls control;
u8 unknown4[3];
struct channel working;
struct channel vfo;
struct channel home;
u8 current_memory;
u8 unknown5[6];
struct autodial dtmf;
u8 unknown6[33];
u8 rf_sql;
u8 unknown7[4];
};
u8 header[5];
#seekto 0x000B;
struct {
u8 panel_lock:1,
key_beep:1,
unknown1:1,
same_band:1,
unknown2:1,
vhf_pair:1,
sub_off:1,
unknown3:1;
u8 unknown4:4,
dtmf_beep:1,
unknown5:1,
sub_voltmeter:1,
dtmf_speed:1;
u16 tot;
u8 dimmer;
u8 unknown6:4,
ibd:1,
ptt_lock:1,
unknown7:1,
sub_mute:1;
u8 mute_timeout;
u8 apo;
u8 volts_offset;
} settings;
#seekto 0x001B;
struct band_state right;
struct band_state right_saved;
#seekto 0x011B;
struct band_state left;
struct band_state left_saved;
#seekto 0x021F;
struct channel right_memory[50];
struct channel right_pms[4];
#seekto 0x0409;
struct channel left_memory[50];
struct channel left_pms[4];
#seekto 0x0743;
u8 checksum;
"""
# Path, label, encoded choices, displayed choices (None = boolean).
SETTINGS = [
('settings.apo', 'Auto power off', list(range(13)),
['Off'] + ['%i hour(s)' % i for i in range(1, 13)]),
('settings.tot', 'Transmit timeout', [i * 60 for i in range(61)],
['Off'] + ['%i minute(s)' % i for i in range(1, 61)]),
('settings.dimmer', 'Display brightness', list(range(8)),
['0 (darkest)', '1', '2', '3', '4', '5', '6', '7 (lightest)']),
('settings.mute_timeout', 'Reduced audio timeout', list(range(60)),
['%i minute(s)' % i for i in range(1, 61)]),
('settings.key_beep', 'Keypad beep', [1, 0], None),
('settings.dtmf_beep', 'DTMF beep', [0, 1], None),
('settings.ibd', 'Intelligent band display', [0, 1], None),
('settings.ptt_lock', 'PTT lock', [0, 1], None),
('settings.panel_lock', 'Panel lock', [0, 1], None),
('settings.dtmf_speed', 'DTMF speed', [0, 1],
['Low (5 digits/second)', 'High (9 digits/second)']),
('settings.sub_mute', 'Sub band mute', [0, 1], None),
('settings.volts_offset', 'DC voltmeter correction',
[i & 0xFF for i in range(-30, 31)],
['%+.3f V' % (i * 0.066) for i in range(-30, 31)])]
def checksum(data):
return (sum(data[5:-1]) ^ 0xA5) & 0xFF
def validate_image(data):
if len(data) != IMAGE_SIZE:
raise errors.RadioError('Expected 1860 bytes, received %i' % len(data))
if data[:5] != HEADER:
raise errors.RadioError(
'Unrecognized FT-8000 header; this development driver currently '
'supports only the hardware-verified header 00 01 26 21 00')
if data[-1] != checksum(data):
raise errors.RadioError('FT-8000 image checksum mismatch')
def decode_frequency(raw):
freq = raw * 100
return freq + 500 if freq % 10000 in (2000, 7000) else freq
def frequency_code(freq):
if freq < 280000000 and freq % 5000 == 0:
return 0
if freq % 6250 == 0:
return 1
if freq >= 280000000 and freq % 10000 == 0:
return 0
# Code 1 also covers 2.5 kHz and UHF 5 kHz endings.
return 1
@directory.register
class FT8000RRadio(chirp_common.CloneModeRadio):
VENDOR = 'Yaesu'
MODEL = 'FT-8000R'
BAUD_RATE = 9600
WANTS_DTR = False
WANTS_RTS = True
_memsize = IMAGE_SIZE
@classmethod
def get_prompts(cls):
prompts = chirp_common.RadioPrompts()
prompts.experimental = (
'CTCSS decoding and bell require the optional FTS-22 board. '
'These functions have not been verified on the test radio.')
prompts.pre_download = (
'1. Turn radio OFF. Connect the rear DATA cable.\n'
'2. Hold F/W (FIRST button below the LCD) while\n'
' turning radio ON. Release when all segments blink.\n'
'3. After clicking OK , wait for download progress.\n'
' Press RPT (THIRD button below the LCD) once.\n'
'4. After completion, turn OFF, then ON normally.\n'
' Hold no buttons. Turn OFF before unplugging cable.\n')
prompts.pre_upload = (
'1. Turn radio OFF. Connect the rear DATA cable.\n'
'2. Hold F/W (FIRST button below the LCD) while\n'
' turning radio ON. Release when all segments blink.\n'
'3. Press REV (SECOND button below the LCD) once.\n'
' The display stops blinking; radio is ready to receive.\n'
'4. Click OK. Wait about 25-35 seconds for upload.\n'
' Radio restarts on success. If needed, power OFF/ON\n'
' normally without holding any buttons.\n'
'If ERR appears, stop transfer and repeat from step 1.\n')
return prompts
def get_features(self):
features = chirp_common.RadioFeatures()
features.has_sub_devices = not bool(self.VARIANT)
features.memory_bounds = (1, 50)
features.has_bank = False
features.has_name = False
features.valid_name_length = 0
features.valid_characters = ''
features.has_settings = not bool(self.VARIANT)
features.has_tuning_step = False
features.valid_tuning_steps = [5, 10, 12.5, 15, 20, 25, 50, 100, 2.5]
features.has_nostep_tuning = False
features.has_ctone = False
features.has_dtcs = False
features.has_dtcs_polarity = False
features.can_delete = True
features.valid_tmodes = list(TMODES)
features.valid_tones = list(TONES)
features.valid_skips = ['', 'S']
features.valid_modes = ['FM', 'AM']
features.valid_duplexes = ['', '+', '-', 'split']
features.can_odd_split = True
features.valid_power_levels = self._power_levels()
features.valid_special_chans = list(SPECIALS)
features.valid_bands = ([(110000000, 280000000)]
if self.VARIANT == 'Left' else
[(280000000, 550000000),
(750000000, 1000000000)])
return features
def _power_levels(self):
return [chirp_common.PowerLevel('High', watts=(
50 if self.VARIANT == 'Left' else 35)),
chirp_common.PowerLevel('Mid', watts=10),
chirp_common.PowerLevel('Low', watts=5)]
def get_sub_devices(self):
return [FT8000RLeftRadio(self._mmap), FT8000RRightRadio(self._mmap)]
def process_mmap(self):
validate_image(self._mmap.get_packed())
self._memobj = bitwise.parse(MEM_FORMAT, self._mmap)
def _inactive_side(self):
if not self._memobj.settings.same_band:
return None
return 'right' if self._memobj.settings.vhf_pair else 'left'
def _saved_states(self):
# Same-band reception uses both live receivers for one band. The
# inactive band's state is preserved in two matching saved blocks.
side = self._inactive_side()
if side is None:
return []
other = 'left' if side == 'right' else 'right'
return [getattr(self._memobj, s + '_saved') for s in (side, other)]
def _working_records(self, number):
side = self.VARIANT.lower()
if side == self._inactive_side():
states = self._saved_states()
else:
sides = (('left', 'right')
if self._inactive_side() and number != 56 else (side,))
states = [getattr(self._memobj, s) for s in sides]
working = []
for state in states:
if state.memory_tune:
continue
if state.home_mode:
selected = 55
elif state.memory_mode:
selected = int(state.current_memory) + 1
else:
selected = 56
if selected == number:
working.append(state.working)
return working
def _get_record(self, number):
number = self._memory_number(number)
side = self.VARIANT.lower()
if 1 <= number <= 50:
return getattr(self._memobj, side + '_memory')[number - 1]
if 51 <= number <= 54:
return getattr(self._memobj, side + '_pms')[number - 51]
if number in (55, 56):
if side == self._inactive_side():
state = self._saved_states()[0]
else:
state = getattr(self._memobj, side)
return state.home if number == 55 else state.vfo
raise errors.InvalidMemoryLocation('Unknown FT-8000 memory')
@staticmethod
def _memory_number(number):
if isinstance(number, str) and number in SPECIALS:
return 51 + SPECIALS.index(number)
if not isinstance(number, int):
raise errors.InvalidMemoryLocation('Unknown FT-8000 memory')
return number
@staticmethod
def _memory_extra(record, empty=False):
extra = RadioSettingGroup('extra', 'Extra')
extra.append(MemSetting('bell', 'CTCSS bell (requires TSQL/FTS-22)',
RadioSettingValueBoolean(
False if empty else record.bell)))
extra.append(MemSetting(
'masked', 'Masked (hidden on radio)',
RadioSettingValueBoolean(False if empty else record.masked)))
return extra
def get_memory(self, number):
record = self._get_record(number)
number = self._memory_number(number)
rx = int(record.freq) * 100
used = rx and (not record.masked or rx != SENTINELS[self.VARIANT])
if number >= 55:
used = bool(rx)
memory = chirp_common.Memory(number, empty=not bool(used))
memory.extra = self._memory_extra(record, empty=memory.empty)
if number > 50:
memory.extd_number = SPECIALS[number - 51]
if number >= 55:
memory.immutable = ['empty']
memory.extra['masked'].value.set_mutable(False)
if not used:
return memory
memory.freq = decode_frequency(int(record.freq))
memory.duplex = DUPLEXES[int(record.duplex)]
memory.offset = (decode_frequency(int(record.offset))
if memory.duplex == 'split' else
int(record.offset) * 100)
memory.mode = 'AM' if record.am else 'FM'
memory.tmode = {0: '', 1: 'Tone', 2: '', 3: 'TSQL'}[
int(record.tone_mode)]
if int(record.tone) < len(TONES):
memory.rtone = TONES[int(record.tone)]
else:
LOG.warning('Unknown FT-8000 tone index %i in %s memory %i',
int(record.tone), self.VARIANT, number)
memory.skip = 'S' if record.skip else ''
memory.power = self._power_levels()[
int(record.power) if int(record.power) < 3 else 0]
return memory
def _update_checksum(self):
self._memobj.checksum = checksum(self._mmap.get_packed())
def set_memory(self, memory):
record = self._get_record(memory.number)
existing = self.get_memory(memory.number)
self.check_set_memory_immutable_policy(existing, memory)
if memory.empty:
record.masked = True
record.freq = SENTINELS[self.VARIANT] // 100
self._update_checksum()
return
# The LCD uses a separate working record. Update only receivers whose
# mode and channel index select this memory, even if others match it.
working = self._working_records(memory.number)
tx = memory.freq
if memory.duplex == '+':
tx += memory.offset
elif memory.duplex == '-':
tx -= memory.offset
elif memory.duplex == 'split':
tx = memory.offset
record.freq = memory.freq // 100
record.offset = memory.offset // 100
record.duplex = DUPLEXES.index(memory.duplex)
record.rx_step = frequency_code(memory.freq)
record.tx_step = frequency_code(tx)
record.am = memory.mode == 'AM'
record.tone_mode = {'': 0, 'Tone': 1, 'TSQL': 3}[memory.tmode]
record.tone = TONES.index(memory.rtone)
record.skip = memory.skip == 'S'
record.power = (self._power_levels().index(memory.power)
if memory.power else 0)
record.masked = False
record.bell = False
if memory.extra:
for setting in memory.extra:
setting.apply_to_memobj(record)
if memory.tmode != 'TSQL':
record.bell = False
for item in working:
item.set_raw(record.get_raw())
if memory.number in (55, 56):
if self.VARIANT.lower() == self._inactive_side():
for state in self._saved_states():
item = state.home if memory.number == 55 else state.vfo
item.set_raw(record.get_raw())
elif memory.number == 55 and self._inactive_side():
for side in ('left', 'right'):
getattr(self._memobj, side).home.set_raw(
record.get_raw())
self._update_checksum()
def get_raw_memory(self, number):
return repr(self._get_record(number))
def _settings_schema(self):
schema = list(SETTINGS)
for side in ('left', 'right'):
label = side.title()
control = (side + '_saved.control'
if side == self._inactive_side() else side + '.control')
squelch = (side + '_saved.rf_sql'
if side == self._inactive_side() else side + '.rf_sql')
schema.extend([
(control + '.ars', label + ' auto repeater shift',
[0, 1], None),
(control + '.search', label + ' smart search',
[0, 1], ['One time', 'Continuous']),
(control + '.scan', label + ' scan resume',
[0, 1], ['Carrier drop', '5 seconds']),
(squelch, label + ' RF squelch',
[0, 2, 4, 6, 11], ['Off', '2 units', '4 units', '6 units',
'Full scale'])])
return schema
def get_settings(self):
common = RadioSettingGroup('common', 'Radio settings')
for path, label, values, labels in self._settings_schema():
field = self._memobj
for part in path.split('.'):
field = getattr(field, part)
raw = int(field)
if raw not in values:
value = RadioSettingValueString(0, 64, 'Unknown: 0x%X' % raw)
value.set_mutable(False)
setting = RadioSetting(path.replace('.', '_'), label, value)
elif labels is None:
value = RadioSettingValueBoolean(
bool(values.index(raw)), mem_vals=values)
setting = MemSetting(path, label, value)
else:
value = RadioSettingValueMap(list(zip(labels, values)),
mem_val=raw)
setting = MemSetting(path, label, value)
common.append(setting)
obj = self._memobj.settings
mode = (0 if not obj.same_band else 1 if obj.vhf_pair else 2)
value = RadioSettingValueString(
0, 32, ['VHF/UHF', 'VHF/VHF', 'UHF/UHF'][mode])
value.set_mutable(False)
common.append(RadioSetting('receive_mode', 'Receive mode', value))
sub_mode = (0 if not obj.sub_off else
2 if obj.sub_voltmeter else 1)
common.append(RadioSetting(
'sub_operation', 'Sub band operation', RadioSettingValueList(
['On', 'Off', 'Voltmeter'], current_index=sub_mode)))
dtmf = RadioSettingGroup('dtmf', 'DTMF autodial')
for side in ('left', 'right'):
obj = (self._saved_states()[0].dtmf
if side == self._inactive_side() else
getattr(self._memobj, side).dtmf)
count = int(obj.count)
digits = [int(x) for x in obj.digits[0:min(count, 16)]]
if count <= 16 and all(x <= 16 for x in digits):
text = ''.join('0123456789ABCD*#P'[x] for x in digits)
value = RadioSettingValueString(0, 16, text, autopad=False)
value.set_charset('0123456789ABCD*#P')
else:
value = RadioSettingValueString(
0, 80, 'Unknown: ' + bytes(digits).hex())
value.set_mutable(False)
dtmf.append(RadioSetting(side + '_dtmf', side.title() +
' DTMF digits (P = pause)', value))
return RadioSettings(common, dtmf)
def set_settings(self, settings):
# Direct settings use MemSetting. Only multi-field/encoded settings
# need special handling.
obj = self._memobj
for setting in settings.apply_to(obj):
if not setting.changed():
continue
name = setting.get_name()
if name == 'sub_operation':
index = int(setting.value)
obj.settings.sub_off = bool(index)
obj.settings.sub_voltmeter = index == 2
elif name in ('left_dtmf', 'right_dtmf'):
digits = str(setting.value).strip().upper()
dtmf = (self._saved_states()[0].dtmf
if name == str(self._inactive_side()) + '_dtmf' else
getattr(obj, name[:-5]).dtmf)
dtmf.count = len(digits)
dtmf.digits = (['0123456789ABCD*#P'.index(x) for x in digits] +
[16] * (16 - len(digits)))
else:
raise errors.InvalidValueError('Unsupported setting: ' + name)
saved = self._saved_states()
if saved:
source, mirror = saved
for field in ('ars', 'search', 'scan'):
setattr(mirror.control, field, getattr(source.control, field))
mirror.rf_sql = source.rf_sql
mirror.dtmf.set_raw(source.dtmf.get_raw())
self._update_checksum()
def _configure_pipe(self):
self.pipe.bytesize = 8
self.pipe.parity = 'N'
self.pipe.stopbits = 2
self.pipe.xonxoff = False
self.pipe.dsrdtr = False
self.pipe.timeout = 3
def _status(self, message, count):
status = chirp_common.Status()
status.msg = message
status.cur = count
status.max = IMAGE_SIZE
self.status_fn(status)
def sync_in(self):
try:
self._download()
except errors.RadioError:
raise
except Exception as error:
LOG.exception('FT-8000R download failed')
raise errors.RadioError('Download failed: %s' % error) from error
def _download(self):
self._configure_pipe()
# Yaesu requires the operator to start the transfer. Keep the same
# serial timeout throughout: changing it may flush received data.
for attempt in range(20):
first = self.pipe.read(1)
if first:
break
else:
raise errors.RadioNoResponse()
header = first + self.pipe.read(len(HEADER) - 1)
# Some captures have this fixed two-byte preamble. Read its remaining
# bytes directly; do not scan or poll an asynchronous receive buffer.
prefix = b''
if header.startswith(b'\x00\xfc'):
prefix = header[:2]
header = header[2:] + self.pipe.read(2)
if header != HEADER:
raise errors.RadioError(
'Unrecognized or incomplete FT-8000 header')
self._metadata['ft8000_download_prefix'] = prefix.hex()
data = bytearray(header)
self._status('Downloading from FT-8000', len(data))
while len(data) < IMAGE_SIZE:
count = min(64, IMAGE_SIZE - len(data))
chunk = self.pipe.read(count)
if len(chunk) != count:
raise errors.RadioError('Incomplete FT-8000 download')
data.extend(chunk)
self._status('Downloading from FT-8000', len(data))
validate_image(data)
# No per-block or final ACK: passive download is hardware-verified.
self._mmap = memmap.MemoryMapBytes(bytes(data))
self.process_mmap()
def _write_echo(self, value):
# CHIRP's SerialTrace wrapper currently discards Serial.write's count.
# In that case the required matching one-byte echo confirms the write.
count = self.pipe.write(value)
if count is not None and count != 1:
raise errors.RadioError('Incomplete serial write; upload stopped')
self.pipe.flush()
echo = self.pipe.read(1)
if not echo:
raise errors.RadioError('Missing cable echo; upload stopped')
if echo != value:
raise errors.RadioError('Unexpected cable echo; upload stopped')
def sync_out(self):
try:
self._upload()
except errors.RadioError:
raise
except Exception as error:
LOG.exception('FT-8000R upload failed')
raise errors.RadioError('Upload failed: %s' % error) from error
def _upload(self):
data = self._mmap.get_packed()
validate_image(data) # Before touching the serial port.
self._configure_pipe()
self._status('Uploading to FT-8000', 0)
for index, value in enumerate(data):
time.sleep(0.150 if index == 0 else 0.010)
self._write_echo(bytes([value]))
if index % 16 == 0 or index == IMAGE_SIZE - 1:
self._status('Uploading to FT-8000', index + 1)
time.sleep(0.150)
self._write_echo(b'\x06')
LOG.info('FT-8000 writes and echo complete; '
'verify radio display/readback')
class FT8000RLeftRadio(FT8000RRadio):
VARIANT = 'Left'
class FT8000RRightRadio(FT8000RRadio):
VARIANT = 'Right'
chirp-20261030/chirp/drivers/ft8100.py 0000644 0001750 0001750 00000023114 14720054223 016111 0 ustar hibby hibby # Copyright 2010 Eric Allen
#
# 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 time
from chirp import chirp_common, directory, bitwise, errors
from chirp.drivers import yaesu_clone
LOG = logging.getLogger(__name__)
TONES = chirp_common.OLD_TONES
TMODES = ["", "Tone"]
MODES = ['FM', 'AM']
STEPS = [5.0, 10.0, 12.5, 15.0, 20.0, 25.0, 50.0]
DUPLEX = ["", "-", "+", "split"]
# "M" for masked memories, which are invisible until un-masked
SKIPS = ["", "S", "M"]
POWER_LEVELS_VHF = [chirp_common.PowerLevel("Low", watts=5),
chirp_common.PowerLevel("Mid", watts=20),
chirp_common.PowerLevel("High", watts=50)]
POWER_LEVELS_UHF = [chirp_common.PowerLevel("Low", watts=5),
chirp_common.PowerLevel("Mid", watts=20),
chirp_common.PowerLevel("High", watts=35)]
SPECIALS = {'1L': -1,
'1U': -2,
'2L': -3,
'2U': -4,
'Home': -5}
MEM_FORMAT = """
#seekto 0x{skips:X};
u8 skips[13];
#seekto 0x{enables:X};
u8 enables[13];
struct mem_struct {{
u8 unknown4:2,
baud9600:1,
am:1,
unknown4b:4;
u8 power:2,
duplex:2,
unknown1b:4;
u8 unknown2:1,
tone_enable:1,
tone:6;
bbcd freq[3];
bbcd offset[3];
}};
#seekto 0x{memories:X};
struct mem_struct memory[99];
"""
@directory.register
class FT8100Radio(yaesu_clone.YaesuCloneModeRadio):
"""Implementation for Yaesu FT-8100"""
MODEL = "FT-8100"
_memstart = 0
_memsize = 2968
_block_lengths = [10, 32, 114, 101, 101, 97, 128, 128, 128, 128, 128, 9, 9,
9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9,
9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9,
9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9,
9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9,
9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9,
9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9,
9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9,
9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9,
9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9,
9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9,
9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 1]
@classmethod
def get_prompts(cls):
rp = chirp_common.RadioPrompts()
rp.pre_download = _(
"1. Turn Radio off.\n"
"2. Connect data cable.\n"
"3. While holding \"F/W\" button, turn radio on.\n"
"4. After clicking OK , press \"RPT\" to send image.\n")
rp.pre_upload = _(
"1. Turn Radio off.\n"
"2. Connect data cable.\n"
"3. While holding \"F/W\" button, turn radio on.\n"
"4. Press \"REV\" to receive image.\n"
"5. Click OK to start transfer.\n")
return rp
@classmethod
def match_model(cls, data, path):
if (len(data) == cls._memsize and
data[1:10] == b'\x01\x01\x07\x08\x02\x01\x01\x00\x01'):
return True
return False
def get_features(self):
rf = chirp_common.RadioFeatures()
rf.memory_bounds = (1, 99)
rf.has_ctone = False
rf.has_dtcs = False
rf.has_dtcs_polarity = False
rf.has_bank = False
rf.has_name = False
rf.valid_tones = list(TONES)
rf.valid_modes = list(MODES)
rf.valid_tmodes = list(TMODES)
rf.valid_duplexes = list(DUPLEX)
rf.valid_power_levels = POWER_LEVELS_VHF
rf.has_sub_devices = self.VARIANT == ''
rf.valid_tuning_steps = list(STEPS)
# This is not implemented properly, so don't expose it
rf.valid_tuning_steps.remove(12.5)
# This driver doesn't properly support the upper bound of 1300MHz
# so limit us to 999MHz
if self.VARIANT == 'VHF':
rf.valid_bands = [(110000000, 280000000)]
else:
rf.valid_bands = [(280000000, 550000000),
(750000000, 999000000)]
# This is not actually implemented, so don't expose it
# rf.valid_skips = SKIPS
rf.valid_skips = []
# TODO
# rf.valid_special_chans = SPECIALS.keys()
# TODO
# rf.has_tuning_step = False
rf.can_odd_split = True
return rf
def sync_in(self):
super(FT8100Radio, self).sync_in()
self.pipe.write(bytes([yaesu_clone.CMD_ACK]))
self.pipe.read(1)
def sync_out(self):
self.update_checksums()
return _clone_out(self)
def process_mmap(self):
if not self._memstart:
return
mem_format = MEM_FORMAT.format(memories=self._memstart,
skips=self._skipstart,
enables=self._enablestart)
self._memobj = bitwise.parse(mem_format, self._mmap)
def get_sub_devices(self):
return [FT8100RadioVHF(self._mmap), FT8100RadioUHF(self._mmap)]
def get_memory(self, number):
bit, byte = self._bit_byte(number)
_mem = self._memobj.memory[number - 1]
mem = chirp_common.Memory()
mem.number = number
mem.freq = int(_mem.freq) * 1000
if _mem.tone >= len(TONES) or _mem.duplex >= len(DUPLEX):
mem.empty = True
return mem
else:
mem.rtone = TONES[_mem.tone]
mem.tmode = TMODES[_mem.tone_enable]
mem.mode = MODES[_mem.am]
mem.duplex = DUPLEX[_mem.duplex]
mem.offset = int(_mem.offset) * 1000
if mem.freq // 100 == 4:
mem.power = POWER_LEVELS_UHF[_mem.power]
else:
mem.power = POWER_LEVELS_VHF[_mem.power]
# M01 can't be disabled
if not self._memobj.enables[byte] & bit and number != 1:
mem.empty = True
elif number == 1:
mem.immutable = ['empty']
return mem
def get_raw_memory(self, number):
return repr(self._memobj.memory[number - 1])
def set_memory(self, mem):
bit, byte = self._bit_byte(mem.number)
_mem = self._memobj.memory[mem.number - 1]
_mem.freq = mem.freq // 1000
_mem.tone = TONES.index(mem.rtone)
_mem.tone_enable = TMODES.index(mem.tmode)
_mem.am = MODES.index(mem.mode)
_mem.duplex = DUPLEX.index(mem.duplex)
_mem.offset = mem.offset // 1000
if mem.power:
_mem.power = POWER_LEVELS_VHF.index(mem.power)
else:
_mem.power = 0
if mem.empty:
self._memobj.enables[byte] &= ~bit
else:
self._memobj.enables[byte] |= bit
# TODO expose these options
_mem.baud9600 = 0
# These need to be cleared, otherwise strange things happen
_mem.unknown4 = 0
_mem.unknown4b = 0
_mem.unknown1b = 0
_mem.unknown2 = 0
def _checksums(self):
return [yaesu_clone.YaesuChecksum(0x0000, 0x0B96)]
# I didn't believe this myself, but it seems that there's a bit for
# enabling VHF M01, but no bit for UHF01, and the enables are shifted down,
# so that the first bit is for M02
def _bit_byte(self, number):
if self.VARIANT == 'VHF':
bit = 1 << ((number - 1) % 8)
byte = (number - 1) // 8
else:
bit = 1 << ((number - 2) % 8)
byte = (number - 2) // 8
return bit, byte
class FT8100RadioVHF(FT8100Radio):
"""Yaesu FT-8100 VHF subdevice"""
VARIANT = "VHF"
_memstart = 0x447
_skipstart = 0x02D
_enablestart = 0x04D
class FT8100RadioUHF(FT8100Radio):
"""Yaesu FT-8100 UHF subdevice"""
VARIANT = "UHF"
_memstart = 0x7E6
_skipstart = 0x03A
_enablestart = 0x05A
def _clone_out(radio):
try:
return __clone_out(radio)
except Exception as e:
raise errors.RadioError("Failed to communicate with the radio: %s" % e)
def __clone_out(radio):
pipe = radio.pipe
block_lengths = radio._block_lengths
total_written = 0
def _status():
status = chirp_common.Status()
status.msg = "Cloning to radio"
status.max = sum(block_lengths)
status.cur = total_written
radio.status_fn(status)
start = time.time()
pos = 0
for block in radio._block_lengths:
LOG.debug("\nSending %i-%i" % (pos, pos + block))
out = radio.get_mmap()[pos:pos + block]
# need to chew byte-by-byte here or else we lose the ACK...not sure why
for b in out:
pipe.write(bytes([b]))
pipe.read(1) # chew the echo
ack = pipe.read(1)
if ack[0] != yaesu_clone.CMD_ACK:
raise Exception("block not ack'ed: %s" % repr(ack))
total_written += len(out)
_status()
pos += block
LOG.debug("Clone completed in %i seconds" % (time.time() - start))
return True
chirp-20261030/chirp/drivers/ft817.py 0000644 0001750 0001750 00000133636 15172631605 016062 0 ustar hibby hibby #
# Copyright 2012 Filippi Marco
#
# 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 .
"""FT817 - FT817ND - FT817ND/US management module"""
from chirp.drivers import yaesu_clone
from chirp import chirp_common, util, memmap, errors, directory, bitwise
from chirp.settings import RadioSetting, RadioSettingGroup, \
RadioSettingValueInteger, RadioSettingValueList, \
RadioSettingValueBoolean, RadioSettingValueString, \
RadioSettings
import time
import logging
LOG = logging.getLogger(__name__)
CMD_ACK = 0x06
@directory.register
class FT817Radio(yaesu_clone.YaesuCloneModeRadio):
"""Yaesu FT-817"""
BAUD_RATE = 9600
MODEL = "FT-817"
_model = ""
_US_model = False
DUPLEX = ["", "-", "+", "split"]
# narrow modes has to be at end
MODES = ["LSB", "USB", "CW", "CWR", "AM", "FM", "DIG", "PKT", "NCW",
"NCWR", "NFM"]
TMODES = ["", "Tone", "TSQL", "DTCS"]
STEPSFM = [5.0, 6.25, 10.0, 12.5, 15.0, 20.0, 25.0, 50.0]
STEPSAM = [2.5, 5.0, 9.0, 10.0, 12.5, 25.0]
STEPSSSB = [1.0, 2.5, 5.0]
# warning ranges has to be in this exact order
VALID_BANDS = [(100000, 33000000), (33000000, 56000000),
(76000000, 108000000), (108000000, 137000000),
(137000000, 154000000), (420000000, 470000000)]
CHARSET = list(chirp_common.CHARSET_ASCII)
CHARSET.remove("\\")
_memsize = 6509
# block 9 (130 Bytes long) is to be repeated 40 times
_block_lengths = [2, 40, 208, 182, 208, 182, 198, 53, 130, 118, 118]
MEM_FORMAT = """
struct mem_struct {
u8 tag_on_off:1,
tag_default:1,
unknown1:3,
mode:3;
u8 duplex:2,
is_duplex:1,
is_cwdig_narrow:1,
is_fm_narrow:1,
freq_range:3;
u8 skip:1,
unknown2:1,
ipo:1,
att:1,
unknown3:4;
u8 ssb_step:2,
am_step:3,
fm_step:3;
u8 unknown4:6,
tmode:2;
u8 unknown5:2,
tx_mode:3,
tx_freq_range:3;
u8 unknown6:1,
unknown_toneflag:1,
tone:6;
u8 unknown7:1,
dcs:7;
ul16 rit;
u32 freq;
u32 offset;
u8 name[8];
};
#seekto 0x4;
struct {
u8 fst:1,
lock:1,
nb:1,
pbt:1,
unknownb:1,
dsp:1,
agc:2;
u8 vox:1,
vlt:1,
bk:1,
kyr:1,
unknown5:1,
cw_paddle:1,
pwr_meter_mode:2;
u8 vfob_band_select:4,
vfoa_band_select:4;
u8 unknowna;
u8 backlight:2,
color:2,
contrast:4;
u8 beep_freq:1,
beep_volume:7;
u8 arts_beep:2,
main_step:1,
cw_id:1,
scope:1,
pkt_rate:1,
resume_scan:2;
u8 op_filter:2,
lock_mode:2,
cw_pitch:4;
u8 sql_rf_gain:1,
ars_144:1,
ars_430:1,
cw_weight:5;
u8 cw_delay;
u8 unknown8:1,
sidetone:7;
u8 batt_chg:2,
cw_speed:6;
u8 disable_amfm_dial:1,
vox_gain:7;
u8 cat_rate:2,
emergency:1,
vox_delay:5;
u8 dig_mode:3,
mem_group:1,
unknown9:1,
apo_time:3;
u8 dcs_inv:2,
unknown10:1,
tot_time:5;
u8 mic_scan:1,
ssb_mic:7;
u8 mic_key:1,
am_mic:7;
u8 unknown11:1,
fm_mic:7;
u8 unknown12:1,
dig_mic:7;
u8 extended_menu:1,
pkt_mic:7;
u8 unknown14:1,
pkt9600_mic:7;
il16 dig_shift;
il16 dig_disp;
i8 r_lsb_car;
i8 r_usb_car;
i8 t_lsb_car;
i8 t_usb_car;
u8 unknown15:2,
menu_item:6;
u8 unknown16:4,
menu_sel:4;
u16 unknown17;
u8 art:1,
scn_mode:2,
dw:1,
pri:1,
unknown18:1,
tx_power:2;
u8 spl:1,
unknown:1,
uhf_antenna:1,
vhf_antenna:1,
air_antenna:1,
bc_antenna:1,
sixm_antenna:1,
hf_antenna:1;
} settings;
#seekto 0x2A;
struct mem_struct vfoa[15];
struct mem_struct vfob[15];
struct mem_struct home[4];
struct mem_struct qmb;
struct mem_struct mtqmb;
struct mem_struct mtune;
#seekto 0x3FD;
u8 visible[25];
u8 pmsvisible;
#seekto 0x417;
u8 filled[25];
u8 pmsfilled;
#seekto 0x431;
struct mem_struct memory[200];
struct mem_struct pms[2];
#seekto 0x18cf;
u8 callsign[7];
#seekto 0x1979;
struct mem_struct sixtymeterchannels[5];
"""
_CALLSIGN_CHARSET = [chr(x) for x in list(range(ord("0"), ord("9") + 1)) +
list(range(ord("A"), ord("Z") + 1)) + [ord(" ")]]
_CALLSIGN_CHARSET_REV = dict(
list(zip(_CALLSIGN_CHARSET,
list(range(0, len(_CALLSIGN_CHARSET))))))
# WARNING Index are hard wired in memory management code !!!
SPECIAL_MEMORIES = {
"VFOa-1.8M": -35,
"VFOa-3.5M": -34,
"VFOa-7M": -33,
"VFOa-10M": -32,
"VFOa-14M": -31,
"VFOa-18M": -30,
"VFOa-21M": -29,
"VFOa-24M": -28,
"VFOa-28M": -27,
"VFOa-50M": -26,
"VFOa-FM": -25,
"VFOa-AIR": -24,
"VFOa-144": -23,
"VFOa-430": -22,
"VFOa-HF": -21,
"VFOb-1.8M": -20,
"VFOb-3.5M": -19,
"VFOb-7M": -18,
"VFOb-10M": -17,
"VFOb-14M": -16,
"VFOb-18M": -15,
"VFOb-21M": -14,
"VFOb-24M": -13,
"VFOb-28M": -12,
"VFOb-50M": -11,
"VFOb-FM": -10,
"VFOb-AIR": -9,
"VFOb-144M": -8,
"VFOb-430M": -7,
"VFOb-HF": -6,
"HOME HF": -5,
"HOME 50M": -4,
"HOME 144M": -3,
"HOME 430M": -2,
"QMB": -1,
}
FIRST_VFOB_INDEX = -6
LAST_VFOB_INDEX = -20
FIRST_VFOA_INDEX = -21
LAST_VFOA_INDEX = -35
SPECIAL_PMS = {
"PMS-L": -37,
"PMS-U": -36,
}
LAST_PMS_INDEX = -37
SPECIAL_MEMORIES.update(SPECIAL_PMS)
SPECIAL_MEMORIES_REV = dict(list(zip(list(SPECIAL_MEMORIES.values()),
list(SPECIAL_MEMORIES.keys()))))
@classmethod
def get_prompts(cls):
rp = chirp_common.RadioPrompts()
rp.pre_download = _(
"1. Turn radio off.\n"
"2. Connect cable to ACC jack.\n"
"3. Press and hold in the [MODE <] and [MODE >] keys"
" while\n"
" turning the radio on (\"CLONE MODE\" will appear on the\n"
" display).\n"
"4. After clicking OK , press the [A] key to send image.\n")
rp.pre_upload = _(
"1. Turn radio off.\n"
"2. Connect cable to ACC jack.\n"
"3. Press and hold in the [MODE <] and [MODE >] keys"
" while\n"
" turning the radio on (\"CLONE MODE\" will appear on the\n"
" display).\n"
"4. Press the [C] key (\"RX\" will appear on the LCD).\n")
return rp
def _read(self, block, blocknum, lastblock):
# be very patient at first block
if blocknum == 0:
attempts = 60
else:
attempts = 5
for _i in range(0, attempts):
data = self.pipe.read(block + 2)
if data:
break
time.sleep(0.5)
if not data:
raise errors.RadioNoResponse()
if len(data) == block + 2 and data[0] == blocknum:
checksum = yaesu_clone.YaesuChecksum(1, block)
if checksum.get_existing(data) != \
checksum.get_calculated(data):
raise Exception("Checksum Failed [%02X<>%02X] block %02X" %
(checksum.get_existing(data),
checksum.get_calculated(data), blocknum))
# Chew away the block number and the checksum
data = data[1:block + 1]
else:
if lastblock and self._US_model:
raise Exception(_("Unable to read last block. "
"This often happens when the selected model "
"is US but the radio is a non-US one (or "
"widebanded). Please choose the correct "
"model and try again."))
else:
raise Exception("Unable to read block %02X expected %i got %i"
% (blocknum, block + 2, len(data)))
LOG.debug("Read %i" % len(data))
return data
def _clone_in(self):
# Be very patient with the radio
self.pipe.timeout = 2
start = time.time()
data = bytes(b"")
blocks = 0
status = chirp_common.Status()
status.msg = _("Cloning from radio")
nblocks = len(self._block_lengths) + 39
status.max = nblocks
for block in self._block_lengths:
if blocks == 8:
# repeated read of 40 block same size (memory area)
repeat = 40
else:
repeat = 1
for _i in range(0, repeat):
data += self._read(block, blocks, blocks == nblocks - 1)
self.pipe.write(bytes([CMD_ACK]))
blocks += 1
status.cur = blocks
self.status_fn(status)
if not self._US_model:
status.msg = _("Clone completed, checking for spurious bytes")
self.status_fn(status)
moredata = self.pipe.read(2)
if moredata:
raise Exception(
_("Radio sent data after the last awaited block, "
"this happens when the selected model is a non-US "
"but the radio is a US one. "
"Please choose the correct model and try again."))
LOG.info("Clone completed in %i seconds" % (time.time() - start))
return memmap.MemoryMapBytes(data)
def _clone_out(self):
delay = 0.5
start = time.time()
blocks = 0
pos = 0
status = chirp_common.Status()
status.msg = _("Cloning to radio")
status.max = len(self._block_lengths) + 39
mmap = self.get_mmap().get_byte_compatible()
for block in self._block_lengths:
if blocks == 8:
# repeated read of 40 block same size (memory area)
repeat = 40
else:
repeat = 1
for _i in range(0, repeat):
time.sleep(0.01)
checksum = yaesu_clone.YaesuChecksum(pos, pos + block - 1)
LOG.debug("Block %i - will send from %i to %i byte " %
(blocks, pos, pos + block))
LOG.debug(util.hexprint(chr(blocks)))
LOG.debug(util.hexprint(self.get_mmap()[pos:pos + block]))
LOG.debug(util.hexprint(chr(checksum.get_calculated(mmap))))
self.pipe.write(bytes([blocks]))
self.pipe.write(mmap[pos:pos + block])
self.pipe.write(bytes([checksum.get_calculated(
self.get_mmap())]))
buf = self.pipe.read(1)
if not buf or buf[0] != CMD_ACK:
time.sleep(delay)
buf = self.pipe.read(1)
if not buf:
raise errors.RadioNoResponse()
if buf[0] != CMD_ACK:
LOG.debug(util.hexprint(buf))
raise Exception(_("Radio did not ack block %i") % blocks)
pos += block
blocks += 1
status.cur = blocks
self.status_fn(status)
LOG.info("Clone completed in %i seconds" % (time.time() - start))
def sync_in(self):
try:
self._mmap = self._clone_in()
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._clone_out()
except errors.RadioError:
raise
except Exception as e:
raise errors.RadioError("Failed to communicate with radio: %s" % e)
def process_mmap(self):
self._memobj = bitwise.parse(self.MEM_FORMAT, self._mmap)
def get_features(self):
rf = chirp_common.RadioFeatures()
rf.has_bank = False
rf.has_dtcs_polarity = False
rf.has_nostep_tuning = True
rf.valid_modes = list(set(self.MODES))
rf.valid_tmodes = list(self.TMODES)
rf.valid_duplexes = list(self.DUPLEX)
rf.valid_tuning_steps = list(self.STEPSFM)
rf.valid_bands = self.VALID_BANDS
rf.valid_skips = ["", "S"]
rf.valid_power_levels = []
rf.valid_characters = "".join(self.CHARSET)
rf.valid_name_length = 8
rf.valid_special_chans = sorted(self.SPECIAL_MEMORIES.keys())
rf.memory_bounds = (1, 200)
rf.can_odd_split = True
rf.has_ctone = False
rf.has_settings = True
return rf
def get_raw_memory(self, number):
return repr(self._memobj.memory[number - 1])
def _get_duplex(self, mem, _mem):
if _mem.is_duplex == 1:
mem.duplex = self.DUPLEX[_mem.duplex]
else:
mem.duplex = ""
def _get_tmode(self, mem, _mem):
mem.tmode = self.TMODES[_mem.tmode]
mem.rtone = chirp_common.TONES[_mem.tone]
mem.dtcs = chirp_common.DTCS_CODES[_mem.dcs]
def _set_duplex(self, mem, _mem):
_mem.duplex = self.DUPLEX.index(mem.duplex)
_mem.is_duplex = mem.duplex != ""
def _set_tmode(self, mem, _mem):
_mem.tmode = self.TMODES.index(mem.tmode)
# have to put this bit to 0 otherwise we get strange display in tone
# frequency (menu 83). See bug #88 and #163
_mem.unknown_toneflag = 0
_mem.tone = chirp_common.TONES.index(mem.rtone)
_mem.dcs = chirp_common.DTCS_CODES.index(mem.dtcs)
def get_memory(self, number):
if isinstance(number, str):
return self._get_special(number)
elif number < 0:
# I can't stop delete operation from losing extd_number but
# I know how to get it back
return self._get_special(self.SPECIAL_MEMORIES_REV[number])
else:
return self._get_normal(number)
def set_memory(self, memory):
if memory.number < 0:
return self._set_special(memory)
else:
return self._set_normal(memory)
def _get_special(self, number):
mem = chirp_common.Memory()
mem.number = self.SPECIAL_MEMORIES[number]
mem.extd_number = number
if mem.number in range(self.FIRST_VFOA_INDEX,
self.LAST_VFOA_INDEX - 1,
-1):
_mem = self._memobj.vfoa[-self.LAST_VFOA_INDEX + mem.number]
immutable = ["number", "skip", "extd_number",
"name", "dtcs_polarity", "power"]
elif mem.number in range(self.FIRST_VFOB_INDEX,
self.LAST_VFOB_INDEX - 1,
-1):
_mem = self._memobj.vfob[-self.LAST_VFOB_INDEX + mem.number]
immutable = ["number", "skip", "extd_number",
"name", "dtcs_polarity", "power"]
elif mem.number in range(-2, -6, -1):
_mem = self._memobj.home[5 + mem.number]
immutable = ["number", "skip", "extd_number",
"name", "dtcs_polarity", "power"]
elif mem.number == -1:
_mem = self._memobj.qmb
immutable = ["number", "skip", "extd_number",
"name", "dtcs_polarity", "power"]
elif mem.number in list(self.SPECIAL_PMS.values()):
bitindex = -self.LAST_PMS_INDEX + mem.number
used = (self._memobj.pmsvisible >> bitindex) & 0x01
valid = (self._memobj.pmsfilled >> bitindex) & 0x01
if not used:
mem.empty = True
if not valid:
mem.empty = True
return mem
_mem = self._memobj.pms[-self.LAST_PMS_INDEX + mem.number]
immutable = ["number", "skip", "rtone", "ctone", "extd_number",
"dtcs", "tmode", "cross_mode", "dtcs_polarity",
"power", "duplex", "offset"]
else:
raise Exception("Sorry, special memory index %i " % mem.number +
"unknown you hit a bug!!")
mem = self._get_memory(mem, _mem)
mem.immutable = immutable
return mem
def _set_special(self, mem):
if mem.empty and mem.number not in list(self.SPECIAL_PMS.values()):
# can't delete special memories!
raise Exception("Sorry, special memory can't be deleted")
cur_mem = self._get_special(self.SPECIAL_MEMORIES_REV[mem.number])
# TODO add frequency range check for vfo and home memories
if mem.number in range(self.FIRST_VFOA_INDEX,
self.LAST_VFOA_INDEX - 1,
-1):
_mem = self._memobj.vfoa[-self.LAST_VFOA_INDEX + mem.number]
elif mem.number in range(self.FIRST_VFOB_INDEX,
self.LAST_VFOB_INDEX - 1,
-1):
_mem = self._memobj.vfob[-self.LAST_VFOB_INDEX + mem.number]
elif mem.number in range(-2, -6, -1):
_mem = self._memobj.home[5 + mem.number]
elif mem.number == -1:
_mem = self._memobj.qmb
elif mem.number in list(self.SPECIAL_PMS.values()):
# this case has to be last because 817 pms keys overlap with
# 857 derived class other special memories
bitindex = -self.LAST_PMS_INDEX + mem.number
wasused = (self._memobj.pmsvisible >> bitindex) & 0x01
wasvalid = (self._memobj.pmsfilled >> bitindex) & 0x01
if mem.empty:
if wasvalid and not wasused:
# pylint get confused by &= operator
self._memobj.pmsfilled = self._memobj.pmsfilled & \
~ (1 << bitindex)
# pylint get confused by &= operator
self._memobj.pmsvisible = self._memobj.pmsvisible & \
~ (1 << bitindex)
return
# pylint get confused by |= operator
self._memobj.pmsvisible = self._memobj.pmsvisible | 1 << bitindex
self._memobj.pmsfilled = self._memobj.pmsfilled | 1 << bitindex
_mem = self._memobj.pms[-self.LAST_PMS_INDEX + mem.number]
else:
raise Exception("Sorry, special memory index %i " % mem.number +
"unknown you hit a bug!!")
for key in cur_mem.immutable:
if key != "extd_number":
if cur_mem.__dict__[key] != mem.__dict__[key]:
raise errors.RadioError("Editing field `%s' " % key +
"is not supported on this channel")
self._set_memory(mem, _mem)
def _get_normal(self, number):
_mem = self._memobj.memory[number - 1]
used = (self._memobj.visible[(number - 1) / 8] >> (number - 1) % 8) \
& 0x01
valid = (self._memobj.filled[(number - 1) / 8] >> (number - 1) % 8) \
& 0x01
mem = chirp_common.Memory()
mem.number = number
if not used:
mem.empty = True
if not valid or _mem.freq == 0xffffffff:
return mem
if mem.number == 1:
mem.immutable = ['empty']
return self._get_memory(mem, _mem)
def _set_normal(self, mem):
_mem = self._memobj.memory[mem.number - 1]
wasused = (self._memobj.visible[(mem.number - 1) / 8] >>
(mem.number - 1) % 8) & 0x01
wasvalid = (self._memobj.filled[(mem.number - 1) / 8] >>
(mem.number - 1) % 8) & 0x01
if mem.empty:
if mem.number == 1:
# as Dan says "yaesus are not good about that :("
# if you upload an empty image you can brick your radio
raise Exception("Sorry, can't delete first memory")
if wasvalid and not wasused:
self._memobj.filled[(mem.number - 1) // 8] &= \
~(1 << (mem.number - 1) % 8)
_mem.set_raw("\xFF" * (_mem.size() // 8)) # clean up
self._memobj.visible[(mem.number - 1) // 8] &= \
~(1 << (mem.number - 1) % 8)
return
if not wasvalid:
_mem.set_raw("\x00" * (_mem.size() // 8)) # clean up
self._memobj.visible[(mem.number - 1) // 8] |= (
1 << (mem.number - 1) % 8)
self._memobj.filled[(mem.number - 1) // 8] |= (
1 << (mem.number - 1) % 8)
self._set_memory(mem, _mem)
def _get_memory(self, mem, _mem):
mem.freq = int(_mem.freq) * 10
mem.offset = int(_mem.offset) * 10
self._get_duplex(mem, _mem)
mem.mode = self.MODES[_mem.mode]
if mem.mode == "FM":
if _mem.is_fm_narrow == 1:
mem.mode = "NFM"
mem.tuning_step = self.STEPSFM[_mem.fm_step]
elif mem.mode == "AM":
mem.tuning_step = self.STEPSAM[_mem.am_step]
elif mem.mode == "CW" or mem.mode == "CWR":
if _mem.is_cwdig_narrow == 1:
mem.mode = "N" + mem.mode
mem.tuning_step = self.STEPSSSB[_mem.ssb_step]
else:
try:
mem.tuning_step = self.STEPSSSB[_mem.ssb_step]
except IndexError:
pass
mem.skip = _mem.skip and "S" or ""
self._get_tmode(mem, _mem)
if _mem.tag_on_off == 1:
for i in _mem.name:
if i == 0xFF:
break
if chr(i) in self.CHARSET:
mem.name += chr(i)
else:
# radio have some graphical chars that are not supported
# we replace those with a *
LOG.info("Replacing char %x with *" % i)
mem.name += "*"
mem.name = mem.name.rstrip()
else:
mem.name = ""
mem.extra = RadioSettingGroup("extra", "Extra")
ipo = RadioSetting("ipo", "IPO",
RadioSettingValueBoolean(bool(_mem.ipo)))
ipo.set_doc("Bypass preamp")
mem.extra.append(ipo)
att = RadioSetting("att", "ATT",
RadioSettingValueBoolean(bool(_mem.att)))
att.set_doc("10dB front end attenuator")
mem.extra.append(att)
return mem
def _set_memory(self, mem, _mem):
# not supported in chirp so I make label visible if have one
if len(mem.name) > 0:
_mem.tag_on_off = 1
else:
_mem.tag_on_off = 0
_mem.tag_default = 0 # never use default label "CH-nnn"
self._set_duplex(mem, _mem)
if mem.mode[0] == "N": # is it narrow?
_mem.mode = self.MODES.index(mem.mode[1:])
# here I suppose it's safe to set both
_mem.is_fm_narrow = _mem.is_cwdig_narrow = 1
else:
_mem.mode = self.MODES.index(mem.mode)
# here I suppose it's safe to set both
_mem.is_fm_narrow = _mem.is_cwdig_narrow = 0
i = 0
for lo, hi in self.VALID_BANDS:
if mem.freq > lo and mem.freq < hi:
break
i += 1
_mem.freq_range = i
# all this should be safe also when not in split but ...
if mem.duplex == "split":
_mem.tx_mode = _mem.mode
i = 0
for lo, hi in self.VALID_BANDS:
if mem.offset >= lo and mem.offset < hi:
break
i += 1
_mem.tx_freq_range = i
_mem.skip = mem.skip == "S"
self._set_tmode(mem, _mem)
try:
_mem.ssb_step = self.STEPSSSB.index(mem.tuning_step)
except ValueError:
pass
try:
_mem.am_step = self.STEPSAM.index(mem.tuning_step)
except ValueError:
pass
try:
_mem.fm_step = self.STEPSFM.index(mem.tuning_step)
except ValueError:
pass
_mem.rit = 0 # not supported in chirp
_mem.freq = mem.freq / 10
_mem.offset = mem.offset / 10
# there are ft857D that have problems with short labels, see bug #937
# some of the radio fill with 0xff and some with blanks
# the latter is safe for all ft8x7 radio
# so why should i do it only for some?
for i in range(0, 8):
_mem.name[i] = ord(mem.name.ljust(8)[i])
for setting in mem.extra:
setattr(_mem, setting.get_name(), setting.value)
def validate_memory(self, mem):
msgs = yaesu_clone.YaesuCloneModeRadio.validate_memory(self, mem)
lo, hi = self.VALID_BANDS[2] # this is fm broadcasting
if mem.freq >= lo and mem.freq <= hi:
if mem.mode != "FM":
msgs.append(chirp_common.ValidationError(
"Only FM is supported in this band"))
# TODO check that step is valid in current mode
return msgs
@classmethod
def match_model(cls, filedata, filename):
return len(filedata) == cls._memsize
def get_settings(self):
_settings = self._memobj.settings
basic = RadioSettingGroup("basic", "Basic")
cw = RadioSettingGroup("cw", "CW")
packet = RadioSettingGroup("packet", "Digital & packet")
panel = RadioSettingGroup("panel", "Panel settings")
extended = RadioSettingGroup("extended", "Extended")
antenna = RadioSettingGroup("antenna", "Antenna selection")
panelcontr = RadioSettingGroup("panelcontr", "Panel controls")
top = RadioSettings(basic, cw, packet,
panelcontr, panel, extended, antenna)
rs = RadioSetting("ars_144", "144 ARS",
RadioSettingValueBoolean(_settings.ars_144))
basic.append(rs)
rs = RadioSetting("ars_430", "430 ARS",
RadioSettingValueBoolean(_settings.ars_430))
basic.append(rs)
rs = RadioSetting("pkt9600_mic", "Packet 9600 mic level",
RadioSettingValueInteger(0, 100,
_settings.pkt9600_mic))
packet.append(rs)
options = ["enable", "disable"]
rs = RadioSetting(
"disable_amfm_dial", "AM&FM Dial",
RadioSettingValueList(
options, current_index=_settings.disable_amfm_dial))
panel.append(rs)
rs = RadioSetting("am_mic", "AM mic level",
RadioSettingValueInteger(0, 100, _settings.am_mic))
basic.append(rs)
options = ["OFF", "1h", "2h", "3h", "4h", "5h", "6h"]
rs = RadioSetting(
"apo_time", "APO time",
RadioSettingValueList(
options, current_index=_settings.apo_time))
basic.append(rs)
options = ["OFF", "Range", "All"]
rs = RadioSetting(
"arts_beep", "ARTS beep",
RadioSettingValueList(
options, current_index=_settings.arts_beep))
basic.append(rs)
options = ["OFF", "ON", "Auto"]
rs = RadioSetting(
"backlight", "Backlight",
RadioSettingValueList(
options, current_index=_settings.backlight))
panel.append(rs)
options = ["6h", "8h", "10h"]
rs = RadioSetting(
"batt_chg", "Battery charge",
RadioSettingValueList(
options, current_index=_settings.batt_chg))
basic.append(rs)
options = ["440 Hz", "880 Hz"]
rs = RadioSetting(
"beep_freq", "Beep frequency",
RadioSettingValueList(
options, current_index=_settings.beep_freq))
panel.append(rs)
rs = RadioSetting("beep_volume", "Beep volume",
RadioSettingValueInteger(0, 100,
_settings.beep_volume))
panel.append(rs)
options = ["4800", "9600", "38400"]
rs = RadioSetting(
"cat_rate", "CAT rate",
RadioSettingValueList(
options, current_index=_settings.cat_rate))
basic.append(rs)
options = ["Blue", "Amber", "Violet"]
rs = RadioSetting("color", "Color",
RadioSettingValueList(options,
current_index=_settings.color))
panel.append(rs)
rs = RadioSetting("contrast", "Contrast",
RadioSettingValueInteger(1, 12,
_settings.contrast - 1))
panel.append(rs)
rs = RadioSetting("cw_delay", "CW delay (*10 ms)",
RadioSettingValueInteger(1, 250,
_settings.cw_delay))
cw.append(rs)
rs = RadioSetting("cw_id", "CW id",
RadioSettingValueBoolean(_settings.cw_id))
cw.append(rs)
options = ["Normal", "Reverse"]
rs = RadioSetting(
"cw_paddle", "CW paddle",
RadioSettingValueList(
options, current_index=_settings.cw_paddle))
cw.append(rs)
options = ["%i Hz" % i for i in range(300, 1001, 50)]
rs = RadioSetting(
"cw_pitch", "CW pitch",
RadioSettingValueList(
options, current_index=_settings.cw_pitch))
cw.append(rs)
options = ["%i wpm" % i for i in range(4, 61)]
rs = RadioSetting(
"cw_speed", "CW speed",
RadioSettingValueList(
options, current_index=_settings.cw_speed))
cw.append(rs)
options = ["1:%1.1f" % (i / 10) for i in range(25, 46, 1)]
rs = RadioSetting(
"cw_weight", "CW weight",
RadioSettingValueList(
options, current_index=_settings.cw_weight))
cw.append(rs)
rs = RadioSetting("dig_disp", "Dig disp (*10 Hz)",
RadioSettingValueInteger(-300, 300,
_settings.dig_disp))
packet.append(rs)
rs = RadioSetting("dig_mic", "Dig mic",
RadioSettingValueInteger(0, 100,
_settings.dig_mic))
packet.append(rs)
options = ["RTTY", "PSK31-L", "PSK31-U", "USER-L", "USER-U"]
rs = RadioSetting(
"dig_mode", "Dig mode",
RadioSettingValueList(
options, current_index=_settings.dig_mode))
packet.append(rs)
rs = RadioSetting("dig_shift", "Dig shift (*10 Hz)",
RadioSettingValueInteger(-300, 300,
_settings.dig_shift))
packet.append(rs)
rs = RadioSetting("fm_mic", "FM mic",
RadioSettingValueInteger(0, 100,
_settings.fm_mic))
basic.append(rs)
options = ["Dial", "Freq", "Panel"]
rs = RadioSetting(
"lock_mode", "Lock mode",
RadioSettingValueList(
options, current_index=_settings.lock_mode))
panel.append(rs)
options = ["Fine", "Coarse"]
rs = RadioSetting(
"main_step", "Main step",
RadioSettingValueList(
options, current_index=_settings.main_step))
panel.append(rs)
rs = RadioSetting("mem_group", "Mem group",
RadioSettingValueBoolean(_settings.mem_group))
basic.append(rs)
rs = RadioSetting("mic_key", "Mic key",
RadioSettingValueBoolean(_settings.mic_key))
cw.append(rs)
rs = RadioSetting("mic_scan", "Mic scan",
RadioSettingValueBoolean(_settings.mic_scan))
basic.append(rs)
options = ["Off", "SSB", "CW"]
rs = RadioSetting(
"op_filter", "Optional filter",
RadioSettingValueList(
options, current_index=_settings.op_filter))
basic.append(rs)
rs = RadioSetting("pkt_mic", "Packet mic",
RadioSettingValueInteger(0, 100, _settings.pkt_mic))
packet.append(rs)
options = ["1200", "9600"]
rs = RadioSetting(
"pkt_rate", "Packet rate",
RadioSettingValueList(
options, current_index=_settings.pkt_rate))
packet.append(rs)
options = ["Off", "3 sec", "5 sec", "10 sec"]
rs = RadioSetting(
"resume_scan", "Resume scan",
RadioSettingValueList(
options, current_index=_settings.resume_scan))
basic.append(rs)
options = ["Cont", "Chk"]
rs = RadioSetting("scope", "Scope",
RadioSettingValueList(options,
current_index=_settings.scope))
basic.append(rs)
rs = RadioSetting("sidetone", "Sidetone",
RadioSettingValueInteger(0, 100, _settings.sidetone))
cw.append(rs)
options = ["RF-Gain", "Squelch"]
rs = RadioSetting(
"sql_rf_gain", "Squelch/RF-Gain",
RadioSettingValueList(
options, current_index=_settings.sql_rf_gain))
panel.append(rs)
rs = RadioSetting("ssb_mic", "SSB Mic",
RadioSettingValueInteger(0, 100, _settings.ssb_mic))
basic.append(rs)
options = ["%i" % i for i in range(0, 21)]
options[0] = "Off"
rs = RadioSetting(
"tot_time", "Time-out timer",
RadioSettingValueList(
options, current_index=_settings.tot_time))
basic.append(rs)
rs = RadioSetting("vox_delay", "VOX delay (*100 ms)",
RadioSettingValueInteger(1, 25, _settings.vox_delay))
basic.append(rs)
rs = RadioSetting("vox_gain", "VOX Gain",
RadioSettingValueInteger(0, 100, _settings.vox_gain))
basic.append(rs)
rs = RadioSetting("extended_menu", "Extended menu",
RadioSettingValueBoolean(_settings.extended_menu))
extended.append(rs)
options = ["Tn-Rn", "Tn-Riv", "Tiv-Rn", "Tiv-Riv"]
rs = RadioSetting("dcs_inv", "DCS coding", RadioSettingValueList(
options, current_index=_settings.dcs_inv))
extended.append(rs)
rs = RadioSetting("r_lsb_car", "LSB Rx carrier point (*10 Hz)",
RadioSettingValueInteger(-30, 30,
_settings.r_lsb_car))
extended.append(rs)
rs = RadioSetting("r_usb_car", "USB Rx carrier point (*10 Hz)",
RadioSettingValueInteger(-30, 30,
_settings.r_usb_car))
extended.append(rs)
rs = RadioSetting("t_lsb_car", "LSB Tx carrier point (*10 Hz)",
RadioSettingValueInteger(-30, 30,
_settings.t_lsb_car))
extended.append(rs)
rs = RadioSetting("t_usb_car", "USB Tx carrier point (*10 Hz)",
RadioSettingValueInteger(-30, 30,
_settings.t_usb_car))
extended.append(rs)
options = ["Hi", "L3", "L2", "L1"]
rs = RadioSetting(
"tx_power", "TX power",
RadioSettingValueList(
options, current_index=_settings.tx_power))
basic.append(rs)
options = ["Front", "Rear"]
rs = RadioSetting(
"hf_antenna", "HF",
RadioSettingValueList(
options, current_index=_settings.hf_antenna))
antenna.append(rs)
rs = RadioSetting(
"sixm_antenna", "6M",
RadioSettingValueList(
options, current_index=_settings.sixm_antenna))
antenna.append(rs)
rs = RadioSetting(
"bc_antenna", "Broadcasting",
RadioSettingValueList(
options, current_index=_settings.bc_antenna))
antenna.append(rs)
rs = RadioSetting(
"air_antenna", "Air band",
RadioSettingValueList(
options, current_index=_settings.air_antenna))
antenna.append(rs)
rs = RadioSetting(
"vhf_antenna", "VHF",
RadioSettingValueList(
options, current_index=_settings.vhf_antenna))
antenna.append(rs)
rs = RadioSetting(
"uhf_antenna", "UHF",
RadioSettingValueList(
options, current_index=_settings.uhf_antenna))
antenna.append(rs)
st = RadioSettingValueString(0, 7, ''.join([self._CALLSIGN_CHARSET[x]
for x in self._memobj.
callsign]))
st.set_charset(self._CALLSIGN_CHARSET)
rs = RadioSetting("callsign", "Callsign", st)
cw.append(rs)
rs = RadioSetting("spl", "Split",
RadioSettingValueBoolean(_settings.spl))
panelcontr.append(rs)
options = ["None", "Up", "Down", "PMS"]
rs = RadioSetting(
"scn_mode", "Scan mode",
RadioSettingValueList(
options, current_index=_settings.scn_mode))
panelcontr.append(rs)
rs = RadioSetting("pri", "Priority",
RadioSettingValueBoolean(_settings.pri))
panelcontr.append(rs)
rs = RadioSetting("dw", "Dual watch",
RadioSettingValueBoolean(_settings.dw))
panelcontr.append(rs)
rs = RadioSetting("art", "Auto-range transponder",
RadioSettingValueBoolean(_settings.art))
panelcontr.append(rs)
rs = RadioSetting("nb", "Noise blanker",
RadioSettingValueBoolean(_settings.nb))
panelcontr.append(rs)
options = ["Auto", "Fast", "Slow", "Off"]
rs = RadioSetting("agc", "AGC", RadioSettingValueList(
options, current_index=_settings.agc))
panelcontr.append(rs)
options = ["PWR", "ALC", "SWR", "MOD"]
rs = RadioSetting(
"pwr_meter_mode", "Power meter mode",
RadioSettingValueList(
options, current_index=_settings.pwr_meter_mode))
panelcontr.append(rs)
rs = RadioSetting("vox", "Vox",
RadioSettingValueBoolean(_settings.vox))
panelcontr.append(rs)
rs = RadioSetting("bk", "Semi break-in",
RadioSettingValueBoolean(_settings.bk))
cw.append(rs)
rs = RadioSetting("kyr", "Keyer",
RadioSettingValueBoolean(_settings.kyr))
cw.append(rs)
options = ["enabled", "disabled"]
rs = RadioSetting("fst", "Fast", RadioSettingValueList(
options, current_index=_settings.fst))
panelcontr.append(rs)
options = ["enabled", "disabled"]
rs = RadioSetting("lock", "Lock",
RadioSettingValueList(options,
current_index=_settings.lock))
panelcontr.append(rs)
return top
def set_settings(self, settings):
_settings = self._memobj.settings
for element in settings:
if not isinstance(element, RadioSetting):
self.set_settings(element)
continue
try:
if "." in element.get_name():
bits = element.get_name().split(".")
obj = self._memobj
for bit in bits[:-1]:
obj = getattr(obj, bit)
setting = bits[-1]
else:
obj = _settings
setting = element.get_name()
try:
LOG.debug("Setting %s(%s) <= %s" % (setting,
getattr(obj, setting),
element.value))
except AttributeError:
LOG.debug("Setting %s <= %s" % (setting, element.value))
if setting == "contrast":
setattr(obj, setting, int(element.value) + 1)
elif setting == "callsign":
self._memobj.callsign = \
[self._CALLSIGN_CHARSET_REV[x] for x in
str(element.value)]
else:
setattr(obj, setting, element.value)
except Exception:
LOG.debug(element.get_name())
raise
@directory.register
class FT817NDRadio(FT817Radio):
"""Yaesu FT-817ND"""
MODEL = "FT-817ND"
_model = ""
_memsize = 6521
# block 9 (130 Bytes long) is to be repeated 40 times
_block_lengths = [2, 40, 208, 182, 208, 182, 198, 53, 130, 118, 130]
@directory.register
class FT817NDUSRadio(FT817Radio):
"""Yaesu FT-817ND (US version)"""
# seems that radios configured for 5 MHz operations send one packet
# more than others so we have to distinguish sub models
MODEL = "FT-817ND (US)"
_model = ""
_US_model = True
_memsize = 6651
# block 9 (130 Bytes long) is to be repeated 40 times
_block_lengths = [2, 40, 208, 182, 208, 182, 198, 53, 130, 118, 130, 130]
SPECIAL_60M = {
"M-601": -42,
"M-602": -41,
"M-603": -40,
"M-604": -39,
"M-605": -38,
}
LAST_SPECIAL60M_INDEX = -42
SPECIAL_MEMORIES = dict(FT817Radio.SPECIAL_MEMORIES)
SPECIAL_MEMORIES.update(SPECIAL_60M)
SPECIAL_MEMORIES_REV = dict(list(zip(list(SPECIAL_MEMORIES.values()),
list(SPECIAL_MEMORIES.keys()))))
def _get_special_60m(self, number):
mem = chirp_common.Memory()
mem.number = self.SPECIAL_60M[number]
mem.extd_number = number
_mem = self._memobj.sixtymeterchannels[-self.LAST_SPECIAL60M_INDEX +
mem.number]
mem = self._get_memory(mem, _mem)
mem.immutable = ["number", "rtone", "ctone",
"extd_number", "name", "dtcs", "tmode", "cross_mode",
"dtcs_polarity", "power", "duplex", "offset",
"empty"]
return mem
def _set_special_60m(self, mem):
if mem.empty:
# can't delete 60M memories!
raise Exception("Sorry, 60M memory can't be deleted")
cur_mem = self._get_special_60m(self.SPECIAL_MEMORIES_REV[mem.number])
for key in cur_mem.immutable:
if cur_mem.__dict__[key] != mem.__dict__[key]:
raise errors.RadioError("Editing field `%s' " % key +
"is not supported on M-60x channels")
if mem.mode not in ["USB", "LSB", "CW", "CWR", "NCW", "NCWR", "DIG"]:
raise errors.RadioError("Mode {mode} is not valid "
"in 60m channels".format(mode=mem.mode))
_mem = self._memobj.sixtymeterchannels[-self.LAST_SPECIAL60M_INDEX +
mem.number]
self._set_memory(mem, _mem)
def get_memory(self, number):
if number in self.SPECIAL_60M.keys():
return self._get_special_60m(number)
elif (isinstance(number, int) and number < 0 and
self.SPECIAL_MEMORIES_REV[number] in
list(self.SPECIAL_60M.keys())):
# I can't stop delete operation from losing extd_number but
# I know how to get it back
return self._get_special_60m(self.SPECIAL_MEMORIES_REV[number])
else:
return FT817Radio.get_memory(self, number)
def set_memory(self, memory):
if memory.number in list(self.SPECIAL_60M.values()):
return self._set_special_60m(memory)
else:
return FT817Radio.set_memory(self, memory)
def get_settings(self):
top = FT817Radio.get_settings(self)
basic = top[0]
rs = RadioSetting("emergency", "Emergency",
RadioSettingValueBoolean(
self._memobj.settings.emergency))
basic.append(rs)
return top
chirp-20261030/chirp/drivers/ft818.py 0000755 0001750 0001750 00000022742 14665027664 016073 0 ustar hibby hibby #
# Copyright 2012 Filippi Marco
# Copyright 2018 Vinny Stipo
#
# 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 .
"""FT818 management module"""
from chirp.drivers import ft817
from chirp import chirp_common, errors, directory
from chirp.settings import RadioSetting, RadioSettingValueBoolean
import logging
LOG = logging.getLogger(__name__)
@directory.register
class FT818Radio(ft817.FT817Radio):
"""Yaesu FT-818"""
BAUD_RATE = 9600
MODEL = "FT-818"
_model = ""
_memsize = 6573
_block_lengths = [2, 40, 208, 208, 208, 208, 198, 53, 130, 118, 130]
MEM_FORMAT = """
struct mem_struct {
u8 tag_on_off:1,
tag_default:1,
unknown1:3,
mode:3;
u8 duplex:2,
is_duplex:1,
is_cwdig_narrow:1,
is_fm_narrow:1,
freq_range:3;
u8 skip:1,
unknown2:1,
ipo:1,
att:1,
unknown3:4;
u8 ssb_step:2,
am_step:3,
fm_step:3;
u8 unknown4:6,
tmode:2;
u8 unknown5:2,
tx_mode:3,
tx_freq_range:3;
u8 unknown6:1,
unknown_toneflag:1,
tone:6;
u8 unknown7:1,
dcs:7;
ul16 rit;
u32 freq;
u32 offset;
u8 name[8];
};
#seekto 0x4;
struct {
u8 fst:1,
lock:1,
nb:1,
pbt:1,
unknownb:1,
dsp:1,
agc:2;
u8 vox:1,
vlt:1,
bk:1,
kyr:1,
unknown5:1,
cw_paddle:1,
pwr_meter_mode:2;
u8 vfob_band_select:4,
vfoa_band_select:4;
u8 unknowna;
u8 backlight:2,
color:2,
contrast:4;
u8 beep_freq:1,
beep_volume:7;
u8 arts_beep:2,
main_step:1,
cw_id:1,
scope:1,
pkt_rate:1,
resume_scan:2;
u8 op_filter:2,
lock_mode:2,
cw_pitch:4;
u8 sql_rf_gain:1,
ars_144:1,
ars_430:1,
cw_weight:5;
u8 cw_delay;
u8 unknown8:1,
sidetone:7;
u8 batt_chg:2,
cw_speed:6;
u8 disable_amfm_dial:1,
vox_gain:7;
u8 cat_rate:2,
emergency:1,
vox_delay:5;
u8 dig_mode:3,
mem_group:1,
unknown9:1,
apo_time:3;
u8 dcs_inv:2,
unknown10:1,
tot_time:5;
u8 mic_scan:1,
ssb_mic:7;
u8 mic_key:1,
am_mic:7;
u8 unknown11:1,
fm_mic:7;
u8 unknown12:1,
dig_mic:7;
u8 extended_menu:1,
pkt_mic:7;
u8 unknown14:1,
pkt9600_mic:7;
il16 dig_shift;
il16 dig_disp;
i8 r_lsb_car;
i8 r_usb_car;
i8 t_lsb_car;
i8 t_usb_car;
u8 unknown15:2,
menu_item:6;
u8 unknown16:4,
menu_sel:4;
u16 unknown17;
u8 art:1,
scn_mode:2,
dw:1,
pri:1,
unknown18:1,
tx_power:2;
u8 spl:1,
unknown:1,
uhf_antenna:1,
vhf_antenna:1,
air_antenna:1,
bc_antenna:1,
sixm_antenna:1,
hf_antenna:1;
} settings;
#seekto 0x2A;
struct mem_struct vfoa[16];
struct mem_struct vfob[16];
struct mem_struct home[4];
struct mem_struct qmb;
struct mem_struct mtqmb;
struct mem_struct mtune;
#seekto 0x431;
u8 visible[25];
u8 pmsvisible;
#seekto 0x44B;
u8 filled[25];
u8 pmsfilled;
#seekto 0x465;
struct mem_struct memory[200];
struct mem_struct pms[2];
#seekto 0x1903;
u8 callsign[7];
#seekto 0x19AD;
struct mem_struct sixtymeterchannels[5];
"""
SPECIAL_MEMORIES = {
"VFOa-1.8M": -37,
"VFOa-3.5M": -36,
"VFOa-5M": -35,
"VFOa-7M": -34,
"VFOa-10M": -33,
"VFOa-14M": -32,
"VFOa-18M": -31,
"VFOa-21M": -30,
"VFOa-24M": -29,
"VFOa-28M": -28,
"VFOa-50M": -27,
"VFOa-FM": -26,
"VFOa-AIR": -25,
"VFOa-144": -24,
"VFOa-430": -23,
"VFOa-HF": -22,
"VFOb-1.8M": -21,
"VFOb-3.5M": -20,
"VFOb-5M": -19,
"VFOb-7M": -18,
"VFOb-10M": -17,
"VFOb-14M": -16,
"VFOb-18M": -15,
"VFOb-21M": -14,
"VFOb-24M": -13,
"VFOb-28M": -12,
"VFOb-50M": -11,
"VFOb-FM": -10,
"VFOb-AIR": -9,
"VFOb-144": -8,
"VFOb-430": -7,
"VFOb-HF": -6,
"HOME HF": -5,
"HOME 50M": -4,
"HOME 144": -3,
"HOME 430": -2,
"QMB": -1,
}
FIRST_VFOB_INDEX = -6
LAST_VFOB_INDEX = -21
FIRST_VFOA_INDEX = -22
LAST_VFOA_INDEX = -37
SPECIAL_PMS = {
"PMS-L": -39,
"PMS-U": -38,
}
LAST_PMS_INDEX = -39
SPECIAL_MEMORIES.update(SPECIAL_PMS)
SPECIAL_MEMORIES_REV = dict(zip(SPECIAL_MEMORIES.values(),
SPECIAL_MEMORIES.keys()))
@directory.register
class FT818NDUSRadio(FT818Radio):
"""Yaesu FT-818ND (US version)"""
MODEL = "FT-818ND (US)"
_model = ""
_US_model = True
_memsize = 6703
_block_lengths = [2, 40, 208, 208, 208, 208, 198, 53, 130, 118, 130, 130]
SPECIAL_60M = {
"M-601": -44,
"M-602": -43,
"M-603": -42,
"M-604": -41,
"M-605": -40,
}
LAST_SPECIAL60M_INDEX = -44
SPECIAL_MEMORIES = dict(FT818Radio.SPECIAL_MEMORIES)
SPECIAL_MEMORIES.update(SPECIAL_60M)
SPECIAL_MEMORIES_REV = dict(zip(SPECIAL_MEMORIES.values(),
SPECIAL_MEMORIES.keys()))
def _get_special_60m(self, number):
mem = chirp_common.Memory()
mem.number = self.SPECIAL_60M[number]
mem.extd_number = number
_mem = self._memobj.sixtymeterchannels[-self.LAST_SPECIAL60M_INDEX +
mem.number]
mem = self._get_memory(mem, _mem)
mem.immutable = ["number", "rtone", "ctone",
"extd_number", "name", "dtcs", "tmode", "cross_mode",
"dtcs_polarity", "power", "duplex", "offset",
"comment", "empty"]
return mem
def _set_special_60m(self, mem):
if mem.empty:
# can't delete 60M memories!
raise Exception("Sorry, 60M memory can't be deleted")
cur_mem = self._get_special_60m(self.SPECIAL_MEMORIES_REV[mem.number])
for key in cur_mem.immutable:
if cur_mem.__dict__[key] != mem.__dict__[key]:
raise errors.RadioError("Editing field `%s' " % key +
"is not supported on M-60x channels")
if mem.mode not in ["USB", "LSB", "CW", "CWR", "NCW", "NCWR", "DIG"]:
raise errors.RadioError("Mode {mode} is not valid "
"in 60m channels".format(mode=mem.mode))
_mem = self._memobj.sixtymeterchannels[-self.LAST_SPECIAL60M_INDEX +
mem.number]
self._set_memory(mem, _mem)
def get_memory(self, number):
if number in self.SPECIAL_60M.keys():
return self._get_special_60m(number)
elif number < 0 and \
self.SPECIAL_MEMORIES_REV[number] in self.SPECIAL_60M.keys():
# I can't stop delete operation from losing extd_number but
# I know how to get it back
return self._get_special_60m(self.SPECIAL_MEMORIES_REV[number])
else:
return FT818Radio.get_memory(self, number)
def set_memory(self, memory):
if memory.number in self.SPECIAL_60M.values():
return self._set_special_60m(memory)
else:
return FT818Radio.set_memory(self, memory)
def get_settings(self):
top = FT818Radio.get_settings(self)
basic = top[0]
rs = RadioSetting("emergency", "Emergency",
RadioSettingValueBoolean(
self._memobj.settings.emergency))
basic.append(rs)
return top
chirp-20261030/chirp/drivers/ft857.py 0000644 0001750 0001750 00000142215 14670320477 016063 0 ustar hibby hibby #
# Copyright 2012 Filippi Marco
#
# 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 .
"""FT857 - FT857/US management module"""
from chirp.drivers import ft817
from chirp import chirp_common, errors, directory
from chirp.settings import RadioSetting, RadioSettingGroup, \
RadioSettingValueInteger, RadioSettingValueList, \
RadioSettingValueBoolean, RadioSettingValueString, \
RadioSettings
import logging
from chirp.util import safe_charset_string
LOG = logging.getLogger(__name__)
@directory.register
class FT857Radio(ft817.FT817Radio):
"""Yaesu FT-857/897"""
MODEL = "FT-857/897"
_model = ""
TMODES: dict[int, str] = {
0x04: "Tone",
0x05: "TSQL",
# 0x08 : "DTCS Enc", not supported in UI yet
0x0a: "DTCS",
0xff: "Cross",
0x00: "",
}
TMODES_REV = dict(list(zip(list(TMODES.values()), list(TMODES.keys()))))
CROSS_MODES = {
0x01: "->Tone",
0x02: "->DTCS",
0x04: "Tone->",
0x05: "Tone->Tone",
0x06: "Tone->DTCS",
0x08: "DTCS->",
0x09: "DTCS->Tone",
0x0a: "DTCS->DTCS",
}
CROSS_MODES_REV = dict(list(zip(list(CROSS_MODES.values()),
list(CROSS_MODES.keys()))))
_memsize = 7341
# block 9 (140 Bytes long) is to be repeated 40 times
# should be 42 times but this way I can use original 817 functions
_block_lengths = [2, 82, 252, 196, 252, 196, 212, 55, 140, 140, 140,
38, 176]
# warning ranges has to be in this exact order
VALID_BANDS = [(100000, 33000000), (33000000, 56000000),
(76000000, 108000000), (108000000, 137000000),
(137000000, 164000000), (420000000, 470000000)]
CHARSET = list(chirp_common.CHARSET_ASCII)
for i in "\\{|}":
CHARSET.remove(i)
MEM_FORMAT = """
struct mem_struct{
u8 tag_on_off:1,
tag_default:1,
unknown1:3,
mode:3;
u8 duplex:2,
is_duplex:1,
is_cwdig_narrow:1,
is_fm_narrow:1,
freq_range:3;
u8 skip:1,
unknokwn1_1:1,
ipo:1,
att:1,
unknown2:4;
u8 ssb_step:2,
am_step:3,
fm_step:3;
u8 unknown3:3,
is_split_tone:1,
tmode:4;
u8 unknown4:2,
tx_mode:3,
tx_freq_range:3;
u8 unknown5:1,
unknown_toneflag:1,
tone:6;
u8 unknown6:1,
unknown_rxtoneflag:1,
rxtone:6;
u8 unknown7:1,
dcs:7;
u8 unknown8:1,
rxdcs:7;
ul16 rit;
u32 freq;
u32 offset;
u8 name[8];
};
#seekto 0x00;
struct {
u16 radioconfig;
u8 mem_vfo:2,
m_tune:1,
home:1,
pms_tune:1,
qmb:1,
mt_qmb:1,
vfo_ab:1;
u8 unknown;
u8 fst:1,
lock:1,
nb:1,
unknown1:2,
disp:1,
agc:2;
u8 vox:1,
unknown2:1,
bk:1,
kyr:1,
cw_speed_unit:1,
cw_key_rev:1,
pwr_meter_mode:2;
u8 vfo_b_freq_range:4,
vfo_a_freq_range:4;
u8 unknown3;
u8 disp_mode:2,
unknown4:2,
disp_contrast:4;
u8 unknown5:4,
clar_dial_sel:2,
beep_tone:2;
u8 arts_beep:2,
dial_step:1,
arts_id:1,
unknown6:1,
pkt_rate:1,
unknown7:2;
u8 unknown8:2,
lock_mode:2,
unknown9:1,
cw_pitch:3;
u8 sql_rf_gain:1,
ars_144:1,
ars_430:1,
cw_weight:5;
u8 cw_delay;
u8 cw_delay_hi:1,
cw_sidetone:7;
u8 unknown10:2,
cw_speed:6;
u8 disable_amfm_dial:1,
vox_gain:7;
u8 cat_rate:2,
emergency:1,
vox_delay:5;
u8 dig_mode:3,
mem_group:1,
unknown11:1,
apo_time:3;
u8 dcs_inv:2,
unknown12:1,
tot_time:5;
u8 mic_scan:1,
ssb_mic:7;
u8 cw_paddle:1,
am_mic:7;
u8 unknown13:1,
fm_mic:7;
u8 unknown14:1,
dig_mic:7;
u8 extended_menu:1,
pkt1200:7;
u8 unknown15:1,
pkt9600:7;
i16 dig_shift;
i16 dig_disp;
i8 r_lsb_car;
i8 r_usb_car;
i8 t_lsb_car;
i8 t_usb_car;
u8 unknown16:1,
menu_item:7;
u8 unknown17[5];
u8 unknown18:1,
mtr_peak_hold:1,
mic_sel:2,
cat_lin_tun:2,
unknown19:1,
split_tone:1;
u8 unknown20:1,
beep_vol:7;
u8 unknown21:1,
dig_vox:7;
u8 ext_menu:1,
home_vfo:1,
scan_mode:2,
scan_resume:4;
u8 cw_auto_mode:1,
cw_training:2,
cw_qsk:3,
cw_bfo:2;
u8 dsp_nr:4,
dsp_bpf:2,
dsp_mic_eq:2;
u8 unknown22:3,
dsp_lpf:5;
u8 mtr_atx_sel:3,
unknown23:1,
dsp_hpf:4;
u8 unknown24:2,
disp_intensity:2,
unknown25:1,
disp_color:3;
u8 unknown26:1,
disp_color_vfo:1,
disp_color_mtr:1,
disp_color_mode:1,
disp_color_memgrp:1,
unknown27:1,
disp_color_band:1,
disp_color_arts:1;
u8 unknown28:3,
disp_color_fix:5;
u8 unknown29:1,
nb_level:7;
u8 unknown30:1,
proc_level:7;
u8 unknown31:1,
rf_power_hf:7;
u8 unknown32:2,
tuner_atas:3,
mem_vfo_dial_mode:3;
u8 pg_a;
u8 pg_b;
u8 pg_c;
u8 pg_acc;
u8 pg_p1;
u8 pg_p2;
u8 unknown33:3,
xvtr_sel:2,
unknown33_1:2,
op_filter1:1;
u8 unknown34:6,
tx_if_filter:2;
u8 unknown35:3,
xvtr_a_negative:1,
xvtr_b_negative:1,
mtr_arx_sel:3;
u8 beacon_time;
u8 unknown36[2];
u8 dig_vox_enable:1,
unknown37:2,
scope_peakhold:1,
scope_width:2,
proc:1,
unknown38:1;
u8 unknown39:1,
rf_power_6m:7;
u8 unknown40:1,
rf_power_vhf:7;
u8 unknown41:1,
rf_power_uhf:7;
} settings;
#seekto 0x54;
struct mem_struct vfoa[16];
struct mem_struct vfob[16];
struct mem_struct home[4];
struct mem_struct qmb;
struct mem_struct mtqmb;
struct mem_struct mtune;
#seekto 0x4a9;
u8 visible[25];
ul16 pmsvisible;
#seekto 0x4c4;
u8 filled[25];
ul16 pmsfilled;
#seekto 0x4df;
struct mem_struct memory[200];
struct mem_struct pms[10];
#seekto 0x1bf3;
u8 arts_idw[10];
u8 beacon_text1[40];
u8 beacon_text2[40];
u8 beacon_text3[40];
u32 xvtr_a_offset;
u32 xvtr_b_offset;
u8 op_filter1_name[4];
u8 op_filter2_name[4];
#seekto 0x1CAD;
struct mem_struct sixtymeterchannels[5];
"""
_CALLSIGN_CHARSET = [chr(x) for x in list(range(ord("0"), ord("9") + 1)) +
list(range(ord("A"), ord("Z") + 1))] + [" ", "/"]
_CALLSIGN_CHARSET_REV = dict(list(zip(_CALLSIGN_CHARSET,
list(range(0, len(_CALLSIGN_CHARSET))))))
_BEACON_CHARSET = _CALLSIGN_CHARSET + ["+", "."]
_BEACON_CHARSET_REV = dict(list(zip(_BEACON_CHARSET,
list(range(0, len(_BEACON_CHARSET))))))
# WARNING Index are hard wired in memory management code !!!
SPECIAL_MEMORIES = {
"VFOa-1.8M": -37,
"VFOa-3.5M": -36,
"VFOa-5M": -35,
"VFOa-7M": -34,
"VFOa-10M": -33,
"VFOa-14M": -32,
"VFOa-18M": -31,
"VFOa-21M": -30,
"VFOa-24M": -29,
"VFOa-28M": -28,
"VFOa-50M": -27,
"VFOa-FM": -26,
"VFOa-AIR": -25,
"VFOa-144": -24,
"VFOa-430": -23,
"VFOa-HF": -22,
"VFOb-1.8M": -21,
"VFOb-3.5M": -20,
"VFOb-5M": -19,
"VFOb-7M": -18,
"VFOb-10M": -17,
"VFOb-14M": -16,
"VFOb-18M": -15,
"VFOb-21M": -14,
"VFOb-24M": -13,
"VFOb-28M": -12,
"VFOb-50M": -11,
"VFOb-FM": -10,
"VFOb-AIR": -9,
"VFOb-144M": -8,
"VFOb-430M": -7,
"VFOb-HF": -6,
"HOME HF": -5,
"HOME 50M": -4,
"HOME 144M": -3,
"HOME 430M": -2,
"QMB": -1,
}
FIRST_VFOB_INDEX = -6
LAST_VFOB_INDEX = -21
FIRST_VFOA_INDEX = -22
LAST_VFOA_INDEX = -37
SPECIAL_PMS = {
"PMS-1L": -47,
"PMS-1U": -46,
"PMS-2L": -45,
"PMS-2U": -44,
"PMS-3L": -43,
"PMS-3U": -42,
"PMS-4L": -41,
"PMS-4U": -40,
"PMS-5L": -39,
"PMS-5U": -38,
}
LAST_PMS_INDEX = -47
SPECIAL_MEMORIES.update(SPECIAL_PMS)
SPECIAL_MEMORIES_REV = dict(list(zip(list(SPECIAL_MEMORIES.values()),
list(SPECIAL_MEMORIES.keys()))))
FILTERS = ["CFIL", "FIL1", "FIL2"]
PROGRAMMABLEOPTIONS = [
"MFa:A/B", "MFa:A=B", "MFa:SPL",
"MFb:MW", "MFb:SKIP/MCLR", "MFb:TAG",
"MFc:STO", "MFc:RCL", "MFc:PROC",
"MFd:RPT", "MFd:REV", "MFd:VOX",
"MFe:TON/ENC", "MFe:TON/DEC", "MFe:TDCH",
"MFf:ARTS", "MFf:SRCH", "MFf:PMS",
"MFg:SCN", "MFg:PRI", "MFg:DW",
"MFh:SCOP", "MFh:WID", "MFh:STEP",
"MFi:MTR", "MFi:SWR", "MFi:DISP",
"MFj:SPOT", "MFj:BK", "MFj:KYR",
"MFk:TUNE", "MFk:DOWN", "MFk:UP",
"MFl:NB", "MFl:AGC", "MFl:AGC SEL",
"MFm:IPO", "MFm:ATT", "MFm:NAR",
"MFn:CFIL", "MFn:FIL1", "MFn:FIL2",
"MFo:PLY1", "MFo:PLY2", "MFo:PLY3",
"MFp:DNR", "MFp:DNF", "MFp:DBF",
"01:EXT MENU", "02:144MHz ARS", "03:430MHz ARS",
"04:AM&FM DIAL", "05:AM MIC GAIN", "06:AM STEP",
"07:APO TIME", "08:ARTS BEEP", "09:ARTS ID",
"10:ARTS IDW", "11:BEACON TEXT", "12:BEACON TIME",
"13:BEEP TONE", "14:BEEP VOL", "15:CAR LSB R",
"16:CAR LSB T", "17:CAR USB R", "18:CAR USB T",
"19:CAT RATE", "20:CAT/LIN/TUN", "21:CLAR DIAL SEL",
"22:CW AUTO MODE", "23:CW BFO", "24:CW DELAY",
"25:CW KEY REV", "26:CW PADDLE", "27:CW PITCH",
"28:CW QSK", "29:CW SIDE TONE", "30:CW SPEED",
"31:CW TRAINING", "32:CW WEIGHT", "33:DCS CODE",
"34:DCS INV", "35:DIAL STEP", "36:DIG DISP",
"37:DIG GAIN", "38:DIG MODE", "39:DIG SHIFT",
"40:DIG VOX", "41:DISP COLOR", "42:DISP CONTRAST",
"43:DISP INTENSITY", "44:DISP MODE", "45:DSP BPF WIDTH",
"46:DSP HPF CUTOFF", "47:DSP LPF CUTOFF", "48:DSP MIC EQ",
"49:DSP NR LEVEL", "50:EMERGENCY", "51:FM MIC GAIN",
"52:FM STEP", "53:HOME->VFO", "54:LOCK MODE",
"55:MEM GROUP", "56:MEM TAG", "57:MEM/VFO DIAL MODE",
"58:MIC SCAN", "59:MIC SEL", "60:MTR ARX",
"61:MTR ATX", "62:MTR PEAK HOLD", "63:NB LEVEL",
"64:OP FILTER", "71:PKT 1200", "72:PKT 9600",
"73:PKT RATE", "74:PROC LEVEL", "75:RF POWER SET",
"76:RPT SHIFT", "77:SCAN MODE", "78:SCAN RESUME",
"79:SPLIT TONE", "80:SQL/RF GAIN", "81:SSB MIC GAIN",
"82:SSB STEP", "83:TONE FREQ", "84:TX TIME",
"85:TUNER/ATAS", "86:TX IF FILTER", "87:VOX DELAY",
"88:VOX GAIN", "89:XVTR A FREQ", "90:XVTR B FREQ",
"91:XVTR SEL",
"MONI", "Q.SPL", "TCALL", "ATC", "USER"]
@classmethod
def get_prompts(cls):
rp = chirp_common.RadioPrompts()
rp.pre_download = _(
"1. Turn radio off.\n"
"2. Connect cable to CAT/LINEAR jack.\n"
"3. Press and hold in the [MODE <] and [MODE >] keys"
" while\n"
" turning the radio on (\"CLONE MODE\" will appear on the\n"
" display).\n"
"4. After clicking OK ,\n"
" press the [C](SEND) key to send image.\n")
rp.pre_upload = _(
"1. Turn radio off.\n"
"2. Connect cable to ACC jack.\n"
"3. Press and hold in the [MODE <] and [MODE >] keys"
" while\n"
" turning the radio on (\"CLONE MODE\" will appear on the\n"
" display).\n"
"4. Press the [A](RCV) key (\"receiving\" will appear on the"
" LCD).\n")
return rp
def get_features(self):
rf = ft817.FT817Radio.get_features(self)
rf.has_cross = True
rf.has_ctone = True
rf.has_rx_dtcs = True
rf.valid_tmodes = list(self.TMODES_REV.keys())
rf.valid_cross_modes = list(self.CROSS_MODES_REV.keys())
return rf
def _get_duplex(self, mem, _mem):
# radio set is_duplex only for + and - but not for split
# at the same time it does not complain if we set it same way 817 does
# (so no set_duplex here)
mem.duplex = self.DUPLEX[_mem.duplex]
def _get_tmode(self, mem, _mem):
if not _mem.is_split_tone:
mem.tmode = self.TMODES[int(_mem.tmode)]
else:
mem.tmode = "Cross"
mem.cross_mode = self.CROSS_MODES[int(_mem.tmode)]
if mem.tmode == "Tone":
mem.rtone = mem.ctone = chirp_common.TONES[_mem.tone]
elif mem.tmode == "TSQL":
mem.rtone = mem.ctone = chirp_common.TONES[_mem.tone]
elif mem.tmode == "DTCS Enc": # UI does not support it yet but
# this code has already been tested
mem.dtcs = mem.rx_dtcs = chirp_common.DTCS_CODES[_mem.dcs]
elif mem.tmode == "DTCS":
mem.dtcs = mem.rx_dtcs = chirp_common.DTCS_CODES[_mem.dcs]
elif mem.tmode == "Cross":
mem.ctone = chirp_common.TONES[_mem.rxtone]
# don't want to fail for this
try:
mem.rtone = chirp_common.TONES[_mem.tone]
except IndexError:
mem.rtone = chirp_common.TONES[0]
mem.dtcs = chirp_common.DTCS_CODES[_mem.dcs]
mem.rx_dtcs = chirp_common.DTCS_CODES[_mem.rxdcs]
def _set_tmode(self, mem, _mem):
if mem.tmode != "Cross":
_mem.is_split_tone = 0
_mem.tmode = self.TMODES_REV[mem.tmode]
else:
_mem.tmode = self.CROSS_MODES_REV[mem.cross_mode]
_mem.is_split_tone = 1
if mem.tmode == "Tone":
_mem.tone = _mem.rxtone = chirp_common.TONES.index(mem.rtone)
elif mem.tmode == "TSQL":
_mem.tone = _mem.rxtone = chirp_common.TONES.index(mem.ctone)
elif mem.tmode == "DTCS Enc": # UI does not support it yet but
# this code has already been tested
_mem.dcs = _mem.rxdcs = chirp_common.DTCS_CODES.index(mem.dtcs)
elif mem.tmode == "DTCS":
_mem.dcs = _mem.rxdcs = chirp_common.DTCS_CODES.index(mem.dtcs)
elif mem.tmode == "Cross":
_mem.tone = chirp_common.TONES.index(mem.rtone)
_mem.rxtone = chirp_common.TONES.index(mem.ctone)
_mem.dcs = chirp_common.DTCS_CODES.index(mem.dtcs)
_mem.rxdcs = chirp_common.DTCS_CODES.index(mem.rx_dtcs)
# have to put this bit to 0 otherwise we get strange display in tone
# frequency (menu 83). See bug #88 and #163
_mem.unknown_toneflag = 0
# dunno if there's the same problem here but to be safe ...
_mem.unknown_rxtoneflag = 0
def get_settings(self):
_settings = self._memobj.settings
basic = RadioSettingGroup("basic", "Basic")
cw = RadioSettingGroup("cw", "CW")
packet = RadioSettingGroup("packet", "Digital & packet")
panel = RadioSettingGroup("panel", "Panel settings")
extended = RadioSettingGroup("extended", "Extended")
panelcontr = RadioSettingGroup("panelcontr", "Panel controls")
top = RadioSettings(basic, cw, packet,
panelcontr, panel, extended)
rs = RadioSetting("extended_menu", "Extended menu",
RadioSettingValueBoolean(_settings.extended_menu))
extended.append(rs)
rs = RadioSetting("ars_144", "144 MHz ARS",
RadioSettingValueBoolean(_settings.ars_144))
basic.append(rs)
rs = RadioSetting("ars_430", "430 MHz ARS",
RadioSettingValueBoolean(_settings.ars_430))
basic.append(rs)
options = ["enable", "disable"]
rs = RadioSetting(
"disable_amfm_dial", "AM&FM Dial",
RadioSettingValueList(
options, current_index=_settings.disable_amfm_dial))
panel.append(rs)
rs = RadioSetting("am_mic", "AM mic gain",
RadioSettingValueInteger(0, 100, _settings.am_mic))
basic.append(rs)
options = ["OFF", "1h", "2h", "3h", "4h", "5h", "6h"]
rs = RadioSetting(
"apo_time", "APO time",
RadioSettingValueList(
options, current_index=_settings.apo_time))
basic.append(rs)
options = ["OFF", "Range", "All"]
rs = RadioSetting(
"arts_beep", "ARTS beep",
RadioSettingValueList(
options, current_index=_settings.arts_beep))
basic.append(rs)
rs = RadioSetting("arts_id", "ARTS ID",
RadioSettingValueBoolean(_settings.arts_id))
extended.append(rs)
st = RadioSettingValueString(0, 10,
safe_charset_string(
self._memobj.arts_idw,
self._CALLSIGN_CHARSET)
)
st.set_charset(self._CALLSIGN_CHARSET)
rs = RadioSetting("arts_idw", "ARTS IDW", st)
extended.append(rs)
st = RadioSettingValueString(0, 40,
safe_charset_string(
self._memobj.beacon_text1,
self._BEACON_CHARSET)
)
st.set_charset(self._BEACON_CHARSET)
rs = RadioSetting("beacon_text1", "Beacon text1", st)
extended.append(rs)
st = RadioSettingValueString(0, 40,
safe_charset_string(
self._memobj.beacon_text2,
self._BEACON_CHARSET)
)
st.set_charset(self._BEACON_CHARSET)
rs = RadioSetting("beacon_text2", "Beacon text2", st)
extended.append(rs)
st = RadioSettingValueString(0, 40,
safe_charset_string(
self._memobj.beacon_text3,
self._BEACON_CHARSET)
)
st.set_charset(self._BEACON_CHARSET)
rs = RadioSetting("beacon_text3", "Beacon text3", st)
extended.append(rs)
options = ["OFF"] + ["%i sec" % i for i in range(1, 256)]
rs = RadioSetting(
"beacon_time", "Beacon time",
RadioSettingValueList(
options, current_index=_settings.beacon_time))
extended.append(rs)
options = ["440 Hz", "880 Hz", "1760 Hz"]
rs = RadioSetting(
"beep_tone", "Beep tone",
RadioSettingValueList(
options, current_index=_settings.beep_tone))
panel.append(rs)
rs = RadioSetting("beep_vol", "Beep volume",
RadioSettingValueInteger(0, 100, _settings.beep_vol))
panel.append(rs)
rs = RadioSetting("r_lsb_car", "LSB Rx carrier point (*10 Hz)",
RadioSettingValueInteger(-30, 30,
_settings.r_lsb_car))
extended.append(rs)
rs = RadioSetting("r_usb_car", "USB Rx carrier point (*10 Hz)",
RadioSettingValueInteger(-30, 30,
_settings.r_usb_car))
extended.append(rs)
rs = RadioSetting("t_lsb_car", "LSB Tx carrier point (*10 Hz)",
RadioSettingValueInteger(-30, 30,
_settings.t_lsb_car))
extended.append(rs)
rs = RadioSetting("t_usb_car", "USB Tx carrier point (*10 Hz)",
RadioSettingValueInteger(-30, 30,
_settings.t_usb_car))
extended.append(rs)
options = ["4800", "9600", "38400"]
rs = RadioSetting(
"cat_rate", "CAT rate",
RadioSettingValueList(
options, current_index=_settings.cat_rate))
basic.append(rs)
options = ["CAT", "Linear", "Tuner"]
rs = RadioSetting(
"cat_lin_tun", "CAT/LIN/TUN selection",
RadioSettingValueList(
options, current_index=_settings.cat_lin_tun))
extended.append(rs)
options = ["MAIN", "VFO/MEM", "CLAR"]
# TODO test the 3 options on non D radio
# which have only SEL and MAIN
rs = RadioSetting(
"clar_dial_sel", "Clarifier dial selection",
RadioSettingValueList(
options, current_index=_settings.clar_dial_sel))
panel.append(rs)
rs = RadioSetting("cw_auto_mode", "CW Automatic mode",
RadioSettingValueBoolean(_settings.cw_auto_mode))
cw.append(rs)
options = ["USB", "LSB", "AUTO"]
rs = RadioSetting("cw_bfo", "CW BFO", RadioSettingValueList(
options, current_index=_settings.cw_bfo))
cw.append(rs)
options = ["FULL"] + ["%i ms" % (i * 10) for i in range(3, 301)]
val = (_settings.cw_delay + _settings.cw_delay_hi * 256) - 2
rs = RadioSetting("cw_delay", "CW delay",
RadioSettingValueList(options, current_index=val))
cw.append(rs)
options = ["Normal", "Reverse"]
rs = RadioSetting(
"cw_key_rev", "CW key reverse",
RadioSettingValueList(
options, current_index=_settings.cw_key_rev))
cw.append(rs)
rs = RadioSetting("cw_paddle", "CW paddle",
RadioSettingValueBoolean(_settings.cw_paddle))
cw.append(rs)
options = ["%i Hz" % i for i in range(400, 801, 100)]
rs = RadioSetting(
"cw_pitch", "CW pitch",
RadioSettingValueList(
options, current_index=_settings.cw_pitch))
cw.append(rs)
options = ["%i ms" % i for i in range(5, 31, 5)]
rs = RadioSetting("cw_qsk", "CW QSK", RadioSettingValueList(
options, current_index=_settings.cw_qsk))
cw.append(rs)
rs = RadioSetting("cw_sidetone", "CW sidetone volume",
RadioSettingValueInteger(0, 100,
_settings.cw_sidetone))
cw.append(rs)
options = ["%i wpm" % i for i in range(4, 61)]
rs = RadioSetting(
"cw_speed", "CW speed",
RadioSettingValueList(
options, current_index=_settings.cw_speed))
cw.append(rs)
options = ["Numeric", "Alphabet", "AlphaNumeric"]
rs = RadioSetting(
"cw_training", "CW training",
RadioSettingValueList(
options, current_index=_settings.cw_training))
cw.append(rs)
options = ["1:%1.1f" % (i / 10) for i in range(25, 46, 1)]
rs = RadioSetting(
"cw_weight", "CW weight",
RadioSettingValueList(
options, current_index=_settings.cw_weight))
cw.append(rs)
options = ["Tn-Rn", "Tn-Riv", "Tiv-Rn", "Tiv-Riv"]
rs = RadioSetting("dcs_inv", "DCS inv", RadioSettingValueList(
options, current_index=_settings.dcs_inv))
extended.append(rs)
options = ["Fine", "Coarse"]
rs = RadioSetting(
"dial_step", "Dial step",
RadioSettingValueList(
options, current_index=_settings.dial_step))
panel.append(rs)
rs = RadioSetting("dig_disp", "Dig disp (*10 Hz)",
RadioSettingValueInteger(-300, 300,
_settings.dig_disp))
packet.append(rs)
rs = RadioSetting("dig_mic", "Dig gain",
RadioSettingValueInteger(0, 100,
_settings.dig_mic))
packet.append(rs)
options = ["RTTYL", "RTTYU", "PSK31-L", "PSK31-U", "USER-L", "USER-U"]
rs = RadioSetting(
"dig_mode", "Dig mode",
RadioSettingValueList(
options, current_index=_settings.dig_mode))
packet.append(rs)
rs = RadioSetting("dig_shift", "Dig shift (*10 Hz)",
RadioSettingValueInteger(-300, 300,
_settings.dig_shift))
packet.append(rs)
rs = RadioSetting("dig_vox", "Dig vox",
RadioSettingValueInteger(0, 100, _settings.dig_vox))
packet.append(rs)
options = ["ARTS", "BAND", "FIX", "MEMGRP", "MODE", "MTR", "VFO"]
rs = RadioSetting(
"disp_color", "Display color mode",
RadioSettingValueList(
options, current_index=_settings.disp_color))
panel.append(rs)
rs = RadioSetting("disp_color_arts", "Display color ARTS set",
RadioSettingValueInteger(0, 1,
_settings.disp_color_arts))
panel.append(rs)
rs = RadioSetting("disp_color_band", "Display color band set",
RadioSettingValueInteger(0, 1,
_settings.disp_color_band))
panel.append(rs)
rs = RadioSetting("disp_color_memgrp",
"Display color memory group set",
RadioSettingValueInteger(0, 1,
_settings.disp_color_memgrp)
)
panel.append(rs)
rs = RadioSetting("disp_color_mode", "Display color mode set",
RadioSettingValueInteger(0, 1,
_settings.disp_color_mode))
panel.append(rs)
rs = RadioSetting("disp_color_mtr", "Display color meter set",
RadioSettingValueInteger(0, 1,
_settings.disp_color_mtr))
panel.append(rs)
rs = RadioSetting("disp_color_vfo", "Display color VFO set",
RadioSettingValueInteger(0, 1,
_settings.disp_color_vfo))
panel.append(rs)
rs = RadioSetting("disp_color_fix", "Display color fix set",
RadioSettingValueInteger(1, 32,
_settings.disp_color_fix + 1
))
panel.append(rs)
rs = RadioSetting("disp_contrast", "Contrast",
RadioSettingValueInteger(3, 15,
_settings.disp_contrast + 2)
)
panel.append(rs)
rs = RadioSetting("disp_intensity", "Intensity",
RadioSettingValueInteger(1, 3,
_settings.disp_intensity))
panel.append(rs)
options = ["OFF", "Auto1", "Auto2", "ON"]
rs = RadioSetting(
"disp_mode", "Display backlight mode",
RadioSettingValueList(
options, current_index=_settings.disp_mode))
panel.append(rs)
options = ["60 Hz", "120 Hz", "240 Hz"]
rs = RadioSetting("dsp_bpf", "Dsp band pass filter",
RadioSettingValueList(
options, current_index=_settings.dsp_bpf))
cw.append(rs)
options = ["100 Hz", "160 Hz", "220 Hz", "280 Hz", "340 Hz", "400 Hz",
"460 Hz", "520 Hz", "580 Hz", "640 Hz", "720 Hz", "760 Hz",
"820 Hz", "880 Hz", "940 Hz", "1000 Hz"]
rs = RadioSetting(
"dsp_hpf", "Dsp hi pass filter cut off",
RadioSettingValueList(options, current_index=_settings.dsp_hpf))
basic.append(rs)
options = ["1000 Hz", "1160 Hz", "1320 Hz", "1480 Hz", "1650 Hz",
"1800 Hz", "1970 Hz", "2130 Hz", "2290 Hz", "2450 Hz",
"2610 Hz", "2770 Hz", "2940 Hz", "3100 Hz", "3260 Hz",
"3420 Hz", "3580 Hz", "3740 Hz", "3900 Hz", "4060 Hz",
"4230 Hz", "4390 Hz", "4550 Hz", "4710 Hz", "4870 Hz",
"5030 Hz", "5190 Hz", "5390 Hz", "5520 Hz", "5680 Hz",
"5840 Hz", "6000 Hz"]
rs = RadioSetting(
"dsp_lpf", "Dsp low pass filter cut off",
RadioSettingValueList(options, current_index=_settings.dsp_lpf))
basic.append(rs)
options = ["OFF", "LPF", "HPF", "BOTH"]
rs = RadioSetting(
"dsp_mic_eq", "Dsp mic equalization",
RadioSettingValueList(
options, current_index=_settings.dsp_mic_eq))
basic.append(rs)
rs = RadioSetting("dsp_nr", "DSP noise reduction level",
RadioSettingValueInteger(1, 16,
_settings.dsp_nr + 1))
basic.append(rs)
# emergency only for US model
rs = RadioSetting("fm_mic", "FM mic gain",
RadioSettingValueInteger(0, 100, _settings.fm_mic))
basic.append(rs)
rs = RadioSetting("home_vfo", "Enable HOME to VFO moving",
RadioSettingValueBoolean(_settings.home_vfo))
panel.append(rs)
options = ["Dial", "Freq", "Panel", "All"]
rs = RadioSetting(
"lock_mode", "Lock mode",
RadioSettingValueList(
options, current_index=_settings.lock_mode))
panel.append(rs)
rs = RadioSetting("mem_group", "Mem group",
RadioSettingValueBoolean(_settings.mem_group))
basic.append(rs)
options = ["CW SIDETONE", "CW SPEED", "MHz/MEM GRP", "MIC GAIN",
"NB LEVEL", "RF POWER", "STEP"]
rs = RadioSetting(
"mem_vfo_dial_mode", "Mem/VFO dial mode",
RadioSettingValueList(
options, current_index=_settings.mem_vfo_dial_mode))
panel.append(rs)
rs = RadioSetting("mic_scan", "Mic scan",
RadioSettingValueBoolean(_settings.mic_scan))
basic.append(rs)
options = ["NOR", "RMT", "CAT"]
rs = RadioSetting("mic_sel", "Mic selection",
RadioSettingValueList(
options, current_index=_settings.mic_sel))
extended.append(rs)
options = ["SIG", "CTR", "VLT", "N/A", "FS", "OFF"]
rs = RadioSetting(
"mtr_arx_sel", "Meter receive selection",
RadioSettingValueList(
options, current_index=_settings.mtr_arx_sel))
extended.append(rs)
options = ["PWR", "ALC", "MOD", "SWR", "VLT", "N/A", "OFF"]
rs = RadioSetting(
"mtr_atx_sel", "Meter transmit selection",
RadioSettingValueList(
options, current_index=_settings.mtr_atx_sel))
extended.append(rs)
rs = RadioSetting("mtr_peak_hold", "Meter peak hold",
RadioSettingValueBoolean(_settings.mtr_peak_hold))
extended.append(rs)
rs = RadioSetting("nb_level", "Noise blanking level",
RadioSettingValueInteger(0, 100, _settings.nb_level))
basic.append(rs)
st = RadioSettingValueString(0, 4,
safe_charset_string(
self._memobj.op_filter1_name,
self._CALLSIGN_CHARSET)
)
st.set_charset(self._CALLSIGN_CHARSET)
rs = RadioSetting("op_filter1_name", "Optional filter1 name", st)
extended.append(rs)
st = RadioSettingValueString(0, 4,
safe_charset_string(
self._memobj.op_filter2_name,
self._CALLSIGN_CHARSET)
)
st.set_charset(self._CALLSIGN_CHARSET)
rs = RadioSetting("op_filter2_name", "Optional filter2 name", st)
extended.append(rs)
rs = RadioSetting("pg_a", "Programmable key MFq:A",
RadioSettingValueList(self.PROGRAMMABLEOPTIONS,
current_index=_settings.pg_a))
extended.append(rs)
rs = RadioSetting("pg_b", "Programmable key MFq:B",
RadioSettingValueList(self.PROGRAMMABLEOPTIONS,
current_index=_settings.pg_b))
extended.append(rs)
rs = RadioSetting("pg_c", "Programmable key MFq:C",
RadioSettingValueList(self.PROGRAMMABLEOPTIONS,
current_index=_settings.pg_c))
extended.append(rs)
rs = RadioSetting("pg_acc", "Programmable mic key ACC",
RadioSettingValueList(
self.PROGRAMMABLEOPTIONS,
current_index=_settings.pg_acc))
extended.append(rs)
rs = RadioSetting("pg_p1", "Programmable mic key P1",
RadioSettingValueList(self.PROGRAMMABLEOPTIONS,
current_index=_settings.pg_p1))
extended.append(rs)
rs = RadioSetting("pg_p2", "Programmable mic key P2",
RadioSettingValueList(self.PROGRAMMABLEOPTIONS,
current_index=_settings.pg_p2))
extended.append(rs)
rs = RadioSetting("pkt1200", "Packet 1200 gain level",
RadioSettingValueInteger(0, 100,
_settings.pkt1200))
packet.append(rs)
rs = RadioSetting("pkt9600", "Packet 9600 gain level",
RadioSettingValueInteger(0, 100, _settings.pkt9600))
packet.append(rs)
options = ["1200", "9600"]
rs = RadioSetting(
"pkt_rate", "Packet rate",
RadioSettingValueList(
options, current_index=_settings.pkt_rate))
packet.append(rs)
rs = RadioSetting("proc_level", "Proc level",
RadioSettingValueInteger(0, 100,
_settings.proc_level))
basic.append(rs)
rs = RadioSetting("rf_power_hf", "Rf power set HF",
RadioSettingValueInteger(5, 100,
_settings.rf_power_hf))
basic.append(rs)
rs = RadioSetting("rf_power_6m", "Rf power set 6m",
RadioSettingValueInteger(5, 100,
_settings.rf_power_6m))
basic.append(rs)
rs = RadioSetting("rf_power_vhf", "Rf power set VHF",
RadioSettingValueInteger(5, 50,
_settings.rf_power_vhf))
basic.append(rs)
rs = RadioSetting("rf_power_uhf", "Rf power set UHF",
RadioSettingValueInteger(2, 20,
_settings.rf_power_uhf))
basic.append(rs)
options = ["TIME", "BUSY", "STOP"]
rs = RadioSetting(
"scan_mode", "Scan mode",
RadioSettingValueList(
options, current_index=_settings.scan_mode))
basic.append(rs)
rs = RadioSetting("scan_resume", "Scan resume",
RadioSettingValueInteger(1, 10,
_settings.scan_resume))
basic.append(rs)
rs = RadioSetting("split_tone", "Split tone enable",
RadioSettingValueBoolean(_settings.split_tone))
extended.append(rs)
options = ["RF-Gain", "Squelch"]
rs = RadioSetting(
"sql_rf_gain", "Squelch/RF-Gain",
RadioSettingValueList(
options, current_index=_settings.sql_rf_gain))
panel.append(rs)
rs = RadioSetting("ssb_mic", "SSB Mic gain",
RadioSettingValueInteger(0, 100, _settings.ssb_mic))
basic.append(rs)
options = ["Off"] + ["%i" % i for i in range(1, 21)]
rs = RadioSetting(
"tot_time", "Time-out timer",
RadioSettingValueList(
options, current_index=_settings.tot_time))
basic.append(rs)
options = ["OFF", "ATAS(HF)", "ATAS(HF&50)", "ATAS(ALL)", "TUNER"]
rs = RadioSetting(
"tuner_atas", "Tuner/ATAS device",
RadioSettingValueList(
options, current_index=_settings.tuner_atas))
extended.append(rs)
rs = RadioSetting("tx_if_filter", "Transmit IF filter",
RadioSettingValueList(
self.FILTERS,
current_index=_settings.tx_if_filter))
basic.append(rs)
rs = RadioSetting("vox_delay", "VOX delay (*100 ms)",
RadioSettingValueInteger(1, 30, _settings.vox_delay))
basic.append(rs)
rs = RadioSetting("vox_gain", "VOX Gain",
RadioSettingValueInteger(1, 100, _settings.vox_gain))
basic.append(rs)
rs = RadioSetting("xvtr_a", "Xvtr A displacement",
RadioSettingValueInteger(
-4294967295, 4294967295,
self._memobj.xvtr_a_offset *
(-1 if _settings.xvtr_a_negative else 1)))
extended.append(rs)
rs = RadioSetting("xvtr_b", "Xvtr B displacement",
RadioSettingValueInteger(
-4294967295, 4294967295,
self._memobj.xvtr_b_offset *
(-1 if _settings.xvtr_b_negative else 1)))
extended.append(rs)
options = ["OFF", "XVTR A", "XVTR B"]
rs = RadioSetting(
"xvtr_sel", "Transverter function selection",
RadioSettingValueList(
options, current_index=_settings.xvtr_sel))
extended.append(rs)
rs = RadioSetting("disp", "Display large",
RadioSettingValueBoolean(_settings.disp))
panel.append(rs)
rs = RadioSetting("nb", "Noise blanker",
RadioSettingValueBoolean(_settings.nb))
panelcontr.append(rs)
options = ["Auto", "Fast", "Slow", "Off"]
rs = RadioSetting("agc", "AGC",
RadioSettingValueList(options,
current_index=_settings.agc))
panelcontr.append(rs)
options = ["PWR", "ALC", "SWR", "MOD"]
rs = RadioSetting(
"pwr_meter_mode", "Power meter mode",
RadioSettingValueList(
options, current_index=_settings.pwr_meter_mode))
panelcontr.append(rs)
rs = RadioSetting("vox", "Vox",
RadioSettingValueBoolean(_settings.vox))
panelcontr.append(rs)
rs = RadioSetting("bk", "Semi break-in",
RadioSettingValueBoolean(_settings.bk))
cw.append(rs)
rs = RadioSetting("kyr", "Keyer",
RadioSettingValueBoolean(_settings.kyr))
cw.append(rs)
options = ["enabled", "disabled"]
rs = RadioSetting("fst", "Fast", RadioSettingValueList(
options, current_index=_settings.fst))
panelcontr.append(rs)
options = ["enabled", "disabled"]
rs = RadioSetting("lock", "Lock",
RadioSettingValueList(options,
current_index=_settings.lock))
panelcontr.append(rs)
rs = RadioSetting("scope_peakhold", "Scope max hold",
RadioSettingValueBoolean(_settings.scope_peakhold))
panelcontr.append(rs)
options = ["21", "31", "127"]
rs = RadioSetting(
"scope_width", "Scope width (channels)",
RadioSettingValueList(
options, current_index=_settings.scope_width))
panelcontr.append(rs)
rs = RadioSetting("proc", "Speech processor",
RadioSettingValueBoolean(_settings.proc))
panelcontr.append(rs)
return top
def set_settings(self, settings):
_settings = self._memobj.settings
for element in settings:
if not isinstance(element, RadioSetting):
self.set_settings(element)
continue
try:
if "." in element.get_name():
bits = element.get_name().split(".")
obj = self._memobj
for bit in bits[:-1]:
obj = getattr(obj, bit)
setting = bits[-1]
else:
obj = _settings
setting = element.get_name()
try:
LOG.debug("Setting %s(%s) <= %s" % (setting,
getattr(obj, setting), element.value))
except AttributeError:
LOG.debug("Setting %s <= %s" % (setting, element.value))
if setting == "arts_idw":
self._memobj.arts_idw = \
[self._CALLSIGN_CHARSET_REV[x]
for x in str(element.value)]
elif setting in ["beacon_text1", "beacon_text2",
"beacon_text3", "op_filter1_name",
"op_filter2_name"]:
setattr(self._memobj, setting,
[self._BEACON_CHARSET_REV[x]
for x in str(element.value)])
elif setting == "cw_delay":
val = int(element.value) + 2
setattr(obj, "cw_delay_hi", val / 256)
setattr(obj, setting, val & 0xff)
elif setting == "dig_vox":
val = int(element.value)
setattr(obj, "dig_vox_enable", int(val > 0))
setattr(obj, setting, val)
elif setting in ["disp_color_fix", "dsp_nr"]:
setattr(obj, setting, int(element.value) - 1)
elif setting == "disp_contrast":
setattr(obj, setting, int(element.value) - 2)
elif setting in ["xvtr_a", "xvtr_b"]:
val = int(element.value)
setattr(obj, setting + "_negative", int(val < 0))
setattr(self._memobj, setting + "_offset", abs(val))
else:
setattr(obj, setting, element.value)
except Exception:
LOG.debug(element.get_name())
raise
@directory.register
class FT857USRadio(FT857Radio):
"""Yaesu FT857/897 (US version)"""
# seems that radios configured for 5 MHz operations send one packet more
# than others so we have to distinguish sub models
MODEL = "FT-857/897 (US)"
_model = ""
_US_model = True
_memsize = 7481
# block 9 (140 Bytes long) is to be repeated 40 times
# should be 42 times but this way I can use original 817 functions
_block_lengths = [2, 82, 252, 196, 252, 196, 212, 55, 140, 140, 140, 38,
176, 140]
SPECIAL_60M = {
"M-601": -52,
"M-602": -51,
"M-603": -50,
"M-604": -49,
"M-605": -48,
}
LAST_SPECIAL60M_INDEX = -52
SPECIAL_MEMORIES = dict(FT857Radio.SPECIAL_MEMORIES)
SPECIAL_MEMORIES.update(SPECIAL_60M)
SPECIAL_MEMORIES_REV = dict(list(zip(list(SPECIAL_MEMORIES.values()),
list(SPECIAL_MEMORIES.keys()))))
# this is identical to the one in FT817ND_US_Radio but we inherit from 857
def _get_special_60m(self, number):
mem = chirp_common.Memory()
mem.number = self.SPECIAL_60M[number]
mem.extd_number = number
_mem = self._memobj.sixtymeterchannels[-self.LAST_SPECIAL60M_INDEX +
mem.number]
mem = self._get_memory(mem, _mem)
mem.immutable = ["number", "rtone", "ctone",
"extd_number", "name", "dtcs", "tmode", "cross_mode",
"dtcs_polarity", "power", "duplex", "offset",
"empty"]
return mem
# this is identical to the one in FT817ND_US_Radio but we inherit from 857
def _set_special_60m(self, mem):
if mem.empty:
# can't delete 60M memories!
raise Exception("Sorry, 60M memory can't be deleted")
cur_mem = self._get_special_60m(self.SPECIAL_MEMORIES_REV[mem.number])
for key in cur_mem.immutable:
if cur_mem.__dict__[key] != mem.__dict__[key]:
raise errors.RadioError("Editing field `%s' " % key +
"is not supported on M-60x channels")
if mem.mode not in ["USB", "LSB", "CW", "CWR", "NCW", "NCWR", "DIG"]:
raise errors.RadioError("Mode {mode} is not valid "
"in 60m channels".format(mode=mem.mode))
_mem = self._memobj.sixtymeterchannels[-self.LAST_SPECIAL60M_INDEX +
mem.number]
self._set_memory(mem, _mem)
def get_memory(self, number):
if number in self.SPECIAL_60M:
return self._get_special_60m(number)
elif isinstance(number, int) and number < 0 and (
self.SPECIAL_MEMORIES_REV[number] in
list(self.SPECIAL_60M.keys())):
# I can't stop delete operation from losing extd_number but
# I know how to get it back
return self._get_special_60m(self.SPECIAL_MEMORIES_REV[number])
else:
return FT857Radio.get_memory(self, number)
def set_memory(self, memory):
if memory.number in list(self.SPECIAL_60M.values()):
return self._set_special_60m(memory)
else:
return FT857Radio.set_memory(self, memory)
def get_settings(self):
top = FT857Radio.get_settings(self)
basic = top[0]
rs = RadioSetting("emergency", "Emergency",
RadioSettingValueBoolean(
self._memobj.settings.emergency))
basic.append(rs)
return top
chirp-20261030/chirp/drivers/ft90.py 0000644 0001750 0001750 00000056547 15172631605 016000 0 ustar hibby hibby # Copyright 2011 Dan Smith
# Copyright 2013 Jens Jensen AF5MI
#
# 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.drivers import yaesu_clone
from chirp import chirp_common, bitwise, memmap, directory, errors, util
from chirp.settings import RadioSetting, RadioSettingGroup, \
RadioSettingValueList, RadioSettingValueBoolean, \
RadioSettingValueString, RadioSettings
import time
import traceback
import string
import re
import logging
LOG = logging.getLogger(__name__)
CMD_ACK = b'\x06'
FT90_STEPS = [5.0, 10.0, 12.5, 15.0, 20.0, 25.0, 50.0]
FT90_MODES = ["AM", "FM", "Auto"]
# idx 3 (Bell) not supported yet
FT90_TMODES = ["", "Tone", "TSQL", "", "DTCS"]
FT90_TONES = list(chirp_common.TONES)
for tone in [165.5, 171.3, 177.3]:
FT90_TONES.remove(tone)
FT90_POWER_LEVELS_VHF = [chirp_common.PowerLevel("Hi", watts=50),
chirp_common.PowerLevel("Mid1", watts=20),
chirp_common.PowerLevel("Mid2", watts=10),
chirp_common.PowerLevel("Low", watts=5)]
FT90_POWER_LEVELS_UHF = [chirp_common.PowerLevel("Hi", watts=35),
chirp_common.PowerLevel("Mid1", watts=20),
chirp_common.PowerLevel("Mid2", watts=10),
chirp_common.PowerLevel("Low", watts=5)]
FT90_DUPLEX = ["", "-", "+", "split"]
FT90_CWID_CHARS = list(string.digits) + list(string.ascii_uppercase) \
+ list(" ")
FT90_DTMF_CHARS = list("0123456789ABCD*#")
FT90_SPECIAL = ["vfo_vhf", "home_vhf", "vfo_uhf", "home_uhf",
"pms_1L", "pms_1U", "pms_2L", "pms_2U"]
@directory.register
class FT90Radio(yaesu_clone.YaesuCloneModeRadio):
VENDOR = "Yaesu"
MODEL = "FT-90"
ID = b"\x8E\xF6"
_memsize = 4063
# block 03 (200 Bytes long) repeats 18 times; channel memories
_block_lengths = [2, 232, 24] + ([200] * 18) + [205]
mem_format = """
u16 id;
#seekto 0x22;
struct {
u8 dtmf_active;
u8 dtmf1_len;
u8 dtmf2_len;
u8 dtmf3_len;
u8 dtmf4_len;
u8 dtmf5_len;
u8 dtmf6_len;
u8 dtmf7_len;
u8 dtmf8_len;
u8 dtmf1[8];
u8 dtmf2[8];
u8 dtmf3[8];
u8 dtmf4[8];
u8 dtmf5[8];
u8 dtmf6[8];
u8 dtmf7[8];
u8 dtmf8[8];
char cwid[7];
u8 unk1;
u8 scan1:2,
beep:1,
unk3:3,
rfsqlvl:2;
u8 unk4:2,
scan2:1,
cwid_en:1,
txnarrow:1,
dtmfspeed:1,
pttlock:2;
u8 dtmftxdelay:3,
fancontrol:2,
unk5:3;
u8 dimmer:3,
unk6:1,
lcdcontrast:4;
u8 dcsmode:2,
unk16:2,
tot:4;
u8 unk14;
u8 unk8:1,
ars:1,
lock:1,
txpwrsave:1,
apo:4;
u8 unk15;
u8 unk9:4,
key_lt:4;
u8 unk10:4,
key_rt:4;
u8 unk11:4,
key_p1:4;
u8 unk12:4,
key_p2:4;
u8 unk13:4,
key_acc:4;
} settings;
struct mem_struct {
u8 mode:2,
isUhf1:1,
unknown1:2,
step:3;
u8 artsmode:2,
unknown2:1,
isUhf2:1,
power:2,
shift:2;
u8 skip:1,
showname:1,
unknown3:1,
isUhfHi:1,
unknown4:1,
tmode:3;
u32 rxfreq;
u32 txfreqoffset;
u8 UseDefaultName:1,
ars:1,
tone:6;
u8 packetmode:1,
dcstone:7;
char name[7];
};
#seekto 0x86;
struct mem_struct vfo_vhf;
struct mem_struct home_vhf;
struct mem_struct vfo_uhf;
struct mem_struct home_uhf;
#seekto 0xEB;
u8 chan_enable[23];
#seekto 0x101;
struct {
u8 pms_2U_enable:1,
pms_2L_enable:1,
pms_1U_enable:1,
pms_1L_enable:1,
unknown6:4;
} special_enables;
#seekto 0x102;
struct mem_struct memory[180];
#seekto 0xf12;
struct mem_struct pms_1L;
struct mem_struct pms_1U;
struct mem_struct pms_2L;
struct mem_struct pms_2U;
#seekto 0x0F7B;
struct {
char demomsg1[50];
char demomsg2[50];
} demomsg;
"""
@classmethod
def get_prompts(cls):
rp = chirp_common.RadioPrompts()
rp.pre_download = _(
"1. Turn radio off.\n"
"2. Connect mic and hold [ACC] on mic while powering on.\n"
" (\"CLONE\" will appear on the display)\n"
"3. Replace mic with PC programming cable.\n"
"4. After clicking OK , press the [SET] key to send"
" image.\n")
rp.pre_upload = _(
"1. Turn radio off.\n"
"2. Connect mic and hold [ACC] on mic while powering on.\n"
" (\"CLONE\" will appear on the display)\n"
"3. Replace mic with PC programming cable.\n"
"4. Press the [DISP/SS] key\n"
" (\"R\" will appear on the lower left of LCD).\n")
rp.display_pre_upload_prompt_before_opening_port = False
return rp
@classmethod
def match_model(cls, filedata, filename):
return len(filedata) == cls._memsize
def get_features(self):
rf = chirp_common.RadioFeatures()
rf.has_settings = True
rf.has_ctone = False
rf.has_bank = False
rf.has_dtcs_polarity = False
rf.has_dtcs = True
rf.can_odd_split = True
rf.valid_modes = FT90_MODES
rf.valid_tmodes = FT90_TMODES
rf.valid_duplexes = FT90_DUPLEX
rf.valid_tuning_steps = FT90_STEPS[1:]
rf.valid_power_levels = FT90_POWER_LEVELS_VHF
rf.valid_name_length = 7
rf.valid_characters = chirp_common.CHARSET_ASCII
rf.valid_skips = ["", "S"]
rf.valid_special_chans = FT90_SPECIAL
rf.valid_tones = FT90_TONES
rf.memory_bounds = (1, 180)
rf.valid_bands = [(100000000, 230000000),
(300000000, 530000000),
(810000000, 999975000)]
return rf
def _read(self, blocksize, blocknum):
data = self.pipe.read(blocksize+2)
if not data:
if blocknum == 0:
raise errors.RadioNoResponse()
raise errors.RadioError(
"No response reading block %02X" % blocknum)
# chew echo'd ack
self.pipe.write(CMD_ACK)
time.sleep(0.02)
self.pipe.read(1) # chew echoed ACK from 1-wire serial
if len(data) == blocksize + 2 and data[0] == blocknum:
checksum = yaesu_clone.YaesuChecksum(1, blocksize)
if checksum.get_existing(data) != checksum.get_calculated(data):
raise Exception("Checksum Failed [%02X<>%02X] block %02X, "
"data len: %i" %
(checksum.get_existing(data),
checksum.get_calculated(data),
blocknum, len(data)))
data = data[1:blocksize + 1] # Chew blocknum and checksum
else:
raise Exception("Unable to read blocknum %02X "
"expected blocksize %i got %i." %
(blocknum, blocksize+2, len(data)))
return data
def _clone_in(self):
# Be very patient with the radio
self.pipe.timeout = 4
start = time.time()
data = b""
blocknum = 0
status = chirp_common.Status()
status.msg = "Cloning..."
self.status_fn(status)
status.max = len(self._block_lengths)
for blocksize in self._block_lengths:
data += self._read(blocksize, blocknum)
blocknum += 1
status.cur = blocknum
self.status_fn(status)
LOG.info("Clone completed in %i seconds, blocks read: %i" %
(time.time() - start, blocknum))
return memmap.MemoryMapBytes(data)
def _clone_out(self):
looppredelay = 0.4
looppostdelay = 1.9
self.pipe.timeout = 4
start = time.time()
blocknum = 0
pos = 0
status = chirp_common.Status()
status.msg = "Cloning to radio..."
self.status_fn(status)
status.max = len(self._block_lengths)
for blocksize in self._block_lengths:
checksum = yaesu_clone.YaesuChecksum(pos, pos+blocksize-1)
blocknumbyte = bytes([blocknum])
payloadbytes = self.get_mmap()[pos:pos+blocksize]
checksumbyte = bytes([checksum.get_calculated(self.get_mmap())])
LOG.debug("Block %i - will send from %i to %i byte " %
(blocknum, pos, pos + blocksize))
LOG.debug(util.hexprint(blocknumbyte))
LOG.debug(util.hexprint(payloadbytes))
LOG.debug(util.hexprint(checksumbyte))
# send wrapped bytes
time.sleep(looppredelay)
self.pipe.write(blocknumbyte)
self.pipe.write(payloadbytes)
self.pipe.write(checksumbyte)
tmp = self.pipe.read(blocksize + 2) # chew echo
LOG.debug("bytes echoed: ")
LOG.debug(util.hexprint(tmp))
# radio is slow to write/ack:
time.sleep(looppostdelay)
buf = self.pipe.read(1)
LOG.debug("ack recd:")
LOG.debug(util.hexprint(buf))
if not buf:
if blocknum == 0:
raise errors.RadioNoResponse()
raise errors.RadioError(
"Radio did not ack block %i" % blocknum)
if buf != CMD_ACK:
raise Exception("Radio did not ack block %i" % blocknum)
pos += blocksize
blocknum += 1
status.cur = blocknum
self.status_fn(status)
LOG.info("Clone completed in %i seconds" % (time.time() - start))
def sync_in(self):
try:
self._mmap = self._clone_in()
except errors.RadioError:
raise
except Exception:
trace = traceback.format_exc()
raise errors.RadioError(
"Failed to communicate with radio: %s" % trace)
self.process_mmap()
def sync_out(self):
try:
self._clone_out()
except errors.RadioError:
raise
except Exception:
trace = traceback.format_exc()
raise errors.RadioError(
"Failed to communicate with radio: %s" % trace)
def process_mmap(self):
self._memobj = bitwise.parse(self.mem_format, self._mmap)
def _get_chan_enable(self, number):
number = number - 1
bytepos = number // 8
bitpos = number % 8
chan_enable = self._memobj.chan_enable[bytepos]
if chan_enable & (1 << bitpos):
return True
else:
return False
def _set_chan_enable(self, number, enable):
number = number - 1
bytepos = number // 8
bitpos = number % 8
chan_enable = self._memobj.chan_enable[bytepos]
if enable:
chan_enable = chan_enable | (1 << bitpos) # enable
else:
chan_enable = chan_enable & ~(1 << bitpos) # disable
self._memobj.chan_enable[bytepos] = chan_enable
def get_memory(self, number):
mem = chirp_common.Memory()
if isinstance(number, str):
# special channel
_mem = getattr(self._memobj, number)
mem.number = - len(FT90_SPECIAL) + FT90_SPECIAL.index(number)
mem.extd_number = number
if re.match('^pms', mem.extd_number):
# enable pms_XY channel flag
_special_enables = self._memobj.special_enables
# Disabled in 2023 because this doesn't pass tests and I'm
# not sure what it is really trying to accomplish.
# mem.empty = not getattr(_special_enables,
# mem.extd_number + "_enable")
else:
# regular memory
_mem = self._memobj.memory[number-1]
mem.number = number
mem.empty = not self._get_chan_enable(number)
if mem.empty:
return mem # bail out, do not parse junk
mem.freq = _mem.rxfreq * 10
mem.offset = _mem.txfreqoffset * 10
if not _mem.tmode < len(FT90_TMODES):
_mem.tmode = 0
mem.tmode = FT90_TMODES[_mem.tmode]
mem.rtone = FT90_TONES[_mem.tone]
mem.dtcs = chirp_common.DTCS_CODES[_mem.dcstone]
mem.mode = FT90_MODES[_mem.mode]
mem.duplex = FT90_DUPLEX[_mem.shift]
if mem.freq / 1000000 > 300:
mem.power = FT90_POWER_LEVELS_UHF[_mem.power]
else:
mem.power = FT90_POWER_LEVELS_VHF[_mem.power]
# radio has a known bug with 5 kHz step and squelch
if _mem.step == 0 or _mem.step > len(FT90_STEPS)-1:
_mem.step = 2
mem.tuning_step = FT90_STEPS[_mem.step]
mem.skip = _mem.skip and "S" or ""
if not all(char in chirp_common.CHARSET_ASCII
for char in str(_mem.name)):
# don't display blank/junk name
mem.name = ""
else:
mem.name = str(_mem.name).rstrip()
return mem
def get_raw_memory(self, number):
return repr(self._memobj.memory[number-1])
def set_memory(self, mem):
if mem.number < 0: # special channels
_mem = getattr(self._memobj, mem.extd_number)
if re.match('^pms', mem.extd_number):
# enable pms_XY channel flag
_special_enables = self._memobj.special_enables
setattr(_special_enables, mem.extd_number + "_enable", True)
else:
_mem = self._memobj.memory[mem.number - 1]
self._set_chan_enable(mem.number, not mem.empty)
_mem.skip = mem.skip == "S"
# radio has a known bug with 5 kHz step and dead squelch
if not mem.tuning_step or mem.tuning_step == FT90_STEPS[0]:
_mem.step = 2
else:
_mem.step = FT90_STEPS.index(mem.tuning_step)
_mem.rxfreq = mem.freq / 10
# vfo will unlock if not in right band?
if mem.freq > 300000000:
# uhf
_mem.isUhf1 = 1
_mem.isUhf2 = 1
if mem.freq > 810000000:
# uhf hiband
_mem.isUhfHi = 1
else:
_mem.isUhfHi = 0
else:
# vhf
_mem.isUhf1 = 0
_mem.isUhf2 = 0
_mem.isUhfHi = 0
_mem.txfreqoffset = mem.offset / 10
_mem.tone = FT90_TONES.index(mem.rtone)
_mem.tmode = FT90_TMODES.index(mem.tmode)
_mem.mode = FT90_MODES.index(mem.mode)
_mem.shift = FT90_DUPLEX.index(mem.duplex)
_mem.dcstone = chirp_common.DTCS_CODES.index(mem.dtcs)
_mem.step = FT90_STEPS.index(mem.tuning_step)
_mem.shift = FT90_DUPLEX.index(mem.duplex)
if mem.power:
try:
_mem.power = FT90_POWER_LEVELS_VHF.index(mem.power)
except ValueError:
_mem.power = FT90_POWER_LEVELS_UHF.index(mem.power)
else:
_mem.power = 3 # default to low power
if (len(mem.name) == 0):
_mem.name = bytearray.fromhex("80ffffffffffff")
_mem.showname = 0
else:
_mem.name = str(mem.name).ljust(7)
_mem.showname = 1
_mem.UseDefaultName = 0
def _decode_cwid(self, cwidarr):
cwid = ""
LOG.debug("@ +_decode_cwid:")
for byte in cwidarr.get_value():
char = int(byte)
LOG.debug(char)
# bitwise wraps in quotes! get rid of those
if char < len(FT90_CWID_CHARS):
cwid += FT90_CWID_CHARS[char]
return cwid
def _encode_cwid(self, cwidarr):
cwid = ""
LOG.debug("@ _encode_cwid:")
for char in cwidarr.get_value():
cwid += chr(FT90_CWID_CHARS.index(char))
LOG.debug(cwid)
return cwid
def _bbcd2dtmf(self, bcdarr, strlen=16):
# doing bbcd, but with support for ABCD*#
LOG.debug(bcdarr.get_value())
string = ''.join("%02X" % b for b in bcdarr)
LOG.debug("@_bbcd2dtmf, received: %s" % string)
string = string.replace('E', '*').replace('F', '#')
if strlen <= 16:
string = string[:strlen]
return string
def _dtmf2bbcd(self, dtmf):
dtmfstr = dtmf.get_value()
dtmfstr = dtmfstr.replace('*', 'E').replace('#', 'F')
dtmfstr = str.ljust(dtmfstr.strip(), 16, "0")
bcdarr = list(bytearray.fromhex(dtmfstr))
LOG.debug("@_dtmf2bbcd, sending: %s" % bcdarr)
return bcdarr
def get_settings(self):
_settings = self._memobj.settings
basic = RadioSettingGroup("basic", "Basic")
autodial = RadioSettingGroup("autodial", "AutoDial")
keymaps = RadioSettingGroup("keymaps", "KeyMaps")
top = RadioSettings(basic, keymaps, autodial)
rs = RadioSetting(
"beep", "Beep",
RadioSettingValueBoolean(_settings.beep))
basic.append(rs)
rs = RadioSetting(
"lock", "Lock",
RadioSettingValueBoolean(_settings.lock))
basic.append(rs)
rs = RadioSetting(
"ars", "Auto Repeater Shift",
RadioSettingValueBoolean(_settings.ars))
basic.append(rs)
rs = RadioSetting(
"txpwrsave", "TX Power Save",
RadioSettingValueBoolean(_settings.txpwrsave))
basic.append(rs)
rs = RadioSetting(
"txnarrow", "TX Narrow",
RadioSettingValueBoolean(_settings.txnarrow))
basic.append(rs)
options = ["Off", "S-3", "S-5", "S-Full"]
rs = RadioSetting(
"rfsqlvl", "RF Squelch Level",
RadioSettingValueList(
options, current_index=_settings.rfsqlvl))
basic.append(rs)
options = ["Off", "Band A", "Band B", "Both"]
rs = RadioSetting(
"pttlock", "PTT Lock",
RadioSettingValueList(
options, current_index=_settings.pttlock))
basic.append(rs)
rs = RadioSetting(
"cwid_en", "CWID Enable",
RadioSettingValueBoolean(_settings.cwid_en))
basic.append(rs)
cwid = RadioSettingValueString(0, 7, self._decode_cwid(_settings.cwid))
cwid.set_charset(FT90_CWID_CHARS)
rs = RadioSetting("cwid", "CWID", cwid)
basic.append(rs)
options = ["OFF"] + [str(x) for x in (range(1, 12+1))]
rs = RadioSetting(
"apo", "APO time (hrs)",
RadioSettingValueList(options, current_index=_settings.apo))
basic.append(rs)
options = ["Off"] + [str(x) for x in (range(1, 60+1))]
rs = RadioSetting(
"tot", "Time Out Timer (mins)",
RadioSettingValueList(options, current_index=_settings.tot))
basic.append(rs)
options = ["off", "Auto/TX", "Auto", "TX"]
rs = RadioSetting(
"fancontrol", "Fan Control",
RadioSettingValueList(
options, current_index=_settings.fancontrol))
basic.append(rs)
keyopts = ["Scan Up", "Scan Down", "Repeater", "Reverse", "Tone Burst",
"Tx Power", "Home Ch", "VFO/MR", "Tone", "Priority"]
rs = RadioSetting(
"key_lt", "Left Key",
RadioSettingValueList(keyopts, current_index=_settings.key_lt))
keymaps.append(rs)
rs = RadioSetting(
"key_rt", "Right Key",
RadioSettingValueList(keyopts, current_index=_settings.key_rt))
keymaps.append(rs)
rs = RadioSetting(
"key_p1", "P1 Key",
RadioSettingValueList(keyopts, current_index=_settings.key_p1))
keymaps.append(rs)
rs = RadioSetting(
"key_p2", "P2 Key",
RadioSettingValueList(keyopts, current_index=_settings.key_p2))
keymaps.append(rs)
rs = RadioSetting(
"key_acc", "ACC Key",
RadioSettingValueList(
keyopts, current_index=_settings.key_acc))
keymaps.append(rs)
options = [str(x) for x in range(0, 12+1)]
rs = RadioSetting(
"lcdcontrast", "LCD Contrast",
RadioSettingValueList(
options, current_index=_settings.lcdcontrast))
basic.append(rs)
options = ["off", "d4", "d3", "d2", "d1"]
rs = RadioSetting(
"dimmer", "Dimmer",
RadioSettingValueList(options, current_index=_settings.dimmer))
basic.append(rs)
options = ["TRX Normal", "RX Reverse", "TX Reverse", "TRX Reverse"]
rs = RadioSetting(
"dcsmode", "DCS Mode",
RadioSettingValueList(
options, current_index=_settings.dcsmode))
basic.append(rs)
options = ["50 ms", "100 ms"]
rs = RadioSetting(
"dtmfspeed", "DTMF Speed",
RadioSettingValueList(
options, current_index=_settings.dtmfspeed))
autodial.append(rs)
options = ["50 ms", "250 ms", "450 ms", "750 ms", "1 sec"]
rs = RadioSetting(
"dtmftxdelay", "DTMF TX Delay",
RadioSettingValueList(
options, current_index=_settings.dtmftxdelay))
autodial.append(rs)
options = [str(x) for x in range(1, 8 + 1)]
rs = RadioSetting(
"dtmf_active", "DTMF Active",
RadioSettingValueList(
options, current_index=_settings.dtmf_active))
autodial.append(rs)
# setup 8 dtmf autodial entries
for i in range(1, 9):
objname = "dtmf" + str(i)
dtmfsetting = getattr(_settings, objname)
dtmflen = getattr(_settings, objname + "_len")
dtmfstr = self._bbcd2dtmf(dtmfsetting, dtmflen)
dtmf = RadioSettingValueString(0, 16, dtmfstr)
dtmf.set_charset(FT90_DTMF_CHARS + list(" "))
rs = RadioSetting(objname, objname.upper(), dtmf)
autodial.append(rs)
return top
def set_settings(self, uisettings):
_settings = self._memobj.settings
for element in uisettings:
if not isinstance(element, RadioSetting):
self.set_settings(element)
continue
if not element.changed():
continue
try:
setting = element.get_name()
oldval = getattr(_settings, setting)
newval = element.value
if setting == "cwid":
newval = self._encode_cwid(newval)
if re.match(r'dtmf\d', setting):
# set dtmf length field and then get bcd dtmf
dtmfstrlen = len(str(newval).strip())
setattr(_settings, setting + "_len", dtmfstrlen)
newval = self._dtmf2bbcd(newval)
LOG.debug("Setting %s(%s) <= %s" % (setting, oldval, newval))
setattr(_settings, setting, newval)
except Exception:
LOG.debug(element.get_name())
raise
chirp-20261030/chirp/drivers/ftm150.py 0000644 0001750 0001750 00000112532 15270732464 016221 0 ustar hibby hibby # Copyright 2026 CHIRP contributors
#
# 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 .
"""Yaesu FTM-150E experimental CHIRP driver.
Memory map from ADMS-17 CLNFTM150nn.dat dumps (98304 bytes, ident AH086).
Downloading from and uploading to the radio over a cable both work.
Also, only the full backup (CLNFTM150nn.dat) is supported -
setup (SYSFTM150nn.dat) and memory (MEMFTM150nn.dat) files are not supported.
Not every feature of FTM-150/ADMS-17 is currently supported by this driver.
"""
import logging
from chirp.drivers import ftm300d
from chirp.drivers import yaesu_clone
from chirp import bitwise
from chirp import chirp_common
from chirp import directory
from chirp import errors
from chirp.settings import RadioSetting
from chirp.settings import RadioSettingGroup
from chirp.settings import RadioSettings
from chirp.settings import RadioSettingValueBoolean
from chirp.settings import RadioSettingValueList
from chirp.settings import RadioSettingValueString
LOG = logging.getLogger(__name__)
# ---------------------------------------------------------------------------
# Clone protocol, 38400 8N1 on the DATA jack. Captured from a real
# [58 This -> Other] transfer: the FTM-150 speaks exactly what ftm300d.py
# already implements for the FTM-300DR, down to the skipped blocks, the
# wrap to the name tables and the two trailer frames, so the clone code is
# reused from there. The image has the layout of the radio's own SD backup
# (CLNFTM150nn.dat). The radio never transmits block 0x0400, so it stays
# zero in a cable-read image; it is zero in every SD backup seen so far,
# too, and no known setting lives there.
# ---------------------------------------------------------------------------
# The .dat format keeps a checksum over everything before this offset, but
# the radio transmits 0x00 there -- the clone stream carries its own
# per-frame checksum instead. ADMS-17 fills the byte in when it writes a
# file from a cable read and sends 0x00 again when it uploads, so do the
# same: without it, saving a downloaded image yields a .dat whose checksum
# does not match its contents. Nothing this driver exposes for editing
# lives below the offset, so computing it once after the download is
# enough.
FILE_CHECKSUM_AT = 0x007F
# ---------------------------------------------------------------------------
# Frequency encoding, the same as on the FTM-300D: the 100 MHz digit and
# the BCD digits down to 10 kHz, plus a 4-bit count of 625 Hz steps on
# top. ftm300d.py and ftm350.py treat the latter as single 5k/2.5k/1.25k
# (/625) flags, but together they are just binary 8/4/2/1 x 625 Hz.
# ---------------------------------------------------------------------------
YFREQ_FORMAT = """
struct yfreq {
u8 fine:4,
hun:4;
bbcd khz10[2];
};
"""
def decode_freq(freq):
"""Decode a yfreq struct to Hz."""
return ((int(freq.hun) * 10000 + int(freq.khz10)) * 10000
+ int(freq.fine) * 625)
def encode_freq(freq, hz):
"""Encode Hz into a yfreq struct."""
khz10, rest = divmod(hz, 10000)
freq.hun = khz10 // 10000 % 10
freq.khz10 = khz10 % 10000
freq.fine = rest // 625
# ---------------------------------------------------------------------------
# Memory map.
#
# `chanrecord` is the 16-byte per-channel layout shared, byte-for-byte, by
# regular memory channels, PMS entries, and VFO/HOME band records.
#
# `namerecord` is the 16-byte, 0xFF-padded ASCII name shared by all of
# those tables too.
#
# The FTM-300D shares this layout, but some of its fields do not apply to
# the analog-only FTM-150 and are named unknown here on purpose:
# - unknown3 (byte 1, 0x40) is ftm300d's "ams". The FTM-150 has no AMS,
# yet the radio sets the bit on some records (also at 0x0091 and
# 0x00E2), while ADMS-17 clears it everywhere when writing a .dat.
# Meaning unknown; the driver never touches it.
# - unknown5 (byte 11, 0x40) is ftm300d's "dn", unknown8 and unknown9
# (bytes 14 and 15) are its "dgid_rx" and "dgid_tx". All C4FM; never
# seen set on an FTM-150.
# ---------------------------------------------------------------------------
MEM_FORMAT = YFREQ_FORMAT + """
struct chanrecord {
u8 used:1,
unknown0:1,
skip:1,
memref:1,
unknown1:2,
band:2;
u8 unused_msb:1,
unknown3:1,
narrow:1,
am:1,
duplex:4;
struct yfreq freq;
u8 tone_mode:4,
tune_step:4;
struct yfreq tx_freq;
u8 power:2,
tone:6;
u8 clock_shift:1,
dcs:7;
u8 unknown4:1,
unknown5:1,
unknown6:1,
pr_freq:5;
u8 unknown7;
u8 offset;
u8 unknown8;
u8 unknown9;
};
struct namerecord {
u8 text[16];
};
struct dtmfrecord {
u8 digits[16];
};
// 0x0000
u8 ident[6];
#seekto 0x00A1;
u8 unit;
#seekto 0x00A8;
u8 tz_west:1,
tz_mag:7;
#seekto 0x00CF;
u8 pmg_selected;
#seekto 0x00DF;
u8 vfo_a_active;
#seekto 0x00FB;
u8 pmg_active;
#seekto 0x0100;
struct chanrecord vfo_a[4];
#seekto 0x0150;
struct chanrecord vfo_b[4];
#seekto 0x01A0;
struct chanrecord home_a[4];
#seekto 0x01F0;
struct chanrecord home_b[4];
#seekto 0x028F;
u8 lcd_brightness;
#seekto 0x02C8;
u8 callsign[10];
// 0x02D2
u8 pager_rx[2];
u8 pager_tx[2];
#seekto 0x0300;
struct dtmfrecord dtmf[9];
#seekto 0x03B0;
struct namerecord home_b_names[4];
#seekto 0x0480;
struct namerecord home_a_names[4];
#seekto 0x0800;
struct chanrecord memory[999];
// 0x4670
struct chanrecord pms[100];
// 0x4CB0
u16 nameflags[999];
#seekto 0x5600;
struct chanrecord pmg[5];
#seekto 0x6800;
struct namerecord wx_names[10];
// 0x68A0
u16 wx_nameflags[10];
#seekto 0x10000;
struct namerecord names[999];
// 0x13E70
struct namerecord pms_names[100];
"""
# This is the real radio-generated (not ADMS-17-resaved) empty slot backup.
# ADMS-17 itself writes a *different* pattern for empty channels on resave.
EMPTY_SLOT = bytes.fromhex(
"01 00 01 44 00 04 00 00 00 0c 00 0f 00 0c 00 00")
USED_PROTOTYPE = bytes.fromhex(
"81 00 01 46 50 00 00 00 00 0c 00 0f 00 0c 00 00")
POWER_LEVELS = [
chirp_common.PowerLevel("Hi", watts=50),
chirp_common.PowerLevel("Mid", watts=25),
chirp_common.PowerLevel("Low", watts=5),
]
STEPS = ftm300d.STEPS
# Duplex nibble: 0 simplex, 2 minus, 3 plus, 4 split.
DUPLEX_FROM_RADIO = ftm300d.DUPLEX_FROM_RADIO
DUPLEX_TO_RADIO = ftm300d.DUPLEX_TO_RADIO
# High nibble of byte 5, in ADMS-17 terms: 0 OFF, 1 TONE, 2 TONE SQL,
# 3 REV TONE, 4 DCS, 5 PR FREQ, 6 PAGER. CHIRP has no tmode for PR FREQ
# (reverse tone on the User CTCSS frequency, see pr_freq) or PAGER (EPCS
# selective call), so like ft4.py they go into the "sql_override" channel
# extra and the grid shows tmode "" for them. The override wins over tmode
# on write.
TMODE_FROM_RADIO = ftm300d.TMODE_FROM_RADIO
TMODE_TO_RADIO = ftm300d.TMODE_TO_RADIO
EDITABLE_TMODES = ["", "Tone", "TSQL", "TSQL-R", "DTCS"]
SQL_OVERRIDE_NONE = "(None)"
SQL_OVERRIDE_FROM_RADIO = {5: "PR FREQ", 6: "PAGER"}
SQL_OVERRIDE_TO_RADIO = {v: k for k, v in SQL_OVERRIDE_FROM_RADIO.items()}
SQL_OVERRIDES = [SQL_OVERRIDE_NONE] + list(SQL_OVERRIDE_FROM_RADIO.values())
# ADMS-17 "User CTCSS", 300-3000 Hz in 100 Hz steps, stored as Hz / 100 in
# the low 5 bits of byte 11 (confirmed by controlled diff: 300 -> 3,
# 1100 -> 11, 3000 -> 30). Every channel has it, whatever its tone mode;
# the factory value is 1500 Hz. Bit 5 has never been seen set.
PR_FREQS = ["%d Hz" % hz for hz in range(300, 3001, 100)]
PR_FREQ_MIN = 3
# ADMS-17 Signaling -> Pager Code: RX and TX each take two EPCS codes,
# 1-50, stored as code - 1 (confirmed by controlled diff; factory 05/47).
PAGER_CODES = ["%02d" % code for code in range(1, 51)]
# DTMF digit encoding confirmed by controlled diff.
DTMF_CHARSET = "0123456789*#"
DTMF_CHAR_TO_CODE = {str(d): d for d in range(10)}
DTMF_CHAR_TO_CODE.update({"*": 0x0E, "#": 0x0F})
DTMF_CODE_TO_CHAR = {v: k for k, v in DTMF_CHAR_TO_CODE.items()}
DTMF_PAD = 0xFF
# WX is the ten-channel NOAA weather broadcast bank. Only the user-assignable
# name and its flag are stored in the backup (0x6800 / 0x68A0); the
# frequencies themselves live in firmware, which is why they are hardcoded
# here just to label the name fields.
#
# UNTESTED on real hardware: the name storage is confirmed by controlled
# diff, but only against a European FTM-150E, where the WX bank cannot
# normally be reached -- the [P4] key ships assigned to T-CALL there, while
# the USA FTM-150R ships with WX on P4. Nobody has yet checked that a name
# written here shows up on a radio that can actually display the bank.
WX_FREQS = ("162.550", "162.400", "162.475", "162.425", "162.450",
"162.500", "162.525", "161.650", "161.775", "163.275")
def _decode_name(raw):
return ftm300d._decode_padded(raw, 0xFF)
def _encode_name(text, length=16):
cleaned = "".join(
c if c in chirp_common.CHARSET_ASCII else " " for c in text
)[:length]
padded = cleaned.encode("ascii", "replace")
return padded + b"\xff" * (length - len(padded))
def _decode_dtmf(raw):
buf = [int(x) for x in raw]
chars = []
for b in buf:
if b == DTMF_PAD:
break
chars.append(DTMF_CODE_TO_CHAR.get(b, "?"))
return "".join(chars)
def _encode_dtmf(text, length=16):
codes = [DTMF_CHAR_TO_CODE[c] for c in text if c in DTMF_CHAR_TO_CODE]
codes = codes[:length]
return bytes(codes) + bytes([DTMF_PAD]) * (length - len(codes))
# ---------------------------------------------------------------------------
# Special channels: VFO A/B (4 bands each), HOME CH A/B (4 bands each, with
# names), and PMS (50 L/U pairs). All share the exact same 16-byte
# chanrecord layout as regular memory channels
# ---------------------------------------------------------------------------
# Order of the four records inside vfo_a/vfo_b/home_a/home_b, ascending by
# frequency. Byte 0 of each record carries its own index, which is how this
# order was confirmed; "OTHER" covers everything between 2 m and 70 cm.
SPECIAL_BANDS = ("AIR", "144", "OTHER", "430")
def _band_index(hz):
"""Index into SPECIAL_BANDS that the radio stores for a frequency.
Edges confirmed by controlled diff on memory channels and VFO/HOME
records set on the radio: 136.9875 is AIR, 137.9875 and 174.0 are
144, 174.1 and 399.9875 are OTHER, 400.0 and 480.0 are 430. The radio
does not accept exactly 137.0. (ADMS-17 disagrees on 174.0 for
VFO/HOME and puts it in OTHER.)
"""
if hz < 137000000:
return 0
if hz <= 174000000:
return 1
if hz < 400000000:
return 2
return 3
def _build_special_memories():
mapping = {}
idx = -1
for prefix in ("VFO_A", "VFO_B", "Home_A", "Home_B"):
for band in SPECIAL_BANDS:
mapping["%s_%s" % (prefix, band)] = idx
idx -= 1
for n in range(1, 51):
mapping["PMS_L_%02d" % n] = idx
idx -= 1
mapping["PMS_U_%02d" % n] = idx
idx -= 1
return mapping
# ---------------------------------------------------------------------------
# PMG (Primary Memory Group): five chanrecords at 0x5600, packed from slot 1
# without gaps -- the radio moves the later slots up when one is deleted.
# A slot either holds a frequency of its own, as a full record like a VFO,
# or, with memref set, points at a memory channel: then byte 1 is 0xFF,
# bytes 2-3 are the channel number - 1, big endian, and the band in byte 0
# is that of the channel. Deleting a slot clears only byte 0. pmg_selected
# is the slot selected on the radio, 0-based. All confirmed by controlled
# diff.
# ---------------------------------------------------------------------------
PMG_EMPTY = "(Empty)"
def _pmg_channel(raw):
"""Memory channel number a raw PMG slot points at, or None."""
if not raw[0] & 0x80 or not raw[0] & 0x10:
return None
return (raw[2] << 8 | raw[3]) + 1
def _pmg_ref(number, band):
"""Raw PMG slot pointing at memory channel number."""
return bytes([0x90 | band, 0xFF, (number - 1) >> 8,
(number - 1) & 0xFF]) + b"\xff" * 12
SPECIAL_MEMORIES = _build_special_memories()
SPECIAL_MEMORIES_REV = {v: k for k, v in SPECIAL_MEMORIES.items()}
def _special_memory_sort_key(name):
if name.startswith(("VFO_", "Home_")):
family, side, band = name.split("_")
family_order = 0 if family == "VFO" else 1
side_order = 0 if side == "A" else 1
return family_order, side_order, SPECIAL_BANDS.index(band), 0
band, number = name[4:].split("_")
side_order = 0 if band == "L" else 1
return 2, int(number), side_order, 0
@directory.register
class FTM150Radio(yaesu_clone.YaesuCloneModeRadio,
chirp_common.ExperimentalRadio):
"""Yaesu FTM-150E.
Only the European FTM-150E has been tested. The USA FTM-150R probably
uses the same image, but that is unconfirmed, so it is not offered.
"""
VENDOR = "Yaesu"
MODEL = "FTM-150E"
BAUD_RATE = 38400 # confirmed against the radio
NEEDS_COMPAT_SERIAL = False
FORMATS = [directory.register_format('Yaesu FTM-150E SD ALL', '*.dat')]
_model = b"AH086"
_memsize = 98304
# IARU Region 1 limits of the FTM-150E. Many radios are frequency-
# expanded (the test radio is), so validate_memory() only warns.
_tx_bands = [
(144000000, 146000000),
(430000000, 440000000),
]
@classmethod
def get_prompts(cls):
rp = chirp_common.RadioPrompts()
rp.experimental = (
"This FTM-150E driver is experimental. Downloading from and\n"
"uploading to the radio over a cable work, as does opening and\n"
"saving the SD card 'ALL' backup file\n"
"(FTM150/BACKUP/CLNFTM150nn.dat).\n"
"\n"
"Keep an SD card 'ALL' backup before uploading.")
rp.pre_download = (
"1. Connect the cable to the DATA jack on the back of the\n"
" radio.\n"
"2. Press and hold the [F(SETUP)] key, turn the right DIAL\n"
" knob to select [58 This -> Other], then press the DIAL\n"
" knob.\n"
"3. After clicking OK here , turn the DIAL knob on the\n"
" radio to select [OK] and press it to start the transfer.\n")
rp.pre_upload = (
"Keep an SD card 'ALL' backup first.\n"
"\n"
"1. Connect the cable to the DATA jack on the back of the\n"
" radio.\n"
"2. Press and hold the [F(SETUP)] key, turn the right DIAL\n"
" knob to select [59 Other -> This], then press the DIAL\n"
" knob.\n"
"3. Turn the DIAL knob to select [OK] and press it, so the\n"
" radio waits for data.\n"
"4. Click OK here within 30 seconds.\n")
return rp
def get_features(self):
rf = chirp_common.RadioFeatures()
rf.has_bank = False
rf.has_bank_names = False
rf.has_settings = True
rf.has_ctone = False
rf.has_dtcs = True
rf.has_dtcs_polarity = False
rf.has_cross = False
rf.has_rx_dtcs = False
rf.has_comment = False
rf.has_nostep_tuning = False
rf.can_odd_split = True
rf.can_delete = True
rf.memory_bounds = (1, 999)
rf.valid_special_chans = sorted(
SPECIAL_MEMORIES, key=_special_memory_sort_key)
# No "DN" -- the FTM-150 is analog only.
rf.valid_modes = ["FM", "NFM", "AM"]
rf.valid_tmodes = list(EDITABLE_TMODES)
rf.valid_duplexes = ["", "-", "+", "split"]
rf.valid_tuning_steps = list(STEPS)
rf.valid_skips = ["", "S"]
rf.valid_power_levels = POWER_LEVELS
rf.valid_name_length = 16
rf.valid_characters = chirp_common.CHARSET_ASCII
rf.valid_dtcs_codes = list(chirp_common.DTCS_CODES)
rf.valid_bands = [
(108000000, 174000000),
(174000000, 400000000),
# The radio accepts 480.0 itself, but nothing above it.
(400000000, 480000001),
]
return rf
def process_mmap(self):
self._memobj = bitwise.parse(MEM_FORMAT, self._mmap)
def _checksums(self):
return [yaesu_clone.YaesuChecksum(0x0000, FILE_CHECKSUM_AT - 1)]
def sync_in(self):
try:
self._mmap = ftm300d._clone_in(self)
self.update_checksums()
except errors.RadioError:
raise
except Exception as e:
LOG.exception("Failed to download from radio")
raise errors.RadioError(
"Failed to download from radio (%s)" % e)
self.process_mmap()
def sync_out(self):
# Send 0x00 in place of the file checksum, like the radio and
# ADMS-17 do, but leave the image itself intact.
checksum = self._mmap[FILE_CHECKSUM_AT]
self._mmap[FILE_CHECKSUM_AT] = b"\x00"
try:
ftm300d._clone_out(self)
except errors.RadioError:
raise
except Exception as e:
LOG.exception("Failed to upload to radio")
raise errors.RadioError(
"Failed to upload to radio (%s)" % e)
finally:
self._mmap[FILE_CHECKSUM_AT] = checksum
# -- settings: the small Set Mode tables so far. VFO A/B, HOME CH A/B
# and PMS are not here -- they are exposed as special channels in the
# Memories grid instead, see the "special channels" section above.
def get_settings(self):
top = RadioSettings()
top.append(self._get_status())
config = RadioSettingGroup("config", "Config")
top.append(config)
unit = int(self._memobj.unit)
config.append(RadioSetting(
"unit", "Display Units",
RadioSettingValueList(
ftm300d.UNIT_LABELS, current_index=unit if unit in (0, 1)
else 0)))
brightness = int(self._memobj.lcd_brightness)
config.append(RadioSetting(
"lcd_brightness", "LCD Brightness (raw value)",
RadioSettingValueString(0, 2, str(brightness))))
dtmf = RadioSettingGroup("dtmf", "DTMF Memory")
top.append(dtmf)
for i in range(9):
code = _decode_dtmf(self._memobj.dtmf[i].digits)
dtmf.append(RadioSetting(
"dtmf%d" % i, "Channel %d" % (i + 1),
RadioSettingValueString(
0, 16, code, autopad=False, charset=DTMF_CHARSET)))
signaling = RadioSettingGroup("signaling", "Signaling")
top.append(signaling)
for direction in ("rx", "tx"):
codes = getattr(self._memobj, "pager_" + direction)
for i in range(2):
code = int(codes[i])
if code >= len(PAGER_CODES):
# Out of range values are left alone rather than shown
# wrong.
continue
rs = RadioSetting(
"pager_%s%d" % (direction, i),
"Pager %s Code %d" % (direction.upper(), i + 1),
RadioSettingValueList(PAGER_CODES, current_index=code))
rs.set_doc(
"Used by channels whose Squelch override is PAGER.")
signaling.append(rs)
wx = RadioSettingGroup("wx", "WX Channel Names")
top.append(wx)
for i in range(10):
rs = RadioSetting(
"wx%d" % i, "WX-CH%02d (%s MHz)" % (i + 1, WX_FREQS[i]),
RadioSettingValueString(
0, 16, _decode_name(self._memobj.wx_names[i].text),
autopad=False))
rs.set_doc(
"Name only -- the frequency is fixed in firmware. The WX "
"bank is reachable only via a programmable key, assigned "
"to [P4] on the USA FTM-150R but to T-CALL on the "
"European FTM-150E.")
wx.append(rs)
top.append(self._get_pmg())
return top
# Operating state from block 0x0080, which the radio sends last and an
# upload restores. Read only: switching it means changing several
# bytes that depend on the active VFO. Confirmed by controlled diff:
# 0x00FB is 0x0A with PMG on and 0x00 with it off, on either VFO, and
# 0x00DF is 1 on VFO A (left on the display) and 0 on VFO B (right).
# With PMG on, the radio also sets 0x80 in byte 3 of the active VFO's
# block (0x00E0 A, 0x00F0 B), 0x01 in byte 4 of the other one, and
# clears 0x00D0 (A) or 0x00C0 (B).
def _get_status(self):
status = RadioSettingGroup("status", "Current Status")
pmg_active = int(self._memobj.pmg_active)
if pmg_active in (0x00, 0x0A):
rs = RadioSetting(
"pmg_active", "PMG active",
RadioSettingValueBoolean(pmg_active == 0x0A))
rs.value.set_mutable(False)
rs.set_doc("Uploading restores the state saved in this "
"image, whatever the radio was set to since.")
status.append(rs)
vfo_a = int(self._memobj.vfo_a_active)
if vfo_a in (0, 1):
rs = RadioSetting(
"vfo_a_active", "Active VFO",
RadioSettingValueList(
["B (right)", "A (left)"], current_index=vfo_a))
rs.value.set_mutable(False)
rs.set_doc("Uploading restores the state saved in this "
"image, whatever the radio was set to since.")
status.append(rs)
return status
def _channel_label(self, number):
if int(self._memobj.memory[number - 1].used):
name = _decode_name(self._memobj.names[number - 1].text)
else:
name = "(empty)"
return ("%d %s" % (number, name)).strip()
# PMG slots are settings, as the radio treats PMG as a list of five
# entries. A slot holding a frequency of its own is a full channel
# (offset, tone, power, ...) that a settings field cannot edit fully,
# so it is shown and kept but cannot be created or changed here. The
# intended way to add that is special channels PMG_1..PMG_5 in the
# memory grid, like VFO and HOME, with these settings still showing
# what each slot holds.
def _get_pmg(self):
pmg = RadioSettingGroup("pmg", "PMG")
channels = [self._channel_label(i + 1)
for i, rec in enumerate(self._memobj.memory)
if int(rec.used)]
for i, rec in enumerate(self._memobj.pmg):
options = [PMG_EMPTY] + channels
current = PMG_EMPTY
number = _pmg_channel(rec.get_raw())
if number is not None:
current = self._channel_label(number)
if current not in options:
# Points at a channel deleted since; kept as is.
options.append(current)
elif int(rec.used):
current = "Frequency %s MHz" % chirp_common.format_freq(
decode_freq(rec.freq))
options.insert(1, current)
rs = RadioSetting(
"pmg%d" % i, "Slot %d" % (i + 1),
RadioSettingValueList(
options, current_index=options.index(current)))
rs.set_doc(
"A memory channel, or a frequency stored in the slot "
"itself. Frequencies can only be stored on the radio. "
"Emptied slots are filled by the slots after them, as on "
"the radio.")
pmg.append(rs)
selected = int(self._memobj.pmg_selected)
slots = ["Slot %d" % (i + 1) for i in range(len(self._memobj.pmg))]
rs = RadioSetting(
"pmg_selected", "Selected slot",
RadioSettingValueList(
slots, current_index=selected if selected < len(slots)
else 0))
rs.set_doc("The slot whose name the radio shows. Follows its "
"entry when that moves up; falls back to slot 1 when "
"the selected slot is emptied.")
pmg.append(rs)
return pmg
def _set_pmg(self, pmg):
old = [rec.get_raw() for rec in self._memobj.pmg]
slots = []
moved_to = {}
selected = 0
for element in pmg:
name = element.get_name()
value = str(element.value)
if name == "pmg_selected":
selected = int(value.split()[1]) - 1
continue
if value == PMG_EMPTY:
continue
idx = int(name[len("pmg"):])
moved_to[idx] = len(slots)
raw = old[idx]
if value.startswith("Frequency "):
slots.append(raw)
continue
number = int(value.split()[0])
if _pmg_channel(raw) != number:
raw = _pmg_ref(
number, int(self._memobj.memory[number - 1].band))
slots.append(raw)
for i, rec in enumerate(self._memobj.pmg):
if i < len(slots):
rec.set_raw(slots[i])
elif old[i][0] & 0x80:
rec.set_raw(b"\x00" + old[i][1:])
# The selection follows its slot when that moves up.
self._memobj.pmg_selected = moved_to.get(selected, 0)
def set_settings(self, settings):
for element in settings:
if not isinstance(element, RadioSetting):
if element.get_name() == "pmg":
self._set_pmg(element)
else:
self.set_settings(element)
continue
name = element.get_name()
value = str(element.value)
if name == "unit":
self._memobj.unit = ftm300d.UNIT_LABELS.index(value)
elif name == "lcd_brightness":
try:
self._memobj.lcd_brightness = int(value)
except ValueError:
pass
elif name.startswith("dtmf"):
idx = int(name[len("dtmf"):])
ftm300d._set_bytes(self._memobj.dtmf[idx].digits,
_encode_dtmf(value))
elif name.startswith(("pager_rx", "pager_tx")):
codes = getattr(self._memobj, name[:-1])
codes[int(name[-1])] = PAGER_CODES.index(value)
elif name.startswith("wx"):
idx = int(name[len("wx"):])
text = value.strip()
ftm300d._set_bytes(self._memobj.wx_names[idx].text,
_encode_name(text))
self._memobj.wx_nameflags[idx] = 0x0000 if text else 0xFFFF
# -- memory channels (1-999): logic ported from ftm300d.py
def get_raw_memory(self, number):
if isinstance(number, str):
arr, _names_arr, idx, _kind = self._special_target(number)
return repr(getattr(self._memobj, arr)[idx])
if number < 0:
return self.get_raw_memory(SPECIAL_MEMORIES_REV[number])
return repr(self._memobj.memory[number - 1])
def validate_memory(self, mem):
msgs = super().validate_memory(mem)
if mem.empty:
return msgs
if mem.number in SPECIAL_MEMORIES_REV:
name = SPECIAL_MEMORIES_REV[mem.number]
_arr, _names_arr, idx, kind = self._special_target(name)
if kind != "pms" and _band_index(mem.freq) != idx:
msgs.append(chirp_common.ValidationError(
"%s only holds %s band frequencies" % (
name, SPECIAL_BANDS[idx])))
if mem.duplex == "split":
tx = mem.offset
elif mem.duplex == "-":
tx = mem.freq - mem.offset
elif mem.duplex == "+":
tx = mem.freq + mem.offset
else:
tx = mem.freq
in_tx = any(lo <= tx < hi for lo, hi in self._tx_bands)
if not in_tx:
if mem.duplex in ("-", "+", "split"):
# Only a warning: an expanded radio transmits here, and a
# stock one refuses to on its own.
msgs.append(chirp_common.ValidationWarning(
"TX frequency is outside amateur allocations "
"for this model"))
else:
msgs.append(chirp_common.ValidationWarning(
"RX-only frequency; radio will not transmit here"))
return msgs
# Shared record decode/encode, used by both regular channels and
# every special channel (VFO/HOME/PMS) below, since they're all the
# same 16-byte chanrecord layout
def _decode_chanrecord(self, mem, _mem):
mem.freq = decode_freq(_mem.freq)
mem.duplex = DUPLEX_FROM_RADIO.get(int(_mem.duplex), "")
if mem.duplex == "split":
mem.offset = decode_freq(_mem.tx_freq)
else:
mem.offset = int(_mem.offset) * 50000
mem.skip = "S" if int(_mem.skip) else ""
try:
mem.power = POWER_LEVELS[int(_mem.power)]
except IndexError:
mem.power = POWER_LEVELS[0]
try:
mem.tuning_step = STEPS[int(_mem.tune_step)]
except IndexError:
mem.tuning_step = STEPS[0]
tone_mode = int(_mem.tone_mode)
mem.tmode = TMODE_FROM_RADIO.get(tone_mode, "")
mem.extra = RadioSettingGroup("extra", "Extra")
rs = RadioSetting(
"sql_override", "Squelch override",
RadioSettingValueList(
SQL_OVERRIDES,
SQL_OVERRIDE_FROM_RADIO.get(tone_mode, SQL_OVERRIDE_NONE)))
rs.set_doc(
"PR FREQ and PAGER have no CHIRP tone mode. When one is "
"selected here, the channel uses it on the radio regardless "
"of Tone Mode. PR FREQ uses the User CTCSS frequency, PAGER "
"the pager codes under Settings -> Signaling.")
mem.extra.append(rs)
pr_freq = int(_mem.pr_freq) - PR_FREQ_MIN
if 0 <= pr_freq < len(PR_FREQS):
# Out of range values are left alone rather than shown wrong.
rs = RadioSetting(
"pr_freq", "User CTCSS",
RadioSettingValueList(PR_FREQS, current_index=pr_freq))
rs.set_doc(
"Select the idle line frequency to remove signals such "
"as idle line signals used by private railways and "
"control signals of MCA radio systems. Only used when "
"Squelch override is PR FREQ.")
mem.extra.append(rs)
rs = RadioSetting(
"clock_shift", "Clock Shift",
RadioSettingValueBoolean(bool(int(_mem.clock_shift))))
rs.set_doc(
"Shift the CPU clock frequency on this channel, to move an "
"internal spurious signal (\"birdie\") away from it.")
mem.extra.append(rs)
try:
mem.rtone = chirp_common.TONES[int(_mem.tone)]
except IndexError:
mem.rtone = 88.5
mem.ctone = mem.rtone
try:
mem.dtcs = chirp_common.DTCS_CODES[int(_mem.dcs)]
except IndexError:
mem.dtcs = 23
if int(_mem.am):
mem.mode = "AM"
elif int(_mem.narrow):
mem.mode = "NFM"
else:
mem.mode = "FM"
def _encode_chanrecord(self, _mem, mem):
_mem.used = 1
_mem.band = _band_index(mem.freq)
_mem.skip = 1 if mem.skip == "S" else 0
encode_freq(_mem.freq, mem.freq)
_mem.duplex = DUPLEX_TO_RADIO.get(mem.duplex, 0)
if mem.duplex == "split":
encode_freq(_mem.tx_freq, int(mem.offset))
_mem.offset = 0
else:
encode_freq(_mem.tx_freq, 0)
_mem.offset = (
int(round(mem.offset / 50000.0)) if mem.offset else 0)
_mem.am = 1 if mem.mode == "AM" else 0
_mem.narrow = 1 if mem.mode == "NFM" else 0
try:
_mem.tune_step = STEPS.index(mem.tuning_step)
except ValueError:
_mem.tune_step = 0
_mem.tone_mode = TMODE_TO_RADIO.get(mem.tmode, 0)
for setting in mem.extra:
if setting.get_name() == "sql_override":
override = str(setting.value)
if override in SQL_OVERRIDE_TO_RADIO:
_mem.tone_mode = SQL_OVERRIDE_TO_RADIO[override]
elif setting.get_name() == "pr_freq":
_mem.pr_freq = (
PR_FREQS.index(str(setting.value)) + PR_FREQ_MIN)
elif setting.get_name() == "clock_shift":
_mem.clock_shift = 1 if setting.value else 0
try:
_mem.tone = chirp_common.TONES.index(mem.rtone)
except ValueError:
_mem.tone = chirp_common.TONES.index(88.5)
try:
_mem.dcs = chirp_common.DTCS_CODES.index(mem.dtcs)
except ValueError:
_mem.dcs = 0
if mem.power in POWER_LEVELS:
_mem.power = POWER_LEVELS.index(mem.power)
else:
_mem.power = 0
# dispatch the regular numbered channels (1-999) vs. the special
# channels (string name, or the negative int CHIRP stores internally)
def get_memory(self, number):
if isinstance(number, str):
return self._get_special(number)
if number < 0:
return self._get_special(SPECIAL_MEMORIES_REV[number])
return self._get_normal(number)
def set_memory(self, mem):
if mem.number < 0:
return self._set_special(mem)
return self._set_normal(mem)
def _get_normal(self, number):
mem = chirp_common.Memory()
mem.number = number
_mem = self._memobj.memory[number - 1]
if not int(_mem.used):
mem.empty = True
return mem
mem.empty = False
self._decode_chanrecord(mem, _mem)
mem.name = _decode_name(self._memobj.names[number - 1].text)
return mem
def _set_normal(self, mem):
_mem = self._memobj.memory[mem.number - 1]
name_idx = mem.number - 1
if mem.empty:
_mem.set_raw(EMPTY_SLOT)
ftm300d._set_bytes(self._memobj.names[name_idx].text, b"\xff" * 16)
self._memobj.nameflags[name_idx] = 0xFFFF
return
if not int(_mem.used):
_mem.set_raw(USED_PROTOTYPE)
self._encode_chanrecord(_mem, mem)
encoded = _encode_name(mem.name)
ftm300d._set_bytes(self._memobj.names[name_idx].text, encoded)
self._memobj.nameflags[name_idx] = (
0x0000 if mem.name.strip() else 0xFFFF)
def _special_target(self, name):
"""Return (chanrecord array attr, names array attr or None, index,
kind) for a special channel name. kind is 'vfo', 'home' or 'pms'."""
for prefix, arr, names_arr, kind in (
("VFO_A", "vfo_a", None, "vfo"),
("VFO_B", "vfo_b", None, "vfo"),
("Home_A", "home_a", "home_a_names", "home"),
("Home_B", "home_b", "home_b_names", "home")):
if name.startswith(prefix + "_"):
band = name[len(prefix) + 1:]
return arr, names_arr, SPECIAL_BANDS.index(band), kind
if name.startswith(("PMS_L_", "PMS_U_")):
prefix, number = name.rsplit("_", 1)
n = int(number) if number.isdigit() else 0
if 1 <= n <= 50:
slot = (n - 1) * 2 + (0 if prefix == "PMS_L" else 1)
return "pms", "pms_names", slot, "pms"
raise errors.InvalidMemoryLocation(
"Unknown FTM-150 special channel %r" % name)
def _get_special(self, name):
arr, names_arr, idx, kind = self._special_target(name)
_mem = getattr(self._memobj, arr)[idx]
_name_obj = (getattr(self._memobj, names_arr)[idx]
if names_arr else None)
mem = chirp_common.Memory()
mem.number = SPECIAL_MEMORIES[name]
mem.extd_number = name
if kind == "pms" and not int(_mem.used):
mem.empty = True
mem.immutable = ["number", "extd_number"]
return mem
mem.empty = False
self._decode_chanrecord(mem, _mem)
if _name_obj is not None:
mem.name = _decode_name(_name_obj.text)
immutable = ["number", "extd_number"]
if _name_obj is None:
# VFO A/B have no name storage at all.
immutable.append("name")
mem.immutable = immutable
return mem
def _set_special(self, mem):
name = SPECIAL_MEMORIES_REV[mem.number]
arr, names_arr, idx, kind = self._special_target(name)
_mem = getattr(self._memobj, arr)[idx]
_name_obj = (getattr(self._memobj, names_arr)[idx]
if names_arr else None)
if mem.empty:
if kind != "pms":
raise errors.RadioError(
"%s cannot be deleted -- VFO/HOME bands always exist "
"on the radio; only PMS entries can be cleared" % name)
_mem.set_raw(EMPTY_SLOT)
if _name_obj is not None:
ftm300d._set_bytes(_name_obj.text, b"\xff" * 16)
return
if kind == "pms" and not int(_mem.used):
_mem.set_raw(USED_PROTOTYPE)
self._encode_chanrecord(_mem, mem)
if _name_obj is not None:
ftm300d._set_bytes(_name_obj.text, _encode_name(mem.name))
chirp-20261030/chirp/drivers/ftm300d.py 0000644 0001750 0001750 00000127607 15253200010 016345 0 ustar hibby hibby # Copyright 2026 CHIRP contributors
#
# 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 .
"""Yaesu FTM-300DR experimental driver.
Memory map from ADMS-12 CLNFTM300D.dat dumps (98304 bytes, ident AH071).
Clone-in/out is 38400 8N1, 131-byte frames (BE address + 128 payload +
sum%256), ACK 0x06, address wrap at 0x10000 for names.
"""
import logging
import struct
from chirp.drivers import yaesu_clone
from chirp import bitwise
from chirp import chirp_common
from chirp import directory
from chirp import errors
from chirp import memmap
from chirp.settings import MemSetting
from chirp.settings import RadioSetting
from chirp.settings import RadioSettingGroup
from chirp.settings import RadioSettings
from chirp.settings import RadioSettingValueBoolean
from chirp.settings import RadioSettingValueFloat
from chirp.settings import RadioSettingValueInteger
from chirp.settings import RadioSettingValueList
from chirp.settings import RadioSettingValueMap
from chirp.settings import RadioSettingValueString
LOG = logging.getLogger(__name__)
FRAME_LEN = 131
PAYLOAD_LEN = 128
# 16-bit clone address wraps; pass 2 is file offset + 0x10000 (names).
CLONE_PASS2 = 0x10000
CLONE_VFO_ADDR = 0xFFFD
CLONE_END_ADDR = 0xFFFE
CLONE_VFO_DEST = 0x0080
# Pass 1 skips 0x0080 (sent later as FFFD) and 0x0400 (never cloned).
CLONE_SKIP = (0x0080, 0x0400)
# ADMS-12 PUT (SCU-56): 768 ACKed frames. An extra leading 0x0000 in the
# capture had no ACK (~11.5s later identical retry) — not part of the
# protocol. GET from the radio also starts 0x0000, 0x0100, ...
# Wire time ~34ms/frame at 38400; ACK ~36ms after OUT; next OUT ~11ms
# after ACK. Do not copy FTM-350's 50ms post-ACK sleep. No greeting
# byte before frame 1; PC writes first and waits for 0x06.
def clone_addresses():
"""Return the frozen ADMS-12 clone address list (768 frames).
Pass 1 0x0000-0xFF80 step 128 skipping 0x0080 and 0x0400, wrap to
file +0x10000 for names, pass 2 0x0000-0x7F80, trailer FFFD (VFO
at 0x80) and FFFE (end, 128 zeros, checksum 0xFD).
"""
addrs = [
addr for addr in range(0x0000, 0x10000, PAYLOAD_LEN)
if addr not in CLONE_SKIP]
addrs.extend(range(0x0000, 0x8000, PAYLOAD_LEN))
addrs.append(CLONE_VFO_ADDR)
addrs.append(CLONE_END_ADDR)
return addrs
def clone_frame(addr, payload):
"""Build one 131-byte clone frame (BE address + 128 payload + sum%256)."""
if len(payload) != PAYLOAD_LEN:
raise ValueError("Clone payload must be %d bytes" % PAYLOAD_LEN)
head = struct.pack(">H", addr) + payload
return head + bytes([sum(head) % 256])
def image_to_clone_frames(mmap):
"""Serialize a 96 KiB image to the 768-frame clone stream."""
data = mmap.get_packed() if hasattr(mmap, "get_packed") else mmap
frames = []
bank = 0
prev = None
for addr in clone_addresses():
if prev is not None and prev >= 0xFF00 and addr < 0x100:
bank = CLONE_PASS2
if addr == CLONE_END_ADDR:
payload = b"\x00" * PAYLOAD_LEN
elif addr == CLONE_VFO_ADDR:
payload = data[CLONE_VFO_DEST:CLONE_VFO_DEST + PAYLOAD_LEN]
else:
dest = addr + bank
payload = data[dest:dest + PAYLOAD_LEN]
frames.append(clone_frame(addr, payload))
prev = addr
return frames
def _read_clone_frame(pipe):
"""Read one 131-byte frame. Empty means nothing has arrived yet."""
frame = pipe.read(FRAME_LEN)
if not frame:
return b""
while len(frame) < FRAME_LEN:
more = pipe.read(FRAME_LEN - len(frame))
if not more:
raise errors.RadioError("Timed out reading from radio")
frame += more
return frame
def _clone_in(radio):
"""Download the radio image over the 131-byte framed clone protocol."""
pipe = radio.pipe
if hasattr(pipe, "timeout"):
pipe.timeout = 1
data = memmap.MemoryMapBytes(b"\x00" * radio._memsize)
expected = len(clone_addresses())
bank = 0
prev_addr = None
frames = 0
attempts = 30
got_end = False
while not got_end:
if frames >= expected + 8:
raise errors.RadioError("Radio sent too many clone frames")
frame = _read_clone_frame(pipe)
if not frame:
if frames:
raise errors.RadioError("Timed out reading from radio")
attempts -= 1
if attempts <= 0:
raise errors.RadioNoResponse()
continue
addr, = struct.unpack(">H", frame[:2])
payload = frame[2:130]
checksum = frame[130]
calc = sum(frame[:130]) % 256
if calc != checksum:
LOG.debug("Calc: %02x Real: %02x addr: %04x",
calc, checksum, addr)
raise errors.RadioError("Block failed checksum")
# SCU-56 does not echo; do not chew 0x06 into the next frame.
pipe.write(b"\x06")
if frames == 0 and addr == 0:
ident = payload[:len(radio._model)]
if ident != radio._model:
raise errors.RadioError(
"Incorrect model ident (got %r)" % payload[:6])
if addr == CLONE_END_ADDR:
got_end = True
elif addr == CLONE_VFO_ADDR:
data[CLONE_VFO_DEST] = payload
else:
if (prev_addr is not None and prev_addr >= 0xFF00
and addr < 0x100):
bank = CLONE_PASS2
LOG.debug("Clone wrap to names at frame %d", frames)
dest = addr + bank
try:
data[dest] = payload
except IndexError:
raise errors.RadioError(
"Clone address 0x%04X maps past end of image" % addr)
prev_addr = addr
frames += 1
status = chirp_common.Status()
status.cur = frames
status.max = expected
status.msg = "Cloning from radio"
radio.status_fn(status)
if frames != expected:
LOG.warning("Expected %d clone frames, got %d", expected, frames)
LOG.debug("Clone-in finished after %d frames", frames)
return data
def _clone_out(radio):
"""Upload the radio image over the 131-byte framed clone protocol."""
pipe = radio.pipe
if hasattr(pipe, "timeout"):
pipe.timeout = 1
ident = bytes(radio.get_mmap()[0:len(radio._model)])
if ident != radio._model:
raise errors.RadioError(
"Incorrect model ident (got %r)" % ident)
frames = image_to_clone_frames(radio.get_mmap())
expected = len(frames)
for i, frame in enumerate(frames):
attempts = 30 if i == 0 else 1
while True:
# SCU-56 does not echo; wait for a real 0x06 from the radio.
pipe.write(frame)
ack = pipe.read(1)
if ack == b"\x06":
break
if ack:
raise errors.RadioError(
"Radio refused block %d (got %r)" % (i, ack))
attempts -= 1
if attempts <= 0:
if i == 0:
raise errors.RadioNoResponse()
raise errors.RadioError(
"Timed out waiting for ACK on block %d" % i)
LOG.debug("No ACK on frame 0, retrying (%d left)", attempts)
status = chirp_common.Status()
status.cur = i + 1
status.max = expected
status.msg = "Cloning to radio"
radio.status_fn(status)
LOG.debug("Clone-out finished after %d frames", expected)
YFREQ_FORMAT = """
struct yfreq {
u8 fine_5k:1,
fine_2k5:1,
fine_1k25:1,
fine_625:1,
hun:4;
u8 bcd[2];
};
"""
MEM_FORMAT = YFREQ_FORMAT + """
#seekto 0x0000;
u8 ident[6];
#seekto 0x0081;
u8 unknown_81_hi:2,
fm_bandwidth_a:1,
rx_am_a:1,
rpt_shift_a:4;
#seekto 0x0085;
u8 vfo_tone_a:4,
vfo_step_a:4;
#seekto 0x008C;
u16 rpt_shift_freq_a;
#seekto 0x0091;
u8 unknown_91_hi:2,
fm_bandwidth_b:1,
rx_am_b:1,
rpt_shift_b:4;
#seekto 0x0095;
u8 vfo_tone_b:4,
vfo_step_b:4;
#seekto 0x009C;
u16 rpt_shift_freq_b;
#seekto 0x009A;
u8 clock_type_b:1,
unknown_9a:7;
#seekto 0x00A1;
u8 unit;
#seekto 0x00A4;
u8 apo;
#seekto 0x00A8;
u8 tz_west:1,
tz_mag:7;
#seekto 0x00AA;
u8 tot;
#seekto 0x00AD;
u8 display_mode;
#seekto 0x00AF;
u8 gps_datum;
#seekto 0x00B5;
u8 gps_log;
u8 vox;
u8 vox_delay;
u8 recording_band;
#seekto 0x00E3;
u8 unknown_e3_hi:2,
rx_coverage_a:1,
rx_auto_a:1,
step_auto_a:1,
unknown_e3_2:1,
rx_auto_menu_a:1,
unknown_e3_lo:1;
u8 unknown_e4_hi:5,
rpt_ars_a:1,
unknown_e4_lo:2;
#seekto 0x00E5;
u8 unknown_e5_hi:2,
band_scope:1,
unknown_e5_lo:5;
#seekto 0x00E8;
u8 display_info:1,
unknown_e8_hi:2,
standby_beep_off:1,
unknown_e8_3:1,
target_location:1,
digital_vw:1,
unknown_e8_lo:1;
u8 unknown_e9_hi:6,
sub_band_mute:2;
u8 unknown_ea_hi:2,
unknown_ea_5:1,
unknown_ea_4:1,
recording_mic:1,
unknown_ea_lo:3;
#seekto 0x00EB;
u8 location_service:1,
unknown_eb_6:3,
beep_on:1,
unknown_eb_lo:3;
#seekto 0x00EC;
u8 gps_device:1,
unknown_ec:7;
#seekto 0x00EE;
u8 ams_tx_mode;
#seekto 0x00F3;
u8 unknown_f3_hi:2,
rx_coverage_b:1,
rx_auto_b:1,
step_auto_b:1,
unknown_f3_2:1,
rx_auto_menu_b:1,
unknown_f3_lo:1;
u8 unknown_f4_hi:5,
rpt_ars_b:1,
unknown_f4_lo:2;
#seekto 0x00F9;
u8 unknown_f9_hi:2,
unknown_f9_5:1,
time_12hr:1,
unknown_f9_3:1,
date_fmt:3;
#seekto 0x00FA;
u8 unknown_fa_hi:3,
beep_high:1,
unknown_fa_lo:4;
#seekto 0x028F;
u8 lcd_brightness;
#seekto 0x02D6;
u8 compass;
#seekto 0x02DB;
u8 digital_popup;
#seekto 0x02DD;
u8 mic_gain;
#seekto 0x02C8;
u8 callsign[10];
#seekto 0x0508;
u8 aprs_call[6];
u8 aprs_ssid;
#seekto 0x0534;
u8 aprs_modem;
#seekto 0x0800;
struct {
u8 used:1,
unknown0:1,
skip:1,
unknown1:3,
uhf:1,
unknown2:1;
u8 unused_msb:1,
ams:1,
narrow:1,
am:1,
duplex:4;
struct yfreq freq;
u8 tone_mode:4,
tune_step:4;
struct yfreq tx_freq;
u8 power:2,
tone:6;
u8 clock_shift:1,
dcs:7;
u8 unknown4:1,
dn:1,
unknown5:2,
occupied:4;
u8 unknown6;
u8 offset;
u8 dgid_rx;
u8 dgid_tx;
} memory[999];
#seekto 0x4CB0;
u16 nameflags[999];
#seekto 0x10000;
struct {
u8 text[16];
} names[999];
"""
# Factory-empty slot (144.000 analog) vs ADMS-programmed prototype.
EMPTY_SLOT = bytes.fromhex(
"01 00 01 44 00 00 00 00 00 00 00 03 00 00 00 00")
USED_PROTOTYPE = bytes.fromhex(
"81 00 01 46 50 00 00 00 00 0c 00 0f 00 0c 00 00")
POWER_LEVELS = [
chirp_common.PowerLevel("Hi", watts=50),
chirp_common.PowerLevel("Mid", watts=25),
chirp_common.PowerLevel("Low", watts=5),
]
STEPS = [5.0, 6.25, 8.33, 10.0, 12.5, 15.0, 20.0, 25.0, 50.0, 100.0]
# CONFIG 7 STEP. Nibble at 0x85/0x95 matches STEPS[]. Auto is E3/F3 bit 3
# with nibble 0 (same as 5.0 kHz). Proven: Auto, 5.0, 6.25, 10.0, 100.0.
# 8.33/12.5/15/20/25/50 inferred from STEPS index.
VFO_STEP_LABELS = [
"AUTO",
"5.0 kHz",
"6.25 kHz",
"8.33 kHz",
"10.0 kHz",
"12.5 kHz",
"15.0 kHz",
"20.0 kHz",
"25.0 kHz",
"50.0 kHz",
"100.0 kHz",
]
def vfo_step_to_fields(label):
if label == "AUTO":
return 1, 0
return 0, VFO_STEP_LABELS.index(label) - 1
# Duplex nibble from dumps: 0 simplex, 2 minus, 3 plus, 4 split.
DUPLEX_FROM_RADIO = {0: "", 2: "-", 3: "+", 4: "split"}
DUPLEX_TO_RADIO = {v: k for k, v in DUPLEX_FROM_RADIO.items()}
# High nibble of byte 5. 3 is TSQL-R (ADMS REV TONE). 5/6 are PR/Pager
# in dumps; not in CHIRP valid_tmodes yet.
TMODE_FROM_RADIO = {0: "", 1: "Tone", 2: "TSQL", 3: "TSQL-R", 4: "DTCS"}
TMODE_TO_RADIO = {v: k for k, v in TMODE_FROM_RADIO.items()}
# CONFIG 12 UNIT at 0x00A1 (USA default INCH). CONFIG 3 TIME ZONE at
# 0x00A8: sign-magnitude half-hours (bit7 west, low 7 bits 0-28).
# Proven: 0x00 = UTC +/-0:00, 0x01 = +0:30, 0x81 = -0:30.
UNIT_LABELS = ["METRIC", "INCH"]
TZ_HALF_MIN = -28
TZ_HALF_MAX = 28
def timezone_from_fields(west, mag):
mag = int(mag)
if int(west):
return -mag
return mag
def timezone_to_fields(half_hours):
half = max(TZ_HALF_MIN, min(TZ_HALF_MAX, int(half_hours)))
if half < 0:
return 1, -half
return 0, half
def timezone_label(half_hours):
if half_hours == 0:
return "UTC \u00b10:00"
sign = "+" if half_hours > 0 else "-"
h = abs(half_hours)
return "UTC %s%d:%02d" % (sign, h // 2, (h % 2) * 30)
TIMEZONE_LABELS = [
timezone_label(h) for h in range(TZ_HALF_MIN, TZ_HALF_MAX + 1)]
# CONFIG 2 DATE&TIME FORMAT at 0x00F9: date in bits 2-0, 12-hour in bit 4.
# Proven: MMM/DD/YYYY=2 (0x22), YYYY/MMM/DD=0 (0x20),
# DD/MMM/YYYY=4 (0x24), YYYY/DD/MMM=1 (0x21).
DATE_FORMAT_MAP = (
("YYYY/MMM/DD", 0),
("YYYY/DD/MMM", 1),
("MMM/DD/YYYY", 2),
("DD/MMM/YYYY", 4),
)
TIME_FORMAT_LABELS = ["24 HOUR", "12 HOUR"]
# CONFIG 9 CLOCK TYPE at 0x009A bit 7. Proven A=0, B=1.
CLOCK_TYPE_LABELS = ["A", "B"]
FM_BANDWIDTH_LABELS = ["WIDE", "NARROW"]
# MODE is per band (A=0x0081/0x00E3, B=0x0091/0x00F3). DIGITAL is global.
# FM bandwidth = bit5 of 81/91. RX MODE Auto = bit4 of E3/F3; AM = bit4
# of 81/91; FM = both 0. E3/F3 bit1 is "Auto listed in the radio menu"
# (writes set it).
RX_MODE_LABELS = ["AUTO", "FM", "AM"]
# TX/RX DIGITAL 4 STANDBY BEEP at 0x00E8 bit4 (1=Off).
# DIGITAL VW at 0x00E8 bit1. LOCATION SERVICE at 0x00EB bit7 (1=ON).
STANDBY_BEEP_LABELS = ["ON", "OFF"]
ON_Off_LABELS = ["OFF", "ON"]
# DIGITAL POPUP at 0x02DB. Proven Off=0 .. 60sec=8, Continue=0xFF.
# Default 10sec=5.
DIGITAL_POPUP_MAP = (
("OFF", 0),
("2 SEC", 1),
("4 SEC", 2),
("6 SEC", 3),
("8 SEC", 4),
("10 SEC", 5),
("20 SEC", 6),
("30 SEC", 7),
("60 SEC", 8),
("CONTINUE", 0xFF),
)
# TX/RX AUDIO MIC GAIN at 0x02DD. Proven MIN=0 .. Max=4. Default NORMAL=2.
MIC_GAIN_MAP = (
("MIN", 0),
("LOW", 1),
("NORMAL", 2),
("HIGH", 3),
("MAX", 4),
)
# SUB BAND MUTE at 0x00E9 bits 1-0. Proven ON=0, Off=3 (global).
SUB_BAND_MUTE_MAP = (("ON", 0), ("OFF", 3))
# VOX at 0x00B6. Proven Off=0, LOW=1, HIGH=2.
VOX_LABELS = ["OFF", "LOW", "HIGH"]
# VOX DELAY at 0x00B7. Proven 1.0s=1 .. 3.0s=5. Baseline 0 inferred as 0.5s.
VOX_DELAY_MAP = (
("0.5 SEC", 0),
("1.0 SEC", 1),
("1.5 SEC", 2),
("2.0 SEC", 3),
("2.5 SEC", 4),
("3.0 SEC", 5),
)
# RECORDING BAND at 0x00B8. Proven A=1, B=2, A+B=3.
RECORDING_BAND_MAP = (("A", 1), ("B", 2), ("A+B", 3))
# TX/RX DIGITAL AMS TX MODE at 0x00EE (global).
# Proven: Auto=0, TX FM FIXED=1, TX DN FIXED=2.
AMS_TX_MODE_LABELS = ["AUTO", "TX FM Fixed", "TX DN Fixed"]
# CONFIG 8 BEEP: Off / LOW / HIGH as two flags.
# Proven: enable = 0x00EB bit3, HIGH = 0x00FA bit4. Off leaves volume.
BEEP_LABELS = ["OFF", "LOW", "HIGH"]
def beep_from_fields(beep_on, beep_high):
if not int(beep_on):
return "OFF"
if int(beep_high):
return "HIGH"
return "LOW"
def beep_to_fields(label):
if label == "OFF":
return 0, None
if label == "HIGH":
return 1, 1
return 1, 0
def rx_mode_from_fields(rx_auto, rx_am):
if int(rx_auto):
return "AUTO"
if int(rx_am):
return "AM"
return "FM"
def rx_mode_to_fields(label):
if label == "AUTO":
return 1, 0
if label == "AM":
return 0, 1
return 0, 0
# DISPLAY 1 TARGET LOCATION at 0x00E8 bit 2. Proven Compass=0, Numeric=1.
TARGET_LOCATION_LABELS = ["COMPASS", "NUMERIC"]
# DISPLAY 2 COMPASS at 0x02D6. Proven North Up=0, Heading Up=1.
COMPASS_LABELS = ["NORTH UP", "HEADING UP"]
# DISPLAY 3 BAND SCOPE at 0x00E5 bit 5. Proven Wide=0, Narrow=1.
BAND_SCOPE_LABELS = ["WIDE", "NARROW"]
# GPS DATUM at 0x00AF. Proven WGS-84=0x15, Tokyo Mean=0xB2.
GPS_DATUM_MAP = (("WGS-84", 0x15), ("TOKYO MEAN", 0xB2))
# GPS DEVICE at 0x00EC bit 7. Proven Internal=0, External=1.
GPS_DEVICE_LABELS = ["INTERNAL", "EXTERNAL"]
# CONFIG 17 GPS LOG at 0x00B5.
# Proven: Off=0, 1s=1, 2s=2, 5s=3, 10s=4, 30s=5, 60s=6. Default Off.
GPS_LOG_MAP = (
("OFF", 0),
("1 SEC", 1),
("2 SEC", 2),
("5 SEC", 3),
("10 SEC", 4),
("30 SEC", 5),
("60 SEC", 6),
)
# CONFIG 11 RX COVERAGE at 0x00E3/0x00F3 bit 5 (per band).
# Proven: NORMAL=0, Wide=1 (factory).
RX_COVERAGE_LABELS = ["NORMAL", "WIDE"]
# CONFIG 14 TOT at 0x00AA. Factory 5 min.
# Proven: Off=0, 5min=1, 10min=2, 15min=3, 20min=4, 30min=5,
# 1min=6, 2min=7, 3min=8.
TOT_MAP = (
("OFF", 0),
("1 MIN", 6),
("2 MIN", 7),
("3 MIN", 8),
("5 MIN", 1),
("10 MIN", 2),
("15 MIN", 3),
("20 MIN", 4),
("30 MIN", 5),
)
# CONFIG 13 APO at 0x00A4, 0.5 hour units. Factory OFF=0.
# Proven: OFF=0, 0.5h=1, 1.0h=2, 1.5h=3, 2.0h=4, 3.0h=6, 4.0h=8,
# 12.0h=24. Other 0.5h steps inferred as n = hours*2.
APO_MAP = (("OFF", 0),) + tuple(
("%.1f HOUR" % (n / 2.0), n) for n in range(1, 25))
# CONFIG RPT ARS at 0x00E4/0x00F4 bit 2 (per band). Proven ON=1, Off=0.
# VFO RPT SHIFT is the duplex nibble of 0x81/0x91 (0=Off, 2=-, 3=+).
# VFO SHIFT FREQ at 0x8C/0x9C, u16 BE in 50 kHz units (manual 0.00-99.95).
# Proven: 0.75=0x000F, 1.45=0x001D, 3.10=0x003E, 5.00=0x0064,
# 20.00=0x0190. Factory A=0.60 (0x000C), B=5.00 (0x0064).
RPT_SHIFT_MAP = (("OFF", 0), ("-", 2), ("+", 3))
RPT_SHIFT_FREQ_STEP = 0.05
RPT_SHIFT_FREQ_MAX = 99.95
RPT_SHIFT_FREQ_UNITS_MAX = 1999 # 99.95 / 0.05
def _rpt_shift_freq_to_radio(value):
units = int(round(float(value) / RPT_SHIFT_FREQ_STEP))
if units < 0:
units = 0
if units > RPT_SHIFT_FREQ_UNITS_MAX:
units = RPT_SHIFT_FREQ_UNITS_MAX
return units
# DISPLAY 6 DISPLAY MODE: 0x00E8 bit 7 is the info screen (0=default)
# dual-band display). 0x00AD selects Backtrack=0, Altitude=1,
# Timer/Clock=2, GPS Information=3 when the info screen is on.
DISPLAY_MODE_LABELS = [
"DEFAULT", "BACKTRACK", "ALTITUDE", "TIMER/CLOCK", "GPS INFORMATION"]
def display_mode_to_fields(label):
if label == "DEFAULT":
return 0, None
return 1, DISPLAY_MODE_LABELS.index(label) - 1
# DISPLAY 4 LCD BRIGHTNESS at 0x028F. Proven MIN=3, MID=5, MAX=6.
BRIGHTNESS_MAP = (("MIN", 3), ("MID", 5), ("MAX", 6))
# F(SETUP) CALLSIGN: 10 chars at 0x02C8, 0xFF pad.
# APRS 21 CALLSIGN: 6 chars at 0x0508, 0xCA pad (empty is all 0xCA).
# SSID at 0x050E. Proven AAAAAA-13 -> 0x0D; empty pad 0xCA (not SSID 0).
# APRS 4 MODEM at 0x0534. Proven Off=0, ON=1.
# Radio CALLSIGN allows A-Z, 0-9, hyphen, and slash.
CALLSIGN_CHARSET = "ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-/"
APRS_CALL_CHARSET = "ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"
CALLSIGN_PAD = 0xFF
APRS_CALL_PAD = 0xCA
APRS_SSID_MAX = 15
# Empty SSID is pad 0xCA, distinct from SSID 0.
APRS_SSID_MAP = (("", APRS_CALL_PAD),) + tuple(
(str(i), i) for i in range(APRS_SSID_MAX + 1))
APRS_MODEM_LABELS = ["OFF", "ON"]
def _decode_padded(raw, pad):
buf = bytes(int(x) for x in raw)
return buf.split(bytes([pad]))[0].split(b"\x00")[0].decode(
"ascii", "replace").rstrip()
def _encode_padded(text, length, pad, charset):
cleaned = "".join(c if c in charset else "" for c in text.upper())
cleaned = cleaned[:length]
data = cleaned.encode("ascii") + bytes([pad]) * (length - len(cleaned))
return data
# Fine-step flags in the high nibble of the first freq byte.
# Proven: 0x80 = +5 kHz, 0x20 = +1.25 kHz, together +6.25 kHz (0xA0).
_FREQ_FINE = (
("fine_5k", 5000),
("fine_2k5", 2500),
("fine_1k25", 1250),
("fine_625", 625),
)
def decode_freq(freq):
"""Decode a yfreq struct to Hz."""
digits = "%X%02X%02X" % (
int(freq.hun), int(freq.bcd[0]), int(freq.bcd[1]))
hz = int(digits, 10) * 10000
for name, add in _FREQ_FINE:
if int(getattr(freq, name)):
hz += add
return hz
def encode_freq(freq, hz):
"""Encode Hz into a yfreq struct."""
rem = hz % 10000
base = hz - rem
for name, add in _FREQ_FINE:
if rem >= add:
setattr(freq, name, 1)
rem -= add
else:
setattr(freq, name, 0)
khz10 = base // 10000
s = "%06d" % khz10
freq.hun = int(s[1])
freq.bcd[0] = (int(s[2]) << 4) | int(s[3])
freq.bcd[1] = (int(s[4]) << 4) | int(s[5])
def parse_freq_bytes(raw):
"""Parse a 3-byte freq encoding (for tests)."""
mmap = memmap.MemoryMapBytes(bytes(raw))
freq = bitwise.parse(
YFREQ_FORMAT + "struct yfreq freq;", mmap).freq
return mmap, freq
def _set_bytes(element, data):
for i, value in enumerate(data):
element[i] = value
@directory.register
class FTM300Radio(yaesu_clone.YaesuCloneModeRadio,
chirp_common.ExperimentalRadio):
"""Yaesu FTM-300DR (USA)."""
VENDOR = "Yaesu"
MODEL = "FTM-300DR"
BAUD_RATE = 38400
NEEDS_COMPAT_SERIAL = False
FORMATS = [directory.register_format('Yaesu FTM-300DR SD ALL', '*.dat')]
_model = b"AH071"
_memsize = 98304
_tx_bands = [
(144000000, 148000000),
(430000000, 450000000),
]
@classmethod
def get_prompts(cls):
rp = chirp_common.RadioPrompts()
# Keep these untranslated so we do not churn locale .po files.
rp.experimental = (
"The FTM-300DR driver is experimental. Memories, serial "
"clone, and some Config/Display/callsign settings work; "
"Home/PMS, most Set Mode items, and most APRS are not "
"mapped. "
"Keep a microSD ALL backup (SD CARD -> BACKUP -> WRITE TO "
"SD -> ALL) before uploading. The stock USB cable in the box "
"is not a programming cable.")
rp.info = (
"Use an SCU-56 or SCU-20 on the DATA jack at 38400 8N1. Keep "
"a microSD ALL backup. If clone shows ERROR, restore with "
"SD CARD -> READ FROM SD -> ALL.")
rp.pre_download = (
"1. Connect an SCU-56 or SCU-20 to the DATA jack "
"(not the stock USB firmware cable).\n"
"2. Press and hold [F(SETUP)], select CLONE, then "
"[1 This → Other].\n"
"3. After clicking OK , on the radio select OK and "
"press the DIAL knob.\n")
rp.pre_upload = (
"Keep a microSD ALL backup first.\n"
"1. Connect an SCU-56 or SCU-20 to the DATA jack "
"(not the stock USB firmware cable).\n"
"2. Press and hold [F(SETUP)], select CLONE, then "
"[2 Other → This].\n"
"3. After clicking OK , on the radio select OK and "
"press the DIAL knob.\n")
return rp
def _append_mode(self, parent, suffix, title):
group = RadioSettingGroup("band_%s" % suffix, title)
parent.append(group)
group.append(MemSetting(
"fm_bandwidth_%s" % suffix, "FM Bandwidth",
RadioSettingValueList(
FM_BANDWIDTH_LABELS,
current_index=int(
getattr(self._memobj, "fm_bandwidth_%s" % suffix)))))
rx = rx_mode_from_fields(
getattr(self._memobj, "rx_auto_%s" % suffix),
getattr(self._memobj, "rx_am_%s" % suffix))
group.append(RadioSetting(
"rx_mode_%s" % suffix, "RX Mode",
RadioSettingValueList(
RX_MODE_LABELS, current_index=RX_MODE_LABELS.index(rx))))
def _append_repeater_settings(self, parent, suffix, title):
group = RadioSettingGroup("repeater_%s" % suffix, title)
parent.append(group)
group.append(MemSetting(
"rpt_ars_%s" % suffix, "ARS",
RadioSettingValueList(
ON_Off_LABELS,
current_index=int(
getattr(self._memobj, "rpt_ars_%s" % suffix)))))
group.append(MemSetting(
"rpt_shift_%s" % suffix, "Shift",
RadioSettingValueMap(
RPT_SHIFT_MAP,
mem_val=int(
getattr(self._memobj, "rpt_shift_%s" % suffix)))))
units = int(getattr(self._memobj, "rpt_shift_freq_%s" % suffix))
mhz = round(units * RPT_SHIFT_FREQ_STEP, 2)
group.append(RadioSetting(
"rpt_shift_freq_%s" % suffix, "Shift Freq",
RadioSettingValueFloat(
0.0, RPT_SHIFT_FREQ_MAX, mhz, RPT_SHIFT_FREQ_STEP, 2)))
def _append_step_settings(self, group, suffix, title):
if int(getattr(self._memobj, "step_auto_%s" % suffix)):
idx = 0
else:
idx = int(getattr(self._memobj, "vfo_step_%s" % suffix)) + 1
rs = RadioSetting(
"vfo_step_%s" % suffix, title,
RadioSettingValueList(VFO_STEP_LABELS, current_index=idx))
rs.set_doc(
"VFO mode only. In Memory mode the radio uses each "
"channel's Tuning Step.")
group.append(rs)
def _append_rx_coverage_settings(self, group, suffix, title):
group.append(MemSetting(
"rx_coverage_%s" % suffix, title,
RadioSettingValueList(
RX_COVERAGE_LABELS,
current_index=int(
getattr(self._memobj, "rx_coverage_%s" % suffix)))))
def get_features(self):
rf = chirp_common.RadioFeatures()
rf.has_bank = False
rf.has_bank_names = False
rf.has_settings = True
rf.has_ctone = False
rf.has_dtcs = True
rf.has_dtcs_polarity = False
rf.has_cross = False
rf.has_rx_dtcs = False
rf.has_comment = False
rf.has_nostep_tuning = False
rf.can_odd_split = True
rf.can_delete = True
rf.memory_bounds = (1, 999)
rf.valid_modes = ["FM", "NFM", "AM", "DN"]
rf.valid_tmodes = ["", "Tone", "TSQL", "TSQL-R", "DTCS"]
rf.valid_duplexes = ["", "-", "+", "split"]
rf.valid_tuning_steps = list(STEPS)
rf.valid_skips = ["", "S"]
rf.valid_power_levels = POWER_LEVELS
rf.valid_name_length = 16
rf.valid_characters = chirp_common.CHARSET_ASCII
rf.valid_dtcs_codes = list(chirp_common.DTCS_CODES)
rf.valid_bands = [
(108000000, 174000000),
(174000000, 400000000),
(400000000, 480000000),
(480000000, 824000000),
(849000000, 869000000),
(894000000, 999990000),
]
return rf
def process_mmap(self):
self._memobj = bitwise.parse(MEM_FORMAT, self._mmap)
def get_settings(self):
top = RadioSettings()
config = RadioSettingGroup("config", "Config")
top.append(config)
half = timezone_from_fields(
self._memobj.tz_west, self._memobj.tz_mag)
tz_idx = half - TZ_HALF_MIN
if tz_idx < 0 or tz_idx >= len(TIMEZONE_LABELS):
tz_idx = len(TIMEZONE_LABELS)
# Two bitfields; applied as leftover in set_settings().
config.append(RadioSetting(
"timezone", "Time Zone",
RadioSettingValueList(
TIMEZONE_LABELS, current_index=tz_idx)))
date_time_format = RadioSettingGroup("date_time_format",
"Date & Time Format")
config.append(date_time_format)
date_time_format.append(MemSetting(
"date_fmt", "Date Format",
RadioSettingValueMap(
DATE_FORMAT_MAP, mem_val=int(self._memobj.date_fmt))))
date_time_format.append(MemSetting(
"time_12hr", "Time Format",
RadioSettingValueList(
TIME_FORMAT_LABELS,
current_index=int(self._memobj.time_12hr))))
repeater = RadioSettingGroup("repeater", "Repeater")
config.append(repeater)
self._append_repeater_settings(repeater, "a", "A Band")
self._append_repeater_settings(repeater, "b", "B Band")
vfo_step = RadioSettingGroup("vfo_step", "Step (VFO)")
vfo_step.set_doc(
"Tuning step in VFO mode. Auto follows the band plan. "
"Memory mode uses each channel's Tuning Step in the "
"memory editor.")
config.append(vfo_step)
self._append_step_settings(vfo_step, "a", "A Band")
self._append_step_settings(vfo_step, "b", "B Band")
rx_coverage = RadioSettingGroup("rx_coverage", "RX Coverage")
config.append(rx_coverage)
self._append_rx_coverage_settings(rx_coverage, "a", "A Band")
self._append_rx_coverage_settings(rx_coverage, "b", "B Band")
beep = beep_from_fields(
self._memobj.beep_on, self._memobj.beep_high)
config.append(RadioSetting(
"beep", "Beep",
RadioSettingValueList(
BEEP_LABELS, current_index=BEEP_LABELS.index(beep))))
config.append(MemSetting(
"clock_type_b", "Clock Type",
RadioSettingValueList(
CLOCK_TYPE_LABELS,
current_index=int(self._memobj.clock_type_b))))
config.append(MemSetting(
"unit", "Display Units",
RadioSettingValueList(
UNIT_LABELS, current_index=int(self._memobj.unit))))
config.append(MemSetting(
"apo", "APO",
RadioSettingValueMap(APO_MAP, mem_val=int(self._memobj.apo))))
config.append(MemSetting(
"tot", "TOT",
RadioSettingValueMap(TOT_MAP, mem_val=int(self._memobj.tot))))
config.append(MemSetting(
"gps_datum", "GPS Datum",
RadioSettingValueMap(
GPS_DATUM_MAP, mem_val=int(self._memobj.gps_datum))))
config.append(MemSetting(
"gps_device", "GPS Device",
RadioSettingValueList(
GPS_DEVICE_LABELS,
current_index=int(self._memobj.gps_device))))
config.append(MemSetting(
"gps_log", "GPS Log",
RadioSettingValueMap(
GPS_LOG_MAP, mem_val=int(self._memobj.gps_log))))
txrx = RadioSettingGroup("txrx", "TX/RX")
top.append(txrx)
mode = RadioSettingGroup("mode", "Mode")
self._append_mode(mode, "a", "A Band")
self._append_mode(mode, "b", "B Band")
txrx.append(mode)
digital = RadioSettingGroup("digital", "Digital")
txrx.append(digital)
digital.append(MemSetting(
"ams_tx_mode", "AMS TX Mode",
RadioSettingValueList(
AMS_TX_MODE_LABELS,
current_index=int(self._memobj.ams_tx_mode))))
digital.append(MemSetting(
"digital_popup", "Digital Popup",
RadioSettingValueMap(
DIGITAL_POPUP_MAP,
mem_val=int(self._memobj.digital_popup))))
digital.append(MemSetting(
"location_service", "Location Service",
RadioSettingValueList(
ON_Off_LABELS,
current_index=int(self._memobj.location_service))))
digital.append(MemSetting(
"standby_beep_off", "Standby Beep",
RadioSettingValueList(
STANDBY_BEEP_LABELS,
current_index=int(self._memobj.standby_beep_off))))
digital.append(MemSetting(
"digital_vw", "Digital VW",
RadioSettingValueList(
ON_Off_LABELS,
current_index=int(self._memobj.digital_vw))))
audio = RadioSettingGroup("audio", "Audio")
txrx.append(audio)
audio.append(MemSetting(
"sub_band_mute", "Sub Band Mute",
RadioSettingValueMap(
SUB_BAND_MUTE_MAP,
mem_val=int(self._memobj.sub_band_mute))))
audio.append(MemSetting(
"mic_gain", "Mic Gain",
RadioSettingValueMap(
MIC_GAIN_MAP, mem_val=int(self._memobj.mic_gain))))
vox = RadioSettingGroup("vox", "VOX")
audio.append(vox)
vox.append(MemSetting(
"vox", "VOX",
RadioSettingValueList(
VOX_LABELS, current_index=int(self._memobj.vox))))
vox.append(MemSetting(
"vox_delay", "VOX Delay",
RadioSettingValueMap(
VOX_DELAY_MAP, mem_val=int(self._memobj.vox_delay))))
recording = RadioSettingGroup("recording", "Recording")
audio.append(recording)
recording.append(MemSetting(
"recording_band", "Recording Band",
RadioSettingValueMap(
RECORDING_BAND_MAP,
mem_val=int(self._memobj.recording_band))))
recording.append(MemSetting(
"recording_mic", "Recording Mic",
RadioSettingValueList(
ON_Off_LABELS,
current_index=int(self._memobj.recording_mic))))
display = RadioSettingGroup("display", "Display")
top.append(display)
display.append(MemSetting(
"target_location", "Target Location",
RadioSettingValueList(
TARGET_LOCATION_LABELS,
current_index=int(self._memobj.target_location))))
display.append(MemSetting(
"compass", "COMPASS",
RadioSettingValueList(
COMPASS_LABELS, current_index=int(self._memobj.compass))))
display.append(MemSetting(
"band_scope", "Band Scope",
RadioSettingValueList(
BAND_SCOPE_LABELS,
current_index=int(self._memobj.band_scope))))
display.append(MemSetting(
"lcd_brightness", "LCD Brightness",
RadioSettingValueMap(
BRIGHTNESS_MAP,
mem_val=int(self._memobj.lcd_brightness))))
if int(self._memobj.display_info):
dmode_idx = int(self._memobj.display_mode) + 1
else:
dmode_idx = 0
display.append(RadioSetting(
"display_mode", "Display Mode",
RadioSettingValueList(
DISPLAY_MODE_LABELS, current_index=dmode_idx)))
ident = RadioSettingGroup("callsign", "Callsign")
top.append(ident)
ident.append(RadioSetting(
"callsign", "Radio Callsign",
RadioSettingValueString(
0, 10, self._decode_callsign(),
autopad=False, charset=CALLSIGN_CHARSET)))
aprs = RadioSettingGroup("aprs", "APRS")
top.append(aprs)
aprs.append(RadioSetting(
"aprs_call", "APRS Callsign",
RadioSettingValueString(
0, 6, self._decode_aprs_call(),
autopad=False, charset=APRS_CALL_CHARSET)))
aprs.append(MemSetting(
"aprs_ssid", "APRS SSID",
RadioSettingValueMap(
APRS_SSID_MAP, mem_val=int(self._memobj.aprs_ssid))))
aprs.append(MemSetting(
"aprs_modem", "APRS Modem",
RadioSettingValueList(
APRS_MODEM_LABELS,
current_index=int(self._memobj.aprs_modem))))
return top
def set_settings(self, settings):
leftover = settings.apply_to(self._memobj)
for element in leftover:
name = element.get_name()
value = str(element.value)
if name == "timezone":
half = TIMEZONE_LABELS.index(value) + TZ_HALF_MIN
west, mag = timezone_to_fields(half)
self._memobj.tz_west = west
self._memobj.tz_mag = mag
elif name == "beep":
on, high = beep_to_fields(value)
self._memobj.beep_on = on
if high is not None:
self._memobj.beep_high = high
elif name == "display_mode":
info, mode = display_mode_to_fields(value)
self._memobj.display_info = info
if mode is not None:
self._memobj.display_mode = mode
elif name == "rpt_shift_freq_a":
self._memobj.rpt_shift_freq_a = _rpt_shift_freq_to_radio(
value)
elif name == "rpt_shift_freq_b":
self._memobj.rpt_shift_freq_b = _rpt_shift_freq_to_radio(
value)
elif name == "vfo_step_a":
auto, nibble = vfo_step_to_fields(value)
self._memobj.step_auto_a = auto
self._memobj.vfo_step_a = nibble
elif name == "vfo_step_b":
auto, nibble = vfo_step_to_fields(value)
self._memobj.step_auto_b = auto
self._memobj.vfo_step_b = nibble
elif name == "rx_mode_a":
auto, am = rx_mode_to_fields(value)
self._memobj.rx_auto_a = auto
self._memobj.rx_am_a = am
self._memobj.rx_auto_menu_a = 1
elif name == "rx_mode_b":
auto, am = rx_mode_to_fields(value)
self._memobj.rx_auto_b = auto
self._memobj.rx_am_b = am
self._memobj.rx_auto_menu_b = 1
elif name == "callsign":
self._encode_callsign(value)
elif name == "aprs_call":
self._encode_aprs_call(value)
def _decode_callsign(self):
return _decode_padded(self._memobj.callsign, CALLSIGN_PAD)
def _encode_callsign(self, text):
data = _encode_padded(text, 10, CALLSIGN_PAD, CALLSIGN_CHARSET)
_set_bytes(self._memobj.callsign, data)
def _decode_aprs_call(self):
return _decode_padded(self._memobj.aprs_call, APRS_CALL_PAD)
def _encode_aprs_call(self, text):
data = _encode_padded(text, 6, APRS_CALL_PAD, APRS_CALL_CHARSET)
_set_bytes(self._memobj.aprs_call, data)
def sync_in(self):
try:
self._mmap = _clone_in(self)
except errors.RadioError:
raise
except Exception as e:
LOG.exception("Failed to read: %s", e)
raise errors.RadioError(
"Failed to download from radio (%s)" % e)
self.process_mmap()
def sync_out(self):
try:
_clone_out(self)
except errors.RadioError:
raise
except Exception as e:
LOG.exception("Failed to write: %s", e)
raise errors.RadioError(
"Failed to upload to radio (%s)" % e)
def get_raw_memory(self, number):
return repr(self._memobj.memory[number - 1])
def validate_memory(self, mem):
msgs = super().validate_memory(mem)
if mem.empty:
return msgs
if mem.duplex == "split":
tx = mem.offset
elif mem.duplex == "-":
tx = mem.freq - mem.offset
elif mem.duplex == "+":
tx = mem.freq + mem.offset
else:
tx = mem.freq
in_tx = any(lo <= tx < hi for lo, hi in self._tx_bands)
if not in_tx:
if mem.duplex in ("-", "+", "split"):
msgs.append(chirp_common.ValidationError(
"TX frequency is outside amateur allocations "
"for this model"))
else:
msgs.append(chirp_common.ValidationWarning(
"RX-only frequency; radio will not transmit here"))
return msgs
def _decode_name(self, number):
raw = bytes(int(x) for x in self._memobj.names[number - 1].text)
return raw.split(b"\xff")[0].split(b"\x00")[0].decode(
"ascii", "replace").rstrip()
def _encode_name(self, name):
cleaned = "".join(
c if c in chirp_common.CHARSET_ASCII else " "
for c in name)[:16]
padded = cleaned.encode("ascii", "replace")
return padded + b"\xff" * (16 - len(padded))
def _get_extra(self, mem, _mem):
extra = RadioSettingGroup("Extra", "extra")
extra.append(MemSetting(
"ams", "AMS mode",
RadioSettingValueBoolean(bool(int(_mem.ams)))))
extra.append(MemSetting(
"dgid_rx", "RX DG-ID",
RadioSettingValueInteger(0, 99, int(_mem.dgid_rx))))
extra.append(MemSetting(
"dgid_tx", "TX DG-ID",
RadioSettingValueInteger(0, 99, int(_mem.dgid_tx))))
extra.append(MemSetting(
"clock_shift", "Clock Shift",
RadioSettingValueBoolean(bool(int(_mem.clock_shift)))))
mem.extra = extra
def _apply_extra(self, mem, _mem):
for setting in mem.extra:
setting.apply_to_memobj(_mem)
# AMS extra maps to _mem.ams; DN-only is the inverse bit.
# Analog modes must not leave either digital flag set.
if mem.mode == "DN":
_mem.dn = 0 if int(_mem.ams) else 1
else:
_mem.ams = 0
_mem.dn = 0
def get_memory(self, number):
mem = chirp_common.Memory()
mem.number = number
_mem = self._memobj.memory[number - 1]
if not int(_mem.used):
mem.empty = True
return mem
mem.empty = False
mem.freq = decode_freq(_mem.freq)
mem.duplex = DUPLEX_FROM_RADIO.get(int(_mem.duplex), "")
if mem.duplex == "split":
mem.offset = decode_freq(_mem.tx_freq)
else:
mem.offset = int(_mem.offset) * 50000
mem.name = self._decode_name(number)
mem.skip = "S" if int(_mem.skip) else ""
try:
mem.power = POWER_LEVELS[int(_mem.power)]
except IndexError:
mem.power = POWER_LEVELS[0]
try:
mem.tuning_step = STEPS[int(_mem.tune_step)]
except IndexError:
mem.tuning_step = STEPS[0]
mem.tmode = TMODE_FROM_RADIO.get(int(_mem.tone_mode), "")
try:
mem.rtone = chirp_common.TONES[int(_mem.tone)]
except IndexError:
mem.rtone = 88.5
mem.ctone = mem.rtone
try:
mem.dtcs = chirp_common.DTCS_CODES[int(_mem.dcs)]
except IndexError:
mem.dtcs = 23
if int(_mem.am):
mem.mode = "AM"
elif int(_mem.narrow):
mem.mode = "NFM"
elif int(_mem.ams) or int(_mem.dn):
mem.mode = "DN"
else:
mem.mode = "FM"
self._get_extra(mem, _mem)
return mem
def set_memory(self, mem):
_mem = self._memobj.memory[mem.number - 1]
name_idx = mem.number - 1
if mem.empty:
_mem.set_raw(EMPTY_SLOT)
_set_bytes(self._memobj.names[name_idx].text, b"\xff" * 16)
self._memobj.nameflags[name_idx] = 0xFFFF
return
if not int(_mem.used):
_mem.set_raw(USED_PROTOTYPE)
_mem.used = 1
_mem.unknown2 = 1
_mem.occupied = 0xF
_mem.uhf = 1 if mem.freq >= 400000000 else 0
_mem.skip = 1 if mem.skip == "S" else 0
encode_freq(_mem.freq, mem.freq)
_mem.duplex = DUPLEX_TO_RADIO.get(mem.duplex, 0)
if mem.duplex == "split":
encode_freq(_mem.tx_freq, int(mem.offset))
_mem.offset = 0
else:
encode_freq(_mem.tx_freq, 0)
_mem.offset = int(round(mem.offset / 50000.0)) if mem.offset else 0
_mem.am = 1 if mem.mode == "AM" else 0
_mem.narrow = 1 if mem.mode == "NFM" else 0
if mem.mode != "DN":
_mem.ams = 0
_mem.dn = 0
try:
_mem.tune_step = STEPS.index(mem.tuning_step)
except ValueError:
_mem.tune_step = 0
_mem.tone_mode = TMODE_TO_RADIO.get(mem.tmode, 0)
try:
_mem.tone = chirp_common.TONES.index(mem.rtone)
except ValueError:
_mem.tone = chirp_common.TONES.index(88.5)
try:
_mem.dcs = chirp_common.DTCS_CODES.index(mem.dtcs)
except ValueError:
_mem.dcs = 0
if mem.power in POWER_LEVELS:
_mem.power = POWER_LEVELS.index(mem.power)
else:
_mem.power = 0
encoded = self._encode_name(mem.name)
_set_bytes(self._memobj.names[name_idx].text, encoded)
self._memobj.nameflags[name_idx] = (
0x0000 if mem.name.strip() else 0xFFFF)
self._apply_extra(mem, _mem)
if mem.mode == "DN" and not int(_mem.ams) and not int(_mem.dn):
_mem.ams = 1
_mem.dn = 0
chirp-20261030/chirp/drivers/ftm3200d.py 0000644 0001750 0001750 00000015056 14670320477 016447 0 ustar hibby hibby # Copyright 2010 Dan Smith
# Copyright 2017 Wade Simmons
#
# 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.drivers import yaesu_clone, ft1d
from chirp import chirp_common, directory, bitwise
from chirp.settings import RadioSettings
LOG = logging.getLogger(__name__)
POWER_LEVELS = [chirp_common.PowerLevel("Low", watts=5),
chirp_common.PowerLevel("Mid", watts=30),
chirp_common.PowerLevel("Hi", watts=65)]
TMODES = ["", "Tone", "TSQL", "DTCS", "TSQL-R", None, None, "Pager", "Cross"]
CROSS_MODES = [None, "DTCS->", "Tone->DTCS", "DTCS->Tone"]
MODES = ["FM", "NFM"]
STEPS = [0, 5, 6.25, 10, 12.5, 15, 20, 25, 50, 100] # 0 = auto
RFSQUELCH = ["OFF", "S1", "S2", "S3", "S4", "S5", "S6", "S7", "S8"]
# Charset is subset of ASCII + some unknown chars \x80-\x86
VALID_CHARS = ["%i" % int(x) for x in range(0, 10)] + \
list(":>=@") + \
[chr(x) for x in range(ord("A"), ord("Z") + 1)] + \
list("[\\]_") + \
[chr(x) for x in range(ord("a"), ord("z") + 1)] + \
list("%*+,-/=$ ")
MEM_FORMAT = """
#seekto 0xceca;
struct {
u8 unknown5;
u8 unknown3;
u8 unknown4:6,
dsqtype:2;
u8 dsqcode;
u8 unknown1[2];
char mycall[10];
u8 unknown2[368];
} settings;
#seekto 0xfec9;
u8 checksum;
"""
@directory.register
class FTM3200Radio(ft1d.FT1Radio):
"""Yaesu FTM-3200D"""
BAUD_RATE = 38400
VENDOR = "Yaesu"
MODEL = "FTM-3200D"
VARIANT = "R"
_model = b"AH52N"
_memsize = 65227
_block_lengths = [10, 65217]
_has_vibrate = False
_has_af_dual = False
_mem_params = {
"memnum": 199, # size of memories array
"flgnum": 199} # size of flags array
@classmethod
def get_prompts(cls):
rp = chirp_common.RadioPrompts()
rp.pre_download = _(
"1. Turn radio off.\n"
"2. Connect cable to DATA terminal.\n"
"3. Press and hold in the [MHz(SETUP)] key while turning the"
" radio\n"
" on (\"CLONE\" will appear on the display).\n"
"4. After clicking OK , press the [REV(DW)] key\n"
" to send image.\n")
rp.pre_upload = _(
"1. Turn radio off.\n"
"2. Connect cable to DATA terminal.\n"
"3. Press and hold in the [MHz(SETUP)] key while turning the"
" radio\n"
" on (\"CLONE\" will appear on the display).\n"
"4. Press the [MHz(SETUP)] key\n"
" (\"-WAIT-\" will appear on the LCD).\n")
return rp
def process_mmap(self):
mem_format = ft1d.MEM_FORMAT + MEM_FORMAT
self._memobj = bitwise.parse(mem_format % self._mem_params, self._mmap)
def get_features(self):
rf = chirp_common.RadioFeatures()
rf.has_dtcs_polarity = False
rf.valid_modes = list(MODES)
rf.valid_tmodes = [x for x in TMODES if x is not None]
rf.valid_cross_modes = [x for x in CROSS_MODES if x is not None]
rf.valid_duplexes = list(ft1d.DUPLEX)
rf.valid_tuning_steps = [x for x in STEPS if x]
rf.valid_bands = [(136000000, 174000000)]
# rf.valid_skips = SKIPS
rf.valid_power_levels = POWER_LEVELS
rf.valid_characters = "".join(VALID_CHARS)
rf.valid_name_length = 8
rf.memory_bounds = (1, 199)
rf.can_odd_split = True
rf.has_ctone = False
rf.has_bank = False
rf.has_bank_names = False
# disable until implemented
rf.has_settings = False
return rf
def _decode_label(self, mem):
# TODO preserve the unknown \x80-x86 chars?
return str(mem.label).rstrip("\xFF").rstrip()
def _encode_label(self, mem):
label = mem.name.rstrip().encode('ascii', 'ignore')
return self._add_ff_pad(label, 16)
def _encode_charsetbits(self, mem):
# TODO this is a setting to decide if the memory should be displayed
# as a name or frequency. Should we expose this setting to the user
# instead of autoselecting it (and losing their preference)?
if mem.name.rstrip() == '':
return 0x0000
return 0x0080
def _decode_power_level(self, mem):
return POWER_LEVELS[mem.power - 1]
def _encode_power_level(self, mem):
if mem.power is None:
return 1
else:
return POWER_LEVELS.index(mem.power) + 1
def _decode_mode(self, mem):
return MODES[mem.mode_alt]
def _encode_mode(self, mem):
return MODES.index(mem.mode)
def _get_tmode(self, mem, _mem):
if _mem.tone_mode > 8:
tmode = "Cross"
mem.cross_mode = CROSS_MODES[_mem.tone_mode - 8]
else:
tmode = TMODES[_mem.tone_mode]
if tmode == "Pager":
# TODO chirp_common does not allow 'Pager'
# Expose as a different setting?
mem.tmode = ""
else:
mem.tmode = tmode
def _set_tmode(self, _mem, mem):
if mem.tmode == "Cross":
_mem.tone_mode = 8 + CROSS_MODES.index(mem.cross_mode)
else:
_mem.tone_mode = TMODES.index(mem.tmode)
def _set_mode(self, _mem, mem):
_mem.mode_alt = self._encode_mode(mem)
def get_bank_model(self):
return None
def _debank(self, mem):
return
def _checksums(self):
return [yaesu_clone.YaesuChecksum(0x064A, 0x06C8),
yaesu_clone.YaesuChecksum(0x06CA, 0x0748),
yaesu_clone.YaesuChecksum(0x074A, 0x07C8),
yaesu_clone.YaesuChecksum(0x07CA, 0x0848),
yaesu_clone.YaesuChecksum(0x0000, 0xFEC9)]
def _get_settings(self):
# TODO
top = RadioSettings()
return top
@classmethod
def _wipe_memory(cls, mem):
mem.set_raw("\x00" * (mem.size() // 8))
def sync_out(self):
# Need to give enough time for the radio to ACK after writes
self.pipe.timeout = 1
return super(FTM3200Radio, self).sync_out()
chirp-20261030/chirp/drivers/ftm350.py 0000644 0001750 0001750 00000031363 15172631605 016221 0 ustar hibby hibby # 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 time
import struct
import logging
from chirp.drivers import yaesu_clone
from chirp import chirp_common, directory, errors, bitwise, memmap
from chirp.settings import RadioSettingGroup, RadioSetting, RadioSettings
from chirp.settings import RadioSettingValueInteger, RadioSettingValueString
LOG = logging.getLogger(__name__)
mem_format = """
struct mem {
u8 used:1,
skip:2,
unknown1:4,
visible:1;
u8 unknown2:1,
mode:3,
unknown8:1,
oddsplit:1,
duplex:2;
bbcd freq[3];
u8 unknownA:1,
tmode:3,
unknownB:4;
bbcd split[3];
u8 power:2,
tone:6;
u8 unknownC:1,
dtcs:7;
u8 showalpha:1,
unknown5:7;
u8 unknown6;
u8 offset;
u8 unknown7[2];
};
struct lab {
u8 string[8];
};
#seekto 0x0508;
struct {
char call[6];
u8 ssid;
} aprs_my_callsign;
#seekto 0x0480;
struct mem left_memory_zero;
#seekto 0x04A0;
struct lab left_label_zero;
#seekto 0x04C0;
struct mem right_memory_zero;
#seekto 0x04E0;
struct lab right_label_zero;
#seekto 0x0800;
struct mem left_memory[500];
#seekto 0x2860;
struct mem right_memory[500];
#seekto 0x48C0;
struct lab left_label[518];
struct lab right_label[518];
"""
_TMODES = ["", "Tone", "TSQL", "-RVT", "DTCS", "-PR", "-PAG"]
TMODES = ["", "Tone", "TSQL", "", "DTCS", "", ""]
MODES = ["FM", "AM", "NFM", "", "WFM"]
DUPLEXES = ["", "", "-", "+", "split", "off"]
# TODO: add Japanese characters (viewable in special menu, scroll backwards)
CHARSET = \
('0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz!"' +
'#$%&`()*+,-./:;<=>?@[\\]^_`{|}~?????? ' + '?' * 91)
POWER_LEVELS = [chirp_common.PowerLevel("Hi", watts=50),
chirp_common.PowerLevel("Mid", watts=20),
chirp_common.PowerLevel("Low", watts=5)]
SKIPS = ["", "S", "P"]
def aprs_call_to_str(_call):
call = ""
for i in str(_call):
if i == "\xca":
break
call += i
return call
def _safe_read(radio, length):
data = ""
while len(data) < length:
data += radio.pipe.read(length - len(data))
return data
def _clone_in(radio):
data = ""
radio.pipe.timeout = 1
attempts = 30
data = memmap.MemoryMapBytes(b"\x00" * (radio._memsize + 128))
length = 0
last_addr = 0
while length < radio._memsize:
frame = radio.pipe.read(131)
if length and not frame:
raise errors.RadioError("Timed out reading from radio")
if not frame:
attempts -= 1
if attempts <= 0:
if length:
raise errors.RadioError("Timed out reading from radio")
raise errors.RadioNoResponse()
if frame:
addr, = struct.unpack(">H", frame[0:2])
checksum = frame[130]
block = frame[2:130]
cs = 0
for i in frame[:-1]:
cs = (cs + i) % 256
if cs != checksum:
LOG.debug("Calc: %02x Real: %02x Len: %i" %
(cs, checksum, len(block)))
raise errors.RadioError("Block failed checksum")
radio.pipe.write(b"\x06")
time.sleep(0.05)
if (last_addr + 128) != addr:
LOG.debug("Gap, expecting %04x, got %04x" %
(last_addr+128, addr))
last_addr = addr
data[addr] = block
length += len(block)
status = chirp_common.Status()
status.cur = length
status.max = radio._memsize
status.msg = "Cloning from radio"
radio.status_fn(status)
return data
def _clone_out(radio):
radio.pipe.timeout = 1
# Seriously, WTF Yaesu?
ranges = [
(0x0000, 0x0000),
(0x0100, 0x0380),
(0x0480, 0xFF80),
(0x0080, 0x0080),
(0xFFFE, 0xFFFE),
]
for start, end in ranges:
for i in range(start, end+1, 128):
block = radio._mmap[i:i + 128]
frame = struct.pack(">H", i) + block
cs = 0
for byte in frame:
cs += byte
frame += bytes([cs % 256])
radio.pipe.write(frame)
ack = radio.pipe.read(1)
if not ack:
raise errors.RadioNoResponse()
if ack != b"\x06":
raise errors.RadioError("Radio refused block %i" % (i / 128))
time.sleep(0.05)
status = chirp_common.Status()
status.cur = i + 128
status.max = radio._memsize
status.msg = "Cloning to radio"
radio.status_fn(status)
def get_freq(rawfreq):
"""Decode a frequency that may include a fractional step flag"""
# Ugh. The 0x80 and 0x40 indicate values to add to get the
# real frequency. Gross.
if rawfreq > 8000000000:
rawfreq = (rawfreq - 8000000000) + 5000
if rawfreq > 4000000000:
rawfreq = (rawfreq - 4000000000) + 2500
if rawfreq > 2000000000:
rawfreq = (rawfreq - 2000000000) + 1250
return rawfreq
def set_freq(freq, obj, field):
"""Encode a frequency with any necessary fractional step flags"""
obj[field] = freq / 10000
frac = freq % 10000
if frac >= 5000:
frac -= 5000
obj[field][0].set_bits(0x80)
if frac >= 2500:
frac -= 2500
obj[field][0].set_bits(0x40)
if frac >= 1250:
frac -= 1250
obj[field][0].set_bits(0x20)
return freq
@directory.register
class FTM350Radio(yaesu_clone.YaesuCloneModeRadio):
"""Yaesu FTM-350"""
BAUD_RATE = 48000
VENDOR = "Yaesu"
MODEL = "FTM-350"
_model = ""
_memsize = 65536
_vfo = ""
def get_features(self):
rf = chirp_common.RadioFeatures()
rf.has_bank = False
rf.has_ctone = False
rf.has_settings = self._vfo == "left"
rf.has_tuning_step = False
rf.has_dtcs_polarity = False
rf.has_sub_devices = self.VARIANT == ""
rf.valid_skips = [] # FIXME: Finish this
rf.valid_tmodes = [""] + [x for x in TMODES if x]
rf.valid_modes = [x for x in MODES if x]
rf.valid_duplexes = DUPLEXES
rf.valid_skips = SKIPS
rf.valid_name_length = 8
rf.valid_characters = CHARSET
rf.memory_bounds = (0, 500)
rf.valid_power_levels = POWER_LEVELS
rf.valid_bands = [(500000, 1800000),
(76000000, 250000000),
(30000000, 1000000000)]
rf.can_odd_split = True
rf.valid_tuning_steps = [5.0, 6.25, 8.33, 10.0, 12.5, 15.0, 20.0,
25.0, 50.0, 100.0, 200.0]
return rf
def get_sub_devices(self):
return [FTM350RadioLeft(self._mmap), FTM350RadioRight(self._mmap)]
def sync_in(self):
try:
self._mmap = _clone_in(self)
except errors.RadioError:
raise
except Exception as e:
LOG.exception('Failed to read: %s', e)
raise errors.RadioError("Failed to download from radio (%s)" % e)
self.process_mmap()
def sync_out(self):
try:
_clone_out(self)
except errors.RadioError:
raise
except Exception as e:
LOG.exception('Failed to write: %s', e)
raise errors.RadioError("Failed to upload to radio (%s)" % e)
def process_mmap(self):
self._memobj = bitwise.parse(mem_format, self._mmap)
def get_raw_memory(self, number):
def identity(o):
return o
def indexed(o):
return o[number - 1]
if number == 0:
suffix = "_zero"
fn = identity
else:
suffix = ""
fn = indexed
return (repr(fn(self._memory_obj(suffix))) +
repr(fn(self._label_obj(suffix))))
def _memory_obj(self, suffix=""):
return getattr(self._memobj, "%s_memory%s" % (self._vfo, suffix))
def _label_obj(self, suffix=""):
return getattr(self._memobj, "%s_label%s" % (self._vfo, suffix))
def get_memory(self, number):
if number == 0:
_mem = self._memory_obj("_zero")
_lab = self._label_obj("_zero")
else:
_mem = self._memory_obj()[number - 1]
_lab = self._label_obj()[number - 1]
mem = chirp_common.Memory()
mem.number = number
if not _mem.used:
mem.empty = True
return mem
if _mem.offset == 0xFF:
# TX disabled
mem.duplex = "off"
elif _mem.oddsplit:
mem.duplex = "split"
mem.offset = get_freq(int(_mem.split) * 10000)
else:
mem.duplex = DUPLEXES[_mem.duplex]
mem.offset = int(_mem.offset) * 50000
mem.freq = get_freq(int(_mem.freq) * 10000)
if mem.duplex != "off":
mem.rtone = chirp_common.TONES[_mem.tone]
mem.dtcs = chirp_common.DTCS_CODES[_mem.dtcs]
mem.power = POWER_LEVELS[_mem.power]
mem.tmode = TMODES[_mem.tmode]
mem.mode = MODES[_mem.mode]
mem.skip = SKIPS[_mem.skip]
for char in _lab.string:
if char == 0xCA:
break
try:
mem.name += CHARSET[char]
except IndexError:
mem.name += "?"
mem.name = mem.name.rstrip()
return mem
def set_memory(self, mem):
if mem.number == 0:
_mem = self._memory_obj("_zero")
_lab = self._label_obj("_zero")
else:
_mem = self._memory_obj()[mem.number - 1]
_lab = self._label_obj()[mem.number - 1]
_mem.used = not mem.empty
_mem.visible = not mem.empty
if mem.empty:
return
set_freq(mem.freq, _mem, 'freq')
_mem.oddsplit = 0
_mem.tone = chirp_common.TONES.index(mem.rtone)
_mem.dtcs = chirp_common.DTCS_CODES.index(mem.dtcs)
_mem.tmode = TMODES.index(mem.tmode)
_mem.duplex = DUPLEXES.index(mem.duplex)
if mem.duplex == "off":
_mem.tone = 0x3F
_mem.dtcs = 0x7F
_mem.offset = 0xFF
_mem.power = 0x03
_mem.tmode = 0x00
_mem.duplex = 0x00
elif mem.duplex == "split":
set_freq(mem.offset, _mem, 'split')
_mem.oddsplit = 1
else:
_mem.offset = mem.offset / 50000
_mem.mode = MODES.index(mem.mode)
_mem.skip = SKIPS.index(mem.skip)
if mem.power:
_mem.power = POWER_LEVELS.index(mem.power)
else:
_mem.power = 0
for i in range(0, 8):
try:
char = CHARSET.index(mem.name[i])
except IndexError:
char = 0xCA
_lab.string[i] = char
_mem.showalpha = mem.name.strip() != ""
@classmethod
def match_model(self, filedata, filename):
return filedata.startswith(b"AH033$")
def get_settings(self):
top = RadioSettings()
aprs = RadioSettingGroup("aprs", "APRS")
top.append(aprs)
myc = self._memobj.aprs_my_callsign
rs = RadioSetting("aprs_my_callsign.call", "APRS My Callsign",
RadioSettingValueString(0, 6,
aprs_call_to_str(myc.call)))
aprs.append(rs)
rs = RadioSetting("aprs_my_callsign.ssid", "APRS My SSID",
RadioSettingValueInteger(0, 15, myc.ssid))
aprs.append(rs)
return top
def set_settings(self, settings):
for setting in settings:
if not isinstance(setting, RadioSetting):
self.set_settings(setting)
continue
# Quick hack to make these work
if setting.get_name() == "aprs_my_callsign.call":
self._memobj.aprs_my_callsign.call = \
setting.value.get_value().upper().replace(" ", "\xCA")
elif setting.get_name() == "aprs_my_callsign.ssid":
self._memobj.aprs_my_callsign.ssid = setting.value
class FTM350RadioLeft(FTM350Radio):
VARIANT = "Left"
_vfo = "left"
class FTM350RadioRight(FTM350Radio):
VARIANT = "Right"
_vfo = "right"
chirp-20261030/chirp/drivers/ftm7250d.py 0000644 0001750 0001750 00000015135 14670320477 016456 0 ustar hibby hibby # Copyright 2010 Dan Smith
# Copyright 2017 Wade Simmons
# FTM-7250D driver cloned from the FTM-3200 and modified by Iain White
#
# 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.drivers import yaesu_clone, ft1d
from chirp import chirp_common, directory, bitwise
from chirp.settings import RadioSettings
LOG = logging.getLogger(__name__)
POWER_LEVELS = [chirp_common.PowerLevel("Low", watts=5),
chirp_common.PowerLevel("Mid", watts=25),
chirp_common.PowerLevel("Hi", watts=50)]
TMODES = ["", "Tone", "TSQL", "DTCS", "TSQL-R", None, None, "Pager", "Cross"]
CROSS_MODES = [None, "DTCS->", "Tone->DTCS", "DTCS->Tone"]
MODES = ["FM", "NFM"]
STEPS = [0, 5, 6.25, 10, 12.5, 15, 20, 25, 50, 100] # 0 = auto
RFSQUELCH = ["OFF", "S1", "S2", "S3", "S4", "S5", "S6", "S7", "S8"]
# Charset is subset of ASCII + some unknown chars \x80-\x86
VALID_CHARS = ["%i" % int(x) for x in range(0, 10)] + \
list(":>=@") + \
[chr(x) for x in range(ord("A"), ord("Z") + 1)] + \
list("[\\]_") + \
[chr(x) for x in range(ord("a"), ord("z") + 1)] + \
list("%*+,-/=$ ")
MEM_FORMAT = """
#seekto 0xceca;
struct {
u8 unknown5;
u8 unknown3;
u8 unknown4:6,
dsqtype:2;
u8 dsqcode;
u8 unknown1[2];
char mycall[10];
u8 unknown2[368];
} settings;
#seekto 0xfec9;
u8 checksum;
"""
@directory.register
class FTM7250Radio(ft1d.FT1Radio):
"""Yaesu FTM-7250D"""
BAUD_RATE = 38400
VENDOR = "Yaesu"
MODEL = "FTM-7250D"
VARIANT = "R"
_model = "AH75M"
_memsize = 65227
_block_lengths = [10, 65217]
_has_vibrate = False
_has_af_dual = False
_mem_params = {'memnum': 199, # size of memories array
'flgnum': 199} # size of flags array
@classmethod
def get_prompts(cls):
rp = chirp_common.RadioPrompts()
rp.pre_download = _(
"1. Turn radio off.\n"
"2. Connect cable to MIC Jack.\n"
"3. Press and hold in the [MHz(SETUP)] key while turning the"
" radio\n"
" on (\"CLONE\" will appear on the display).\n"
"4. After clicking OK , press the [GM(AMS)] key\n"
" to send image.\n")
rp.pre_upload = _(
"1. Turn radio off.\n"
"2. Connect cable to MIC Jack.\n"
"3. Press and hold in the [MHz(SETUP)] key while turning the"
" radio\n"
" on (\"CLONE\" will appear on the display).\n"
"4. Press the [MHz(SETUP)] key\n"
" (\"-WAIT-\" will appear on the LCD).\n")
return rp
def process_mmap(self):
mem_format = ft1d.MEM_FORMAT + MEM_FORMAT
self._memobj = bitwise.parse(mem_format % self._mem_params, self._mmap)
def get_features(self):
rf = chirp_common.RadioFeatures()
rf.has_dtcs_polarity = False
rf.valid_modes = list(MODES)
rf.valid_tmodes = [x for x in TMODES if x is not None]
rf.valid_cross_modes = [x for x in CROSS_MODES if x is not None]
rf.valid_duplexes = list(ft1d.DUPLEX)
rf.valid_tuning_steps = [x for x in STEPS if x]
rf.valid_bands = [(108000000, 580000000)]
# rf.valid_skips = SKIPS
rf.valid_power_levels = POWER_LEVELS
rf.valid_characters = "".join(VALID_CHARS)
rf.valid_name_length = 8
rf.memory_bounds = (1, 199)
rf.can_odd_split = True
rf.has_ctone = False
rf.has_bank = False
rf.has_bank_names = False
# disable until implemented
rf.has_settings = False
return rf
def _decode_label(self, mem):
# TODO preserve the unknown \x80-x86 chars?
return str(mem.label).rstrip("\xFF")
def _encode_label(self, mem):
label = mem.name.rstrip().encode('ascii', 'ignore')
return self._add_ff_pad(label, 16)
def _encode_charsetbits(self, mem):
# TODO this is a setting to decide if the memory should be displayed
# as a name or frequency. Should we expose this setting to the user
# instead of autoselecting it (and losing their preference)?
if mem.name.rstrip() == '':
return 0x00
return 0x00
def _decode_power_level(self, mem):
return POWER_LEVELS[mem.power - 1]
def _encode_power_level(self, mem):
if mem.power is None:
return 3
else:
return 3 - POWER_LEVELS.index(mem.power)
def _decode_mode(self, mem):
return MODES[mem.mode_alt]
def _encode_mode(self, mem):
return MODES.index(mem.mode)
def _get_tmode(self, mem, _mem):
if _mem.tone_mode > 8:
tmode = "Cross"
mem.cross_mode = CROSS_MODES[_mem.tone_mode - 8]
else:
tmode = TMODES[_mem.tone_mode]
if tmode == "Pager":
# TODO chirp_common does not allow 'Pager'
# Expose as a different setting?
mem.tmode = ""
else:
mem.tmode = tmode
def _set_tmode(self, _mem, mem):
if mem.tmode == "Cross":
_mem.tone_mode = 8 + CROSS_MODES.index(mem.cross_mode)
else:
_mem.tone_mode = TMODES.index(mem.tmode)
def _set_mode(self, _mem, mem):
_mem.mode_alt = self._encode_mode(mem)
def get_bank_model(self):
return None
def _debank(self, mem):
return
def _checksums(self):
return [yaesu_clone.YaesuChecksum(0x064A, 0x06C8),
yaesu_clone.YaesuChecksum(0x06CA, 0x0748),
yaesu_clone.YaesuChecksum(0x074A, 0x07C8),
yaesu_clone.YaesuChecksum(0x07CA, 0x0848),
yaesu_clone.YaesuChecksum(0x0000, 0xFEC9)]
def _get_settings(self):
# TODO
top = RadioSettings()
return top
@classmethod
def _wipe_memory(cls, mem):
mem.set_raw("\x00" * (mem.size() // 8))
def sync_out(self):
# Need to give enough time for the radio to ACK after writes
self.pipe.timeout = 1
return super(FTM7250Radio, self).sync_out()
chirp-20261030/chirp/drivers/ga510.py 0000644 0001750 0001750 00000112765 15172631605 016026 0 ustar hibby hibby # Copyright 2024 Dan Smith
# Portions copyright 2023 Dave Liske
# Portions copyright 2020 by Jiauxn Yang
#
# 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 struct
from chirp import bitwise
from chirp import chirp_common
from chirp import directory
from chirp.drivers import baofeng_uv17
from chirp.drivers import baofeng_uv17Pro
from chirp import errors
from chirp import memmap
from chirp.settings import RadioSetting, RadioSettingGroup, RadioSettings
from chirp.settings import RadioSettingValueBoolean, RadioSettingValueList
from chirp.settings import RadioSettingValueInteger, RadioSettingValueString
LOG = logging.getLogger(__name__)
# GA510 and SHX8800 also have DTCS code 645
DTCS_CODES = tuple(sorted(chirp_common.DTCS_CODES + (645,)))
DTMFCHARS = '0123456789ABCD*#'
AIRBAND = (108000000, 136000000)
def reset(radio):
try:
radio.pipe.write(b'E')
except Exception as e:
LOG.error('Failed to reset radio: %s' % e)
def start_program(radio):
reset(radio)
radio.pipe.read(256)
radio.pipe.write(radio._magic)
ack = radio.pipe.read(256)
if not ack:
raise errors.RadioNoResponse()
if not ack.endswith(b'\x06'):
LOG.debug('Ack was %r' % ack)
raise errors.RadioError('Unexpected response to clone request')
radio.pipe.write(b'F')
ident = radio.pipe.read(8)
if not ident:
raise errors.RadioNoResponse()
if len(ident) != 8:
raise errors.RadioError('Radio sent short ident response')
LOG.debug('Radio ident string is %r' % ident)
return ident
def do_download(radio):
# No start_program() here because it was done in detect_from_serial()
s = chirp_common.Status()
s.msg = 'Downloading'
s.max = 0x1C00
data = bytes()
for addr in range(0, 0x1C40, 0x40):
cmd = struct.pack('>cHB', b'R', addr, 0x40)
radio.pipe.log('Reading block at %04x' % addr)
radio.pipe.write(cmd)
block = radio.pipe.read(0x44)
if not block:
raise errors.RadioNoResponse()
if len(block) < 4:
raise errors.RadioError('Radio sent short block header')
header = block[:4]
rcmd, raddr, rlen = struct.unpack('>BHB', header)
block = block[4:]
if raddr != addr:
raise errors.RadioError('Radio send address %04x, expected %04x' %
(raddr, addr))
if rlen != 0x40 or len(block) != 0x40:
raise errors.RadioError('Radio sent %02x (%02x) bytes, '
'expected %02x' % (rlen, len(block), 0x40))
data += block
s.cur = addr
radio.status_fn(s)
return data
def do_upload(radio):
ident = start_program(radio)
s = chirp_common.Status()
s.msg = 'Uploading'
s.max = 0x1C00
# The factory software downloads 0x40 for the block
# at 0x1C00, but only uploads 0x20 there. Mimic that
# here.
for addr in range(0, 0x1C20, 0x20):
cmd = struct.pack('>cHB', b'W', addr, 0x20)
radio.pipe.log('Writing block at %04x' % addr)
block = radio._mmap[addr:addr + 0x20]
radio.pipe.write(cmd)
radio.pipe.write(block)
ack = radio.pipe.read(1)
if not ack:
raise errors.RadioNoResponse()
if ack != b'\x06':
raise errors.RadioError('Radio refused block at addr %04x' % addr)
s.cur = addr
radio.status_fn(s)
BASE_FORMAT = """
struct {
lbcd rxfreq[4];
lbcd txfreq[4];
ul16 rxtone;
ul16 txtone;
u8 signal;
u8 unknown1:6,
pttid:2;
u8 unknown2:6,
power:2;
u8 unknown3_0:1,
narrow:1,
unknown3_1:2,
bcl:1,
scan:1,
allow_tx:1, // SHX8800 FAMILY ONLY (unknown3_2 for GA-510)
fhss:1;
} memories[128];
#seekto 0x0C00;
struct {
char name[10];
u8 pad[6];
} names[128];
"""
MODEL_GA510_FORMAT = """
#seekto 0x1A00;
struct {
// 0x1A00
u8 squelch;
u8 savemode; // [off, mode1, mode2, mode3]
u8 vox; // off=0
u8 backlight;
u8 tdr; // bool
u8 timeout; // n*15 = seconds
u8 beep; // bool
u8 voice;
// 0x1A08
u8 language; // [eng, chin]
u8 dtmfst;
u8 scanmode; // [TO, CO, SE]
u8 pttid; // [off, BOT, EOT, Both]
u8 pttdelay; // 0-30
u8 cha_disp; // [ch-name, ch-freq]
// [ch, ch-name]; retevis
u8 chb_disp;
u8 bcl; // bool
// 0x1A10
u8 autolock; // bool
u8 alarm_mode; // [site, tone, code]
u8 alarmsound; // bool
u8 txundertdr; // [off, bandA, bandB]
u8 tailnoiseclear; // [off, on]
u8 rptnoiseclr; // 10*ms, 0-1000
u8 rptnoisedet;
u8 roger; // bool
// 0x1A18
u8 unknown1a10;
u8 fmradio; // boolean, inverted
u8 workmode; // [vfo, chan]; 1A30-1A31 related?
u8 kblock; // boolean
} settings;
#seekto 0x1A80;
struct {
u8 skey1sp; // [off, lamp, sos, fm, noaa, moni, search]
u8 skey1lp; // [off, lamp, sos, fm, noaa, moni, search]
u8 skey2sp; // [off, lamp, sos, fm, noaa, moni, search]
u8 skey2lp; // [off, lamp, sos, fm, noaa, moni, search]
} skey;
struct dtmfcode {
u8 code[5];
u8 ffpad[11]; // always 0xFF
};
#seekto 0x1B00;
struct dtmfcode dtmfgroup[15];
struct {
u8 code[5];
u8 groupcode; // 0->D, *, #
u8 nothing:6,
releasetosend:1,
presstosend:1;
u8 dtmfspeedon; // 80 + n*10, up to [194]
u8 dtmfspeedoff;
} anicode;
//dtmf on -> 90ms
//dtmf off-> 120ms
//group code *->0
//press 0->1
//release 1->0
"""
MODEL_SHX8800_FORMAT = """
#seekto 0x1A00;
struct {
// 0x1A00
u8 squelch;
u8 savemode; // [off, mode1, mode2, mode3]
u8 vox; // off=0
u8 backlight;
u8 tdr; // bool
u8 timeout; // n*15 = seconds
u8 beep; // bool
u8 voice;
// 0x1A08
u8 language; // [inop in 8800]
u8 dtmfst;
u8 scanmode; // [TO, CO, SE]
u8 pttid; // [off, BOT, EOT, Both]
u8 pttdelay; // 0-30
u8 cha_disp; // [ch-name, ch-freq]
// [ch, ch-name]; retevis
u8 chb_disp;
u8 bcl; // bool
// 0x1A10
u8 autolock; // bool
u8 alarm_mode; // [site, tone, code]
u8 alarmsound; // bool
u8 txundertdr; // [off, bandA, bandB]
u8 tailnoiseclear; // [off, on]
u8 rptnoiseclr; // 10*ms, 0-1000
u8 rptnoisedet;
u8 roger; // bool
// 0x1A18
u8 unknown1a10;
u8 fmradio; // boolean, inverted
u8 workmodeb:4,
workmodea:4;
u8 kblock;
u8 unknown2[4];
u8 voxdelay;
u8 menu_timeout;
u8 micgain;
} settings;
#seekto 0x1a40;
struct {
u8 freq[8];
ul16 rxtone;
ul16 txtone;
u8 unknown[2];
u8 unused2:2,
sftd:2,
scode:4;
u8 unknown1;
u8 txpower;
u8 widenarr:1,
unknown2:4,
fhss:1,
unknown3:2;
u8 band;
u8 unknown4:5,
step:3;
u8 unknown5;
u8 offset[6];
} vfoa; // displays in Browser tab
#seekto 0x1a60;
struct {
u8 freq[8];
ul16 rxtone;
ul16 txtone;
u8 unknown[2];
u8 unused2:2,
sftd:2,
scode:4;
u8 unknown1;
u8 txpower;
u8 widenarr:1,
unknown2:4,
fhss:1,
unknown3:2;
u8 band;
u8 unknown4:5,
step:3;
u8 unknown5;
u8 offset[6];
} vfob; // displays in Browser tab
#seekto 0x1a80;
struct {
u8 sidekey;
u8 sidekeyl;
} keymaps;
#seekto 0x1b00;
struct {
u8 code[5];
u8 unused[11];
} dtmfgroup[15];
struct {
u8 code[5];
u8 groupcode;
u8 aniid;
u8 dtmfspeedon;
u8 dtmfspeedoff;
} anicode;
"""
PTTID = ['Off', 'BOT', 'EOT', 'Both']
SIGNAL = [str(i) for i in range(1, 16)]
WORKMODE_LIST = ["VFO", "CH"]
SHIFTD_LIST = ["Off", "+", "-"]
PTTIDCODE_LIST = ["%s" % x for x in range(1, 128)]
STEPS = [6.25, 10.0, 12.5, 20.0, 25.0, 50.0]
STEP_LIST = [str(x) for x in STEPS]
TXPOWER_LIST = ["High (5W)", "Low (1W)"]
BANDWIDTH_LIST = ["Wide", "Narrow"]
@directory.register
class RadioddityGA510Radio(chirp_common.CloneModeRadio):
VENDOR = 'Radioddity'
MODEL = 'GA-510'
BAUD_RATE = 9600
POWER_LEVELS = [
chirp_common.PowerLevel('H', watts=10),
chirp_common.PowerLevel('L', watts=1),
chirp_common.PowerLevel('M', watts=5)]
_mem_format = MODEL_GA510_FORMAT
_magic = (b'PROGROMBFHU')
_gmrs = False
@classmethod
def detect_from_serial(cls, pipe):
saw_data = False
for rcls in reversed(cls.detected_models()):
pipe.baudrate = rcls.BAUD_RATE
radio = rcls(pipe)
try:
if isinstance(radio, baofeng_uv17Pro.UV17Pro):
ident = baofeng_uv17Pro._do_ident(radio)
else:
ident = start_program(radio)
except errors.RadioNoResponse:
LOG.debug('No response from radio with %s', rcls)
pipe.read(256)
continue
except errors.RadioError:
saw_data = True
LOG.debug('Unexpected response from radio with %s', rcls)
pipe.read(256)
continue
if ident:
saw_data = True
return rcls
saw_data = True
if saw_data:
raise errors.RadioError('Unexpected response from radio')
raise errors.RadioNoResponse()
def sync_in(self):
try:
data = do_download(self)
self._mmap = memmap.MemoryMapBytes(data)
except errors.RadioError:
raise
except Exception as e:
LOG.exception('General failure')
raise errors.RadioError('Failed to download from radio: %s' % e)
finally:
reset(self)
self.process_mmap()
def sync_out(self):
try:
do_upload(self)
except errors.RadioError:
raise
except Exception as e:
LOG.exception('General failure')
raise errors.RadioError('Failed to upload to radio: %s' % e)
finally:
reset(self)
def process_mmap(self):
self._memobj = bitwise.parse(BASE_FORMAT + self._mem_format,
self._mmap)
def get_features(self):
rf = chirp_common.RadioFeatures()
rf.memory_bounds = (0, 127)
rf.has_ctone = True
rf.has_cross = True
rf.has_tuning_step = False
rf.has_settings = True
rf.has_bank = False
rf.has_sub_devices = False
rf.has_dtcs_polarity = True
rf.has_rx_dtcs = True
rf.can_odd_split = True
rf.valid_tmodes = ['', 'Tone', 'TSQL', 'DTCS', 'Cross']
rf.valid_cross_modes = ['Tone->Tone', 'DTCS->', '->DTCS', 'Tone->DTCS',
'DTCS->Tone', '->Tone', 'DTCS->DTCS']
rf.valid_modes = ['FM', 'NFM']
rf.valid_tuning_steps = [2.5, 5.0, 6.25, 12.5, 10.0, 15.0, 20.0,
25.0, 50.0, 100.0]
rf.valid_dtcs_codes = DTCS_CODES
rf.valid_duplexes = ['', '-', '+', 'split', 'off']
rf.valid_power_levels = self.POWER_LEVELS
rf.valid_name_length = 10
rf.valid_characters = ('ABCDEFGHIJKLMNOPQRSTUVWXYZ'
'abcdefghijklmnopqrstuvwxyz'
'0123456789'
'!"#$%&\'()~+-,./:;<=>?@[\\]^`{}*| ')
rf.valid_bands = [(136000000, 174000000),
(400000000, 480000000)]
return rf
def get_raw_memory(self, num):
return repr(self._memobj.memories[num]) + repr(self._memobj.names[num])
@staticmethod
def _decode_tone(toneval):
if toneval in (0, 0xFFFF):
LOG.debug('no tone value: %s' % toneval)
return '', None, None
elif toneval < 670:
toneval = toneval - 1
index = toneval % len(DTCS_CODES)
if index != int(toneval):
pol = 'R'
# index -= 1
else:
pol = 'N'
return 'DTCS', DTCS_CODES[index], pol
else:
return 'Tone', toneval / 10.0, 'N'
@staticmethod
def _encode_tone(mode, val, pol):
if not mode:
return 0x0000
elif mode == 'Tone':
return int(val * 10)
elif mode == 'DTCS':
index = DTCS_CODES.index(val)
if pol == 'R':
index += len(DTCS_CODES)
index += 1
LOG.debug('Encoded dtcs %s/%s to %04x' % (val, pol, index))
return index
else:
raise errors.RadioError('Unsupported tone mode %r' % mode)
def _get_extra(self, _mem):
group = RadioSettingGroup('extra', 'Extra')
s = RadioSetting('bcl', 'Busy Channel Lockout',
RadioSettingValueBoolean(_mem.bcl))
group.append(s)
s = RadioSetting('fhss', 'FHSS',
RadioSettingValueBoolean(_mem.fhss))
group.append(s)
# pttid, signal
s = RadioSetting('pttid', 'PTTID',
RadioSettingValueList(PTTID, int(_mem.pttid)))
group.append(s)
s = RadioSetting('signal', 'Signal',
RadioSettingValueList(SIGNAL, int(_mem.signal)))
group.append(s)
return group
def _set_extra(self, _mem, mem):
_mem.bcl = int(mem.extra['bcl'].value)
_mem.fhss = int(mem.extra['fhss'].value)
_mem.pttid = int(mem.extra['pttid'].value)
_mem.signal = int(mem.extra['signal'].value)
def _is_txinh(self, _mem):
raw_tx = ""
for i in range(0, 4):
raw_tx += _mem.txfreq[i].get_raw(asbytes=False)
return raw_tx == "\xFF\xFF\xFF\xFF"
def _get_mem(self, num):
return self._memobj.memories[num]
def _get_nam(self, num):
return self._memobj.names[num]
def get_memory(self, num):
_mem = self._get_mem(num)
_nam = self._get_nam(num)
mem = chirp_common.Memory()
mem.number = num
if int(_mem.rxfreq) == 166666665:
mem.empty = True
return mem
mem.name = ''.join([str(c) for c in _nam.name
if ord(str(c)) < 127]).rstrip()
mem.freq = int(_mem.rxfreq) * 10
offset = (int(_mem.txfreq) - int(_mem.rxfreq)) * 10
if self._is_txinh(_mem):
mem.duplex = 'off'
mem.offset = 0
elif offset == 0:
mem.duplex = ''
elif abs(offset) < 100000000:
mem.duplex = offset < 0 and '-' or '+'
mem.offset = abs(offset)
else:
mem.duplex = 'split'
mem.offset = int(_mem.txfreq) * 10
mem.power = self.POWER_LEVELS[_mem.power]
mem.mode = 'NFM' if _mem.narrow else 'FM'
mem.skip = '' if _mem.scan else 'S'
LOG.debug('got txtone: %s' % repr(self._decode_tone(_mem.txtone)))
LOG.debug('got rxtone: %s' % repr(self._decode_tone(_mem.rxtone)))
chirp_common.split_tone_decode(mem,
self._decode_tone(_mem.txtone),
self._decode_tone(_mem.rxtone))
try:
mem.extra = self._get_extra(_mem)
except:
LOG.exception('Failed to get extra for %i' % num)
return mem
def _set_mem(self, number):
return self._memobj.memories[number]
def _set_nam(self, number):
return self._memobj.names[number]
def set_memory(self, mem):
_mem = self._set_mem(mem.number)
_nam = self._set_nam(mem.number)
if mem.empty:
_mem.set_raw(b'\xff' * 16)
_nam.set_raw(b'\xff' * 16)
return
if int(_mem.rxfreq) == 166666665:
LOG.debug('Initializing new memory %i' % mem.number)
_mem.set_raw(b'\x00' * 16)
_nam.name = mem.name.ljust(10)
if isinstance(self, Senhaix8800Radio):
_mem.allow_tx = True
_mem.rxfreq = mem.freq // 10
if mem.duplex == '':
_mem.txfreq = mem.freq // 10
elif mem.duplex == 'split':
_mem.txfreq = mem.offset // 10
elif mem.duplex == 'off':
if isinstance(self, Senhaix8800Radio):
_mem.allow_tx = False
for i in range(0, 4):
_mem.txfreq[i].set_raw(b'\xFF')
elif mem.duplex == '-':
_mem.txfreq = (mem.freq - mem.offset) // 10
elif mem.duplex == '+':
_mem.txfreq = (mem.freq + mem.offset) // 10
else:
raise errors.RadioError('Unsupported duplex mode %r' % mem.duplex)
txtone, rxtone = chirp_common.split_tone_encode(mem)
LOG.debug('tx tone is %s' % repr(txtone))
LOG.debug('rx tone is %s' % repr(rxtone))
_mem.txtone = self._encode_tone(*txtone)
_mem.rxtone = self._encode_tone(*rxtone)
try:
_mem.power = self.POWER_LEVELS.index(mem.power)
except ValueError:
_mem.power = 0
_mem.narrow = mem.mode == 'NFM'
_mem.scan = mem.skip != 'S'
if mem.extra:
self._set_extra(_mem, mem)
def get_settings(self):
_set = self._memobj.settings
basic = RadioSettingGroup('basic', 'Basic')
adv = RadioSettingGroup('advanced', 'Advanced')
dtmf = RadioSettingGroup('dtmf', 'DTMF')
radioddity_settings = {
'savemode': ['Off', 'Mode 1', 'Mode 2', 'Mode 3'],
'cha_disp': ['CH+Name', 'CH+Freq'],
'chb_disp': ['CH+Name', 'CH+Freq'],
'txundertdr': ['Off', 'Band A', 'Band B'],
'rptnoiseclr': ['Off'] + ['%i' % i for i in range(100, 1001, 100)],
'rptnoisedet': ['Off'] + ['%i' % i for i in range(100, 1001, 100)],
}
retevis_settings = {
'savemode': ['Off', 'On'],
'cha_disp': ['CH', 'CH+Name'],
'chb_disp': ['CH', 'CH+Name'],
}
language_setting = {
'language': ['English', 'Chinese'],
}
ga_workmode = {
'workmode': ['VFO', 'Chan'],
}
shx_workmode = {
'workmodea': ['VFO', 'Chan'],
'workmodeb': ['VFO', 'Chan'],
}
choice_settings = {
'vox': ['Off'] + ['%i' % i for i in range(1, 11)],
'backlight': ['Off'] + ['%i' % i for i in range(1, 11)],
'timeout': ['Off'] + ['%i' % i for i in range(15, 615, 15)],
'dtmfst': ['OFF', 'KB Side Tone', 'ANI Side Tone',
'KB ST+ANI ST', 'Both'],
'scanmode': ['TO', 'CO', 'SE'],
'pttid': ['Off', 'BOT', 'EOT', 'Both'],
'alarm_mode': ['Site', 'Tone', 'Code'],
}
if isinstance(self, Senhaix8800Radio):
choice_settings.update(shx_workmode)
else:
choice_settings.update(language_setting)
choice_settings.update(ga_workmode)
if self.VENDOR == "Retevis":
choice_settings.update(retevis_settings)
else:
choice_settings.update(radioddity_settings)
if isinstance(self, Senhaix8800Radio):
basic_settings = ['timeout', 'vox', 'backlight',
'cha_disp', 'chb_disp', 'workmodea',
'workmodeb']
else:
basic_settings = ['timeout', 'vox', 'backlight', 'language',
'cha_disp', 'chb_disp', 'workmode']
titles = {
'savemode': 'Save Mode',
'vox': 'VOX',
'backlight': 'Auto Backlight',
'timeout': 'Time Out Timer (s)',
'language': 'Language',
'dtmfst': 'DTMF-ST',
'scanmode': 'Scan Mode',
'pttid': 'PTT-ID',
'cha_disp': 'Channel A Display',
'chb_disp': 'Channel B Display',
'alarm_mode': 'Alarm Mode',
'txundertdr': 'TX Under TDR',
'rptnoiseclr': 'RPT Noise Clear (ms)',
'rptnoisedet': 'RPT Noise Detect (ms)',
'workmode': 'Work Mode',
'workmodea': 'Work Mode A',
'workmodeb': 'Work Mode B',
}
basic.append(
RadioSetting('squelch', 'Squelch Level',
RadioSettingValueInteger(0, 9, int(_set.squelch))))
adv.append(
RadioSetting('pttdelay', 'PTT Delay',
RadioSettingValueInteger(0, 30, int(_set.pttdelay))))
adv.append(
RadioSetting('tdr', 'TDR',
RadioSettingValueBoolean(
int(_set.tdr))))
adv.append(
RadioSetting('beep', 'Beep',
RadioSettingValueBoolean(
int(_set.beep))))
basic.append(
RadioSetting('voice', 'Voice Enable',
RadioSettingValueBoolean(
int(_set.voice))))
adv.append(
RadioSetting('bcl', 'BCL',
RadioSettingValueBoolean(
int(_set.bcl))))
adv.append(
RadioSetting('autolock', 'Auto Lock',
RadioSettingValueBoolean(
int(_set.autolock))))
adv.append(
RadioSetting('alarmsound', 'Alarm Sound',
RadioSettingValueBoolean(
int(_set.alarmsound))))
adv.append(
RadioSetting('tailnoiseclear', 'Tail Noise Clear',
RadioSettingValueBoolean(
int(_set.tailnoiseclear))))
adv.append(
RadioSetting('roger', 'Roger',
RadioSettingValueBoolean(
int(_set.roger))))
adv.append(
RadioSetting('fmradio', 'FM Radio Disabled',
RadioSettingValueBoolean(
int(_set.fmradio))))
adv.append(
RadioSetting('kblock', 'KB Lock',
RadioSettingValueBoolean(
int(_set.kblock))))
for key in sorted(choice_settings):
choices = choice_settings[key]
title = titles[key]
if key in basic_settings:
group = basic
else:
group = adv
val = int(getattr(_set, key))
try:
cur = choices[val]
except IndexError:
LOG.error('Value %i for %s out of range for list (%i): %s' % (
val, key, len(choices), choices))
raise
group.append(
RadioSetting(key, title,
RadioSettingValueList(
choices,
current_index=val)))
if self.VENDOR == "Retevis":
# Side Keys
_skey = self._memobj.skey
SK_CHOICES = ['OFF', 'LAMP', 'SOS', 'FM', 'NOAA', 'MONI', 'SEARCH']
SK_VALUES = [0xFF, 0x08, 0x03, 0x07, 0x0C, 0x05, 0x1D]
def apply_sk_listvalue(setting, obj):
LOG.debug("Setting value: " + str(setting.value) +
" from list")
val = str(setting.value)
index = SK_CHOICES.index(val)
val = SK_VALUES[index]
obj.set_value(val)
# Side Key 1 - Short Press
if _skey.skey1sp in SK_VALUES:
idx = SK_VALUES.index(_skey.skey1sp)
else:
idx = SK_VALUES.index(0xFF)
rs = RadioSetting('skey.skey1sp', 'Side Key 1 - Short Press',
RadioSettingValueList(SK_CHOICES,
current_index=idx))
rs.set_apply_callback(apply_sk_listvalue, _skey.skey1sp)
adv.append(rs)
# Side Key 1 - Long Press
if _skey.skey1lp in SK_VALUES:
idx = SK_VALUES.index(_skey.skey1lp)
else:
idx = SK_VALUES.index(0xFF)
rs = RadioSetting('skey.skey1lp', 'Side Key 1 - Long Press',
RadioSettingValueList(SK_CHOICES,
current_index=idx))
rs.set_apply_callback(apply_sk_listvalue, _skey.skey1lp)
adv.append(rs)
# Side Key 2 - Short Press
if _skey.skey2sp in SK_VALUES:
idx = SK_VALUES.index(_skey.skey2sp)
else:
idx = SK_VALUES.index(0xFF)
rs = RadioSetting('skey.skey2sp', 'Side Key 2 - Short Press',
RadioSettingValueList(SK_CHOICES,
current_index=idx))
rs.set_apply_callback(apply_sk_listvalue, _skey.skey2sp)
adv.append(rs)
# Side Key 1 - Long Press
if _skey.skey2lp in SK_VALUES:
idx = SK_VALUES.index(_skey.skey2lp)
else:
idx = SK_VALUES.index(0xFF)
rs = RadioSetting('skey.skey2lp', 'Side Key 2 - Long Press',
RadioSettingValueList(SK_CHOICES,
current_index=idx))
rs.set_apply_callback(apply_sk_listvalue, _skey.skey2lp)
adv.append(rs)
for i in range(1, 16):
cur = ''.join(
DTMFCHARS[i]
for i in self._memobj.dtmfgroup[i - 1].code if int(i) < 0xF)
dtmf.append(
RadioSetting(
'dtmf.code@%i' % i, 'DTMF Group %i' % i,
RadioSettingValueString(0, 5, cur,
autopad=False,
charset=DTMFCHARS)))
cur = ''.join(
'%X' % i
for i in self._memobj.anicode.code if int(i) < 0xE)
anicode = self._memobj.anicode
if isinstance(self, Senhaix8800Radio):
_codeobj = self._memobj.anicode.code
_code = "".join([DTMFCHARS[x] for x in _codeobj if int(x) < 0x1F])
val = RadioSettingValueString(0, 5, _code, False)
val.set_charset(DTMFCHARS)
rs = RadioSetting("anicode.code", "ANI Code", val)
def apply_code(setting, obj):
code = []
for j in range(0, 5):
try:
code.append(DTMFCHARS.index(str(setting.value)[j]))
except IndexError:
code.append(0xFF)
obj.code = code
rs.set_apply_callback(apply_code, anicode)
dtmf.append(rs)
try:
current_group = DTMFCHARS[int(anicode.groupcode)]
except IndexError:
LOG.warning('ANI group code index %i out of range',
anicode.groupcode)
current_group = DTMFCHARS[0]
dtmf.append(
RadioSetting(
"anicode.groupcode", "Group Code",
RadioSettingValueList(list(DTMFCHARS),
current_group)))
else:
dtmf.append(
RadioSetting(
'anicode.code', 'ANI Code',
RadioSettingValueString(0, 5, cur,
autopad=False,
charset=DTMFCHARS)))
dtmf.append(
RadioSetting(
'anicode.groupcode', 'Group Code',
RadioSettingValueList(
list(DTMFCHARS),
DTMFCHARS[int(anicode.groupcode)])))
dtmf.append(
RadioSetting(
'anicode.releasetosend', 'Release To Send',
RadioSettingValueBoolean(
int(anicode.releasetosend))))
dtmf.append(
RadioSetting(
'anicode.presstosend', 'Press To Send',
RadioSettingValueBoolean(
int(anicode.presstosend))))
cur = int(anicode.dtmfspeedon) * 10 + 80
dtmf.append(
RadioSetting(
'anicode.dtmfspeedon', 'DTMF Speed (on time in ms)',
RadioSettingValueInteger(60, 2000, cur, 10)))
cur = int(anicode.dtmfspeedoff) * 10 + 80
dtmf.append(
RadioSetting(
'anicode.dtmfspeedoff', 'DTMF Speed (off time in ms)',
RadioSettingValueInteger(60, 2000, cur, 10)))
top = RadioSettings(basic, adv, dtmf)
return top
def set_settings(self, settings):
for element in settings:
if element.get_name().startswith('anicode.'):
self._set_anicode(element)
elif element.get_name().startswith('dtmf.code'):
self._set_dtmfcode(element)
elif element.get_name().startswith('skey.'):
self._set_skey(element)
elif not isinstance(element, RadioSetting):
self.set_settings(element)
continue
else:
self._set_setting(element)
def _set_setting(self, setting):
key = setting.get_name()
val = setting.value
setattr(self._memobj.settings, key, int(val))
def _set_anicode(self, setting):
name = setting.get_name().split('.', 1)[1]
if name == 'code':
val = [DTMFCHARS.index(c) for c in str(setting.value)]
for i in range(0, 5):
try:
value = val[i]
except IndexError:
value = 0xFF
self._memobj.anicode.code[i] = value
elif name.startswith('dtmfspeed'):
setattr(self._memobj.anicode, name,
(int(setting.value) - 80) // 10)
else:
setattr(self._memobj.anicode, name, int(setting.value))
def _set_dtmfcode(self, setting):
index = int(setting.get_name().split('@', 1)[1]) - 1
val = [DTMFCHARS.index(c) for c in str(setting.value)]
for i in range(0, 5):
try:
value = val[i]
except IndexError:
value = 0xFF
self._memobj.dtmfgroup[index].code[i] = value
def _set_skey(self, setting):
if setting.has_apply_callback():
LOG.debug("Using apply callback")
setting.run_apply_callback()
@directory.register
class RetevisRA685Radio(RadioddityGA510Radio):
VENDOR = 'Retevis'
MODEL = 'RA685'
POWER_LEVELS = [
chirp_common.PowerLevel('H', watts=5),
chirp_common.PowerLevel('L', watts=1),
chirp_common.PowerLevel('M', watts=3)]
_magic = b'PROGROMWLTU'
def get_features(self):
rf = RadioddityGA510Radio.get_features(self)
rf.memory_bounds = (1, 128)
rf.valid_bands = [(136000000, 174000000),
(400000000, 520000000)]
return rf
def _get_mem(self, num):
return self._memobj.memories[num - 1]
def _get_nam(self, number):
return self._memobj.names[number - 1]
def _set_mem(self, num):
return self._memobj.memories[num - 1]
def _set_nam(self, number):
return self._memobj.names[number - 1]
vhftx = [144000000, 146000000]
uhftx = [430000000, 440000000]
@directory.register
class RetevisRA85Radio(RadioddityGA510Radio):
VENDOR = 'Retevis'
MODEL = 'RA85'
POWER_LEVELS = [
chirp_common.PowerLevel('H', watts=5),
chirp_common.PowerLevel('L', watts=0.5),
chirp_common.PowerLevel('M', watts=0.6)]
_magic = b'PROGROMWLTU'
def get_features(self):
rf = RadioddityGA510Radio.get_features(self)
rf.memory_bounds = (1, 128)
rf.valid_bands = [(136000000, 174000000),
(400000000, 520000000)]
return rf
def _get_mem(self, num):
return self._memobj.memories[num - 1]
def _get_nam(self, number):
return self._memobj.names[number - 1]
def _set_mem(self, num):
return self._memobj.memories[num - 1]
def _set_nam(self, number):
return self._memobj.names[number - 1]
@directory.register
class TDH6Radio(RadioddityGA510Radio):
VENDOR = "TIDRADIO"
MODEL = "TD-H6"
def get_features(self):
rf = super().get_features()
rf.valid_bands = [(136000000, 174000000),
(400000000, 520000000)]
return rf
@directory.register
class Senhaix8800Radio(RadioddityGA510Radio):
"""Senhaix 8800"""
VENDOR = "SenhaiX"
MODEL = "8800"
POWER_LEVELS = [
chirp_common.PowerLevel('H', watts=5),
chirp_common.PowerLevel('L', watts=1)]
_mem_format = MODEL_SHX8800_FORMAT
_magic = b'PROGROMSHXU'
def get_features(self):
rf = super().get_features()
rf.valid_bands = rf.valid_bands + [AIRBAND]
rf.valid_modes = rf.valid_modes + ['AM']
return rf
def get_memory(self, number):
m = super().get_memory(number)
if chirp_common.in_range(m.freq, [AIRBAND]):
m.mode = 'AM'
return m
def validate_memory(self, mem):
msgs = []
if chirp_common.in_range(mem.freq, [AIRBAND]):
if not mem.mode == 'AM':
msgs.append(chirp_common.ValidationWarning(
_('Frequency in this range requires AM mode')))
if mem.duplex or mem.tmode:
msgs.append(chirp_common.ValidationError(
_('AM mode does not allow duplex or tone')))
elif not chirp_common.in_range(
mem.freq, [AIRBAND]) and mem.mode == 'AM':
msgs.append(chirp_common.ValidationWarning(
_('Frequency in this range must not be AM mode')))
return msgs + super().validate_memory(mem)
@directory.register
class RadioddityGS5BRadio(Senhaix8800Radio):
"""Radioddity GS-5B"""
VENDOR = "Radioddity"
MODEL = "GS-5B"
# NOTE: This was added as Signus originally in 18295675
@directory.register
class CignusXTR5Radio(Senhaix8800Radio):
"""Cignus XTR-5"""
VENDOR = "Cignus"
MODEL = "XTR-5"
@directory.register
class AnysecuAC580Radio(Senhaix8800Radio):
"""Anysecu AC-580"""
VENDOR = "Anysecu"
MODEL = "AC-580"
@directory.register
class AbbreeARF5Radio(RadioddityGA510Radio):
VENDOR = 'Abbree'
MODEL = 'AR-F5'
POWER_LEVELS = [
chirp_common.PowerLevel('H', watts=5),
chirp_common.PowerLevel('L', watts=1),
chirp_common.PowerLevel('M', watts=2)]
_magic = b'PROGROMWLTU'
def get_features(self):
rf = RadioddityGA510Radio.get_features(self)
rf.memory_bounds = (1, 128)
rf.valid_bands = [(136000000, 174000000),
(200000000, 300000000),
(300000000, 400000000),
(400000000, 520000000)]
return rf
def _get_mem(self, num):
return self._memobj.memories[num - 1]
def _get_nam(self, number):
return self._memobj.names[number - 1]
def _set_mem(self, num):
return self._memobj.memories[num - 1]
def _set_nam(self, number):
return self._memobj.names[number - 1]
@directory.register
@directory.detected_by(RadioddityGA510Radio)
class RadioddityGA510v2(baofeng_uv17.UV17):
"""Baofeng UV-17"""
VENDOR = "Radioddity"
MODEL = "GA-510"
VARIANT = "V2"
MODES = ["FM", "NFM"]
BLOCK_ORDER = [2, 4, 6, 16, 24]
BLOCK_O_READ = list(BLOCK_ORDER)
MEM_TOTAL = 0x6000
WRITE_MEM_TOTAL = 0x6000
BLOCK_SIZE = 0x40
BAUD_RATE = 57600
_idents = [b"PSEARCH"]
_magic_response_length = 8
_magics = [(b"PASSSTA", 3), (b"SYSINFO", 1),
(b"\x56\x00\x00\x0A\x0D", 13), (b"\x06", 1),
(b"\x56\x00\x10\x0A\x0D", 13), (b"\x06", 1),
(b"\x56\x00\x20\x0A\x0D", 13), (b"\x06", 1),
(b"\x56\x00\x00\x00\x0A", 11), (b"\x06", 1),
(b"\xFF\xFF\xFF\xFF\x0C\x44\x4d\x52\x31\x37\x30\x32", 1),
(b"\02", 8), (b"\x06", 1)]
_magic_memsize = []
_radio_memsize = 0x10000
_magics2 = []
_fingerprint = b"\x06DMR1702"
_scode_offset = 1
_tri_band = False
POWER_LEVELS = [chirp_common.PowerLevel("Low", watts=1.00),
chirp_common.PowerLevel("Medium", watts=5.00),
chirp_common.PowerLevel("High", watts=10.00)]
LENGTH_NAME = 8
SCODE_LIST = ["%s" % x for x in range(1, 16)]
SQUELCH_LIST = ["Off"] + list("123456789")
LIST_POWERON_DISPLAY_TYPE = ["Full", "Message", "Voltage"]
LIST_TIMEOUT = ["Off"] + ["%s sec" % x for x in range(15, 615, 15)]
LIST_VOICE = ["Chinese", "English"]
LIST_BACKLIGHT_TIMER = ["Always On"] + ["%s sec" % x for x in range(1, 11)]
LIST_MODE = ["Name", "Frequency"]
CHANNELS = 128
CHANNEL_DEF = """
struct channel {
lbcd rxfreq[4];
lbcd txfreq[4];
u8 unused1;
ul16 rxtone;
ul16 txtone;
u8 unknown1:1,
bcl:1,
pttid:2,
unknown2:1,
wide:1,
lowpower:2;
u8 scode:4,
unknown3:3,
scan:1;
u8 unknown4;
};
"""
MEM_LAYOUT = """
#seekto 0x1000;
struct settings settings;
#seekto 0x2000;
struct pttid pttid[15];
struct ani ani;
#seekto 0x3040;
struct {
struct channel mem[128];
} mem1;
#seekto 0x400B;
struct channelname names1[128];
"""
MEM_FORMAT = CHANNEL_DEF + baofeng_uv17.UV17.MEM_DEFS + MEM_LAYOUT
_has_workmode_support = False
chirp-20261030/chirp/drivers/generic_csv.py 0000644 0001750 0001750 00000046300 15215203722 017460 0 ustar hibby hibby # 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 io
import os
import csv
import logging
from chirp import chirp_common, errors, directory
LOG = logging.getLogger(__name__)
DEFAULT_POWER_LEVEL = chirp_common.AutoNamedPowerLevel(50)
class OmittedHeaderError(Exception):
"""Internal exception to signal that a column has been omitted"""
pass
def get_datum_by_header(headers, data, header):
"""Return the column corresponding to @headers[@header] from @data"""
if header not in headers:
raise OmittedHeaderError("Header %s not provided" % header)
try:
return data[headers.index(header)]
except IndexError:
raise OmittedHeaderError("Header %s not provided on this line" %
header)
def write_memory(writer, mem):
"""Write @mem using @writer if not empty"""
if mem.empty:
return
writer.writerow(mem.to_csv())
def parse_cross_mode(value):
if value not in chirp_common.CROSS_MODES:
raise ValueError('Invalid cross mode %r' % value)
return value
@directory.register
class CSVRadio(chirp_common.FileBackedRadio):
"""A driver for Generic CSV files"""
VENDOR = "Generic"
MODEL = "CSV"
NEEDS_COMPAT_SERIAL = True
FILE_EXTENSION = "csv"
FORMATS = [directory.register_format('CSV', '*.csv')]
SEPCHAR = ','
ATTR_MAP = {
"Location": (int, "number"),
"Name": (str, "name"),
"Frequency": (chirp_common.parse_freq, "freq"),
"Duplex": (str, "duplex"),
"Offset": (chirp_common.parse_freq, "offset"),
"Tone": (str, "tmode"),
"rToneFreq": (float, "rtone"),
"cToneFreq": (float, "ctone"),
"DtcsCode": (int, "dtcs"),
"DtcsPolarity": (str, "dtcs_polarity"),
"RxDtcsCode": (int, "rx_dtcs"),
"CrossMode": (parse_cross_mode, "cross_mode"),
"Mode": (str, "mode"),
"TStep": (float, "tuning_step"),
"Skip": (str, "skip"),
"Power": (chirp_common.parse_power, "power"),
"Comment": (str, "comment"),
}
def _blank(self, setDefault=False, max_memory=999):
self.errors = []
self.memories = [chirp_common.Memory(i, True)
for i in range(0, max_memory + 1)]
if (setDefault):
self.memories[0].empty = False
self.memories[0].freq = 146010000
# Default to 50W
self.memories[0].power = DEFAULT_POWER_LEVEL
def clear(self):
self.memories = []
def __init__(self, pipe, max_memory=999):
chirp_common.FileBackedRadio.__init__(self, None)
self.memories = []
self.file_has_rTone = None # Set in load(), used in _clean_tmode()
self.file_has_cTone = None
# Persistence for comment lines
# List of tuples of (previous_memory, comment)
self._comments = []
self._filename = pipe
if self._filename and os.path.exists(self._filename):
self.load()
else:
self._blank(True, max_memory=max_memory)
def get_features(self):
rf = chirp_common.RadioFeatures()
rf.has_bank = False
rf.requires_call_lists = False
rf.has_implicit_calls = False
rf.memory_bounds = (0, len(self.memories)-1)
rf.has_infinite_number = True
rf.has_nostep_tuning = True
rf.has_comment = True
rf.has_rx_dtcs = True
rf.has_variable_power = True
rf.can_odd_split = True
rf.valid_modes = list(chirp_common.MODES)
rf.valid_tmodes = list(chirp_common.TONE_MODES)
rf.valid_cross_modes = list(chirp_common.CROSS_MODES)
rf.valid_duplexes = ["", "-", "+", "split", "off"]
rf.valid_tuning_steps = list(chirp_common.TUNING_STEPS)
rf.valid_bands = [(1, 10000000000)]
rf.valid_skips = ["", "S"]
rf.valid_characters = chirp_common.CHARSET_1252
rf.valid_name_length = 999
rf.valid_power_levels = [chirp_common.AutoNamedPowerLevel(0.1),
DEFAULT_POWER_LEVEL,
chirp_common.AutoNamedPowerLevel(1500)]
return rf
def _clean(self, headers, line, mem):
"""Runs post-processing functions on new mem objects.
This is useful for parsing other CSV dialects when multiple columns
convert to a single Chirp column."""
for attr in dir(mem):
fname = "_clean_%s" % attr
if hasattr(self, fname):
mem = getattr(self, fname)(headers, line, mem)
return mem
def _clean_tmode(self, headers, line, mem):
""" If there is exactly one of [rToneFreq, cToneFreq] columns in the
csv file, use it for both rtone & ctone. Makes TSQL use friendlier."""
if self.file_has_rTone and not self.file_has_cTone:
mem.ctone = mem.rtone
elif self.file_has_cTone and not self.file_has_rTone:
mem.rtone = mem.ctone
return mem
def _parse_csv_data_line(self, headers, line):
mem = chirp_common.Memory()
try:
if get_datum_by_header(headers, line, "Mode") == "DV":
mem = chirp_common.DVMemory()
except OmittedHeaderError:
pass
for header in headers:
try:
typ, attr = self.ATTR_MAP[header]
except KeyError:
continue
try:
val = get_datum_by_header(headers, line, header)
if not val and typ == int:
val = None
else:
val = typ(val)
if hasattr(mem, attr):
setattr(mem, attr, val)
except OmittedHeaderError:
pass
except Exception as e:
raise Exception("[%s] %s" % (attr, e))
if not mem.power:
# Default power level to something if not set
mem.power = DEFAULT_POWER_LEVEL
return self._clean(headers, line, mem)
def load_from(self, string):
self._load(io.StringIO(string, newline=''))
def load(self, filename=None):
if filename is None and self._filename is None:
raise errors.RadioError("Need a location to load from")
if filename:
self._filename = filename
self._blank()
with open(self._filename, newline='', encoding='utf-8-sig') as f:
return self._load(f)
def _load(self, f):
reader = csv.reader(f, delimiter=self.SEPCHAR, quotechar='"')
self._comments = []
good = 0
lineno = 0
last_number = -1
for line in reader:
# Skip (but stash) comment lines that start with #
if line and line[0].startswith('#'):
self._comments.append((last_number, ' '.join(line)))
continue
lineno += 1
if lineno == 1:
header = line
for field in header:
# Log unknown header names for the UI to capture and expose
if field not in chirp_common.Memory.CSV_FORMAT:
LOG.error('Header line has unknown field %r' % field)
self.file_has_rTone = "rToneFreq" in header
self.file_has_cTone = "cToneFreq" in header
continue
# Spreadsheets like to omit trailing empty columns in TSV
if len(header) > len(line) and not self.SEPCHAR.isspace():
LOG.error("Line %i has %i columns, expected %i",
lineno, len(line), len(header))
self.errors.append("Column number mismatch on line %i" %
lineno)
continue
try:
mem = self._parse_csv_data_line(header, line)
if mem is None or mem.freq == 0:
LOG.debug('Line %i did not contain a valid memory',
lineno)
continue
if mem.number is None:
raise Exception("Invalid Location field" % lineno)
except Exception as e:
LOG.error("Line %i: %s", lineno, e)
self.errors.append("Line %i: %s" % (lineno, e))
continue
last_number = mem.number
self._grow(mem.number)
self.memories[mem.number] = mem
good += 1
if not good:
raise errors.InvalidDataError("No channels found")
def save(self, filename=None):
if filename is None and self._filename is None:
raise errors.RadioError("Need a location to save to")
if filename:
self._filename = filename
with open(self._filename, "w", newline='', encoding='utf-8') as f:
self._write_to(f)
def _write_to(self, f):
comments = list(self._comments)
writer = csv.writer(f, delimiter=self.SEPCHAR)
for index, comment in comments[:]:
if index >= 0:
break
writer.writerow([comment])
comments.pop(0)
writer.writerow(chirp_common.Memory.CSV_FORMAT)
for mem in self.memories:
for index, comment in comments[:]:
if index >= mem.number:
break
writer.writerow([comment])
comments.pop(0)
write_memory(writer, mem)
def as_string(self):
string = io.StringIO(newline='')
self._write_to(string)
return string.getvalue()
# MMAP compatibility
def save_mmap(self, filename):
return self.save(filename)
def load_mmap(self, filename):
return self.load(filename)
def get_memories(self, lo=0, hi=999):
return [x for x in self.memories if x.number >= lo and x.number <= hi]
def get_memory(self, number):
try:
return self.memories[number].dupe()
except:
raise errors.InvalidMemoryLocation("No such memory %s" % number)
def _grow(self, target):
delta = target - len(self.memories)
if delta < 0:
return
delta += 1
for i in range(len(self.memories), len(self.memories) + delta + 1):
mem = chirp_common.Memory()
mem.empty = True
mem.number = i
self.memories.append(mem)
def set_memory(self, newmem):
newmem = newmem.dupe()
# unset immutable to allow name rstrip
newmem.immutable = []
newmem.name = newmem.name.rstrip()
if newmem.power is None:
newmem.power = DEFAULT_POWER_LEVEL
else:
# Accept any power level because we are CSV, but convert it to
# the class that will str() into our desired format.
newmem.power = chirp_common.AutoNamedPowerLevel(
chirp_common.dBm_to_watts(float(newmem.power)))
self._grow(newmem.number)
self.memories[newmem.number] = newmem
def erase_memory(self, number):
mem = chirp_common.Memory()
mem.number = number
mem.empty = True
self.memories[number] = mem
def get_raw_memory(self, number):
return ",".join(chirp_common.Memory.CSV_FORMAT) + \
os.linesep + \
self.SEPCHAR.join(self.memories[number].to_csv())
@classmethod
def match_model(cls, filedata, filename):
"""Match files ending in .CSV"""
try:
filedata = filedata.decode()
except UnicodeDecodeError:
# CSV files are text
return False
return filename.lower().endswith("." + cls.FILE_EXTENSION) and \
(find_csv_header(filedata) or filedata == "")
def find_csv_header(filedata):
if filedata.startswith('\ufeff') or filedata.startswith('\ufffe'):
# Skip BOM
filedata = filedata[1:]
while filedata.startswith('#'):
filedata = filedata[filedata.find('\n') + 1:]
delims = ['', '"', "'"]
return any([filedata.startswith('%sLocation%s,' % (d, d))
for d in delims])
@directory.register
class CommanderCSVRadio(CSVRadio):
"""A driver for reading CSV files generated by KG-UV Commander software"""
VENDOR = "Commander"
MODEL = "KG-UV"
FILE_EXTENSION = "csv"
MODE_MAP = {
"NARR": "NFM",
"WIDE": "FM",
}
SCAN_MAP = {
"ON": "",
"OFF": "S"
}
ATTR_MAP = {
"#": (int, "number"),
"Name": (str, "name"),
"RX Freq": (chirp_common.parse_freq, "freq"),
"Scan": (lambda v: CommanderCSVRadio.SCAN_MAP.get(v), "skip"),
"TX Dev": (lambda v: CommanderCSVRadio.MODE_MAP.get(v), "mode"),
"Group/Notes": (str, "comment"),
}
def _clean_number(self, headers, line, mem):
if mem.number == 0:
for memory in self.memories:
if memory.empty:
mem.number = memory.number
break
return mem
def _clean_duplex(self, headers, line, mem):
try:
txfreq = chirp_common.parse_freq(
get_datum_by_header(headers, line, "TX Freq"))
except ValueError:
mem.duplex = "off"
return mem
if mem.freq == txfreq:
mem.duplex = ""
elif txfreq:
mem.duplex = "split"
mem.offset = txfreq
return mem
def _clean_tmode(self, headers, line, mem):
rtone = get_datum_by_header(headers, line, "Encode")
ctone = get_datum_by_header(headers, line, "Decode")
if rtone == "OFF":
rtone = None
else:
rtone = float(rtone)
if ctone == "OFF":
ctone = None
else:
ctone = float(ctone)
if rtone:
mem.tmode = "Tone"
if ctone:
mem.tmode = "TSQL"
mem.rtone = rtone or 88.5
mem.ctone = ctone or mem.rtone
return mem
@classmethod
def match_model(cls, filedata, filename):
"""Match files ending in .csv and using Commander column names."""
return filename.lower().endswith("." + cls.FILE_EXTENSION) and \
filedata.startswith(b"Name,RX Freq,TX Freq,Decode,Encode,TX Pwr,"
b"Scan,TX Dev,Busy Lck,Group/Notes") or \
filedata.startswith(b'"#","Name","RX Freq","TX Freq","Decode",'
b'"Encode","TX Pwr","Scan","TX Dev",'
b'"Busy Lck","Group/Notes"')
@directory.register
class RTCSVRadio(CSVRadio):
"""A driver for reading CSV files generated by RT Systems software"""
VENDOR = "RT Systems"
MODEL = "CSV"
FILE_EXTENSION = "csv"
DUPLEX_MAP = {
"Minus": "-",
"Plus": "+",
"Simplex": "",
"Split": "split",
}
SKIP_MAP = {
"Off": "",
"On": "S",
"P Scan": "P",
"Skip": "S",
"Scan": "",
}
TMODE_MAP = {
"None": "",
"T Sql": "TSQL",
}
BOOL_MAP = {
"Off": False,
"On": True,
}
MODE_MAP = {
'FM Narrow': 'NFM',
}
ATTR_MAP = {
"Channel Number": (int, "number"),
"Receive Frequency": (chirp_common.parse_freq, "freq"),
"Offset Frequency": (chirp_common.parse_freq, "offset"),
"Offset Direction": (lambda v:
RTCSVRadio.DUPLEX_MAP.get(v, v), "duplex"),
"Operating Mode": (lambda v: RTCSVRadio.MODE_MAP.get(v, v), 'mode'),
"Name": (str, "name"),
"Tone Mode": (lambda v:
RTCSVRadio.TMODE_MAP.get(v, v), "tmode"),
"CTCSS": (lambda v:
float(v.split(" ")[0]), "rtone"),
"DCS": (int, "dtcs"),
"Skip": (lambda v:
RTCSVRadio.SKIP_MAP.get(v, v), "skip"),
"Step": (lambda v:
float(v.split(" ")[0]), "tuning_step"),
"Mask": (lambda v:
RTCSVRadio.BOOL_MAP.get(v, v), "empty",),
"Comment": (str, "comment"),
}
def __init__(self, pipe):
self._last_loaded = 0
super().__init__(pipe)
def _clean_duplex(self, headers, line, mem):
if mem.duplex == "split":
try:
val = get_datum_by_header(headers, line, "Transmit Frequency")
val = chirp_common.parse_freq(val)
mem.offset = val
except OmittedHeaderError:
pass
return mem
def _clean_mode(self, headers, line, mem):
if mem.mode == "FM":
try:
val = get_datum_by_header(headers, line, "Half Dev")
if self.BOOL_MAP[val]:
mem.mode = "FMN"
except OmittedHeaderError:
pass
return mem
def _clean_ctone(self, headers, line, mem):
# RT Systems only stores a single tone value
mem.ctone = mem.rtone
return mem
def _clean_number(self, headers, line, mem):
if 'Channel Number' not in headers and self.memories:
# Some RTSystems software generates this with an empty header name?
self._last_loaded += 1
mem.number = self._last_loaded
LOG.debug('No location column, calculated %i from %r',
mem.number, self.memories[-1])
return mem
def _parse_csv_data_line(self, headers, line):
val = get_datum_by_header(headers, line, "Receive Frequency")
if not val.strip():
return
return super()._parse_csv_data_line(headers, line)
@classmethod
def match_model(cls, filedata, filename):
"""Match files ending in .csv and using RT Systems column names."""
# RT Systems provides a different set of columns for each radio.
# We attempt to match only the first few columns, hoping they are
# consistent across radio models.
try:
filedata = filedata.decode()
except UnicodeDecodeError:
# CSV files are text
return False
try:
firstline, rest = filedata.split('\n', 1)
firstline_fields = firstline.split(',')
except Exception as e:
LOG.warning('Failed to detect file as RTCSV: %s', e)
return False
return filename.lower().endswith("." + cls.FILE_EXTENSION) and \
'Receive Frequency' in firstline_fields
class TSVRadio(CSVRadio):
SEPCHAR = '\t'
chirp-20261030/chirp/drivers/generic_tpe.py 0000644 0001750 0001750 00000004017 14665027664 017475 0 ustar hibby hibby # 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 .
from chirp import chirp_common, directory
from chirp.drivers import generic_csv
@directory.register
class TpeRadio(generic_csv.CSVRadio):
"""Generic ARRL Travel Plus"""
VENDOR = "ARRL"
MODEL = "Travel Plus"
FILE_EXTENSION = "tpe"
FORMATS = [directory.register_format('ARRL TPE', '*.tpe')]
ATTR_MAP = {
"Sequence Number": (int, "number"),
"Location": (str, "comment"),
"Call Sign": (str, "name"),
"Output Frequency": (chirp_common.parse_freq, "freq"),
"Input Frequency": (str, "duplex"),
"CTCSS Tones": (lambda v: float(v)
if v and float(v) in chirp_common.TONES
else 88.5, "rtone"),
"Repeater Notes": (str, "comment"),
}
def _clean_tmode(self, headers, line, mem):
try:
val = generic_csv.get_datum_by_header(headers, line, "CTCSS Tones")
if val and float(val) in chirp_common.TONES:
mem.tmode = "Tone"
except generic_csv.OmittedHeaderError:
pass
return mem
def _clean_ctone(self, headers, line, mem):
# TPE only stores a single tone value
mem.ctone = mem.rtone
return mem
@classmethod
def match_model(cls, filedata, filename):
return filename.lower().endswith("." + cls.FILE_EXTENSION)
chirp-20261030/chirp/drivers/gmrsuv1.py 0000644 0001750 0001750 00000106074 15257672455 016625 0 ustar hibby hibby # Copyright 2016:
# * Jim Unroe KC9HI,
#
# 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 .
import logging
from chirp.drivers import baofeng_common as bfc
from chirp import chirp_common, directory
from chirp import bitwise
from chirp.settings import RadioSettingGroup, RadioSetting, \
RadioSettingValueBoolean, RadioSettingValueList, \
RadioSettingValueString, RadioSettingValueInteger, \
RadioSettingValueFloat, RadioSettings, \
InvalidValueError
LOG = logging.getLogger(__name__)
# #### MAGICS #########################################################
# BTECH GMRS-V1 magic string
MSTRING_GMRSV1 = b"\x50\x5F\x20\x15\x12\x15\x4D"
# #### ID strings #####################################################
# BTECH GMRS-V1
GMRSV1_fp1 = b"US32411" # original
GMRSV1_fp2 = b"US32416" # original
GMRSV1_fp3 = b"US32418" # new rules
GMRSV1_fp4 = b"US32412" # original
DTMF_CHARS = "0123456789 *#ABCD"
STEPS = [2.5, 5.0, 6.25, 10.0, 12.5, 20.0, 25.0, 50.0]
LIST_AB = ["A", "B"]
LIST_ALMOD = ["Off", "Site", "Tone", "Code"]
LIST_BANDWIDTH = ["Wide", "Narrow"]
LIST_COLOR = ["Off", "Blue", "Orange", "Purple"]
LIST_DTMFSPEED = ["%s ms" % x for x in range(50, 2010, 10)]
LIST_DTMFST = ["Off", "DT-ST", "ANI-ST", "DT+ANI"]
LIST_MODE = ["Channel", "Name", "Frequency"]
LIST_OFF1TO9 = ["Off"] + list("123456789")
LIST_OFF1TO10 = LIST_OFF1TO9 + ["10"]
LIST_OFFAB = ["Off"] + LIST_AB
LIST_RESUME = ["TO", "CO", "SE"]
LIST_PONMSG = ["Full", "Message"]
LIST_PTTID = ["Off", "BOT", "EOT", "Both"]
LIST_SCODE = ["%s" % x for x in range(1, 16)]
LIST_RPSTE = ["Off"] + ["%s" % x for x in range(1, 11)]
LIST_RTONE = ["1000 Hz", "1450 Hz", "1750 Hz", "2100 Hz"]
LIST_SAVE = ["Off", "1:1", "1:2", "1:3", "1:4"]
LIST_SHIFTD = ["Off", "+", "-"]
LIST_STEDELAY = ["Off"] + ["%s ms" % x for x in range(100, 1100, 100)]
LIST_STEP = [str(x) for x in STEPS]
LIST_TIMEOUT = ["%s sec" % x for x in range(15, 615, 15)]
LIST_TXPOWER = ["High", "Low"]
LIST_VOICE = ["Off", "English", "Chinese"]
LIST_WORKMODE = ["Frequency", "Channel"]
GMRS_FREQS1 = [462562500, 462587500, 462612500, 462637500, 462662500,
462687500, 462712500]
GMRS_FREQS2 = [467562500, 467587500, 467612500, 467637500, 467662500,
467687500, 467712500]
GMRS_FREQS3 = [462550000, 462575000, 462600000, 462625000, 462650000,
462675000, 462700000, 462725000]
GMRS_FREQS_ORIG = GMRS_FREQS1 + GMRS_FREQS3 * 2
GMRS_FREQS_2017 = GMRS_FREQS1 + GMRS_FREQS2 + GMRS_FREQS3 * 2
def model_match(cls, data):
"""Match the opened/downloaded image to the correct version"""
rid = data[0x1EF0:0x1EF7]
if rid in cls._fileid:
return True
return False
@directory.register
class GMRSV1(bfc.BaofengCommonHT):
"""BTech GMRS-V1"""
VENDOR = "BTECH"
MODEL = "GMRS-V1"
_fileid = [GMRSV1_fp4, GMRSV1_fp3, GMRSV1_fp2, GMRSV1_fp1, ]
_is_orig = [GMRSV1_fp2, GMRSV1_fp1, GMRSV1_fp4, ]
_magic = [MSTRING_GMRSV1, ]
_magic_response_length = 8
_fw_ver_start = 0x1EF0
_recv_block_size = 0x40
_mem_size = 0x2000
_ack_block = True
_ranges = [(0x0000, 0x0DF0),
(0x0E00, 0x1800),
(0x1EE0, 0x1EF0),
(0x1F60, 0x1F70),
(0x1F80, 0x1F90),
(0x1FC0, 0x1FD0)]
_send_block_size = 0x10
@classmethod
def match_model(cls, filedata, filename):
return len(filedata) == 0x2008 and model_match(cls, filedata)
MODES = ["NFM", "FM"]
VALID_CHARS = chirp_common.CHARSET_ALPHANUMERIC + \
"!@#$%^&*()+-=[]:\";'<>?,./"
LENGTH_NAME = 7
SKIP_VALUES = ["", "S"]
DTCS_CODES = tuple(sorted(chirp_common.DTCS_CODES + (645,)))
POWER_LEVELS = [chirp_common.PowerLevel("High", watts=5.00),
chirp_common.PowerLevel("Low", watts=2.00)]
VALID_BANDS = [(130000000, 180000000),
(400000000, 521000000)]
PTTID_LIST = LIST_PTTID
SCODE_LIST = LIST_SCODE
def get_features(self):
"""Get the radio's features"""
rf = chirp_common.RadioFeatures()
rf.has_settings = True
rf.has_bank = False
rf.has_tuning_step = False
rf.can_odd_split = False
rf.has_name = True
rf.has_offset = True
rf.has_mode = True
rf.has_dtcs = True
rf.has_rx_dtcs = True
rf.has_dtcs_polarity = True
rf.has_ctone = True
rf.has_cross = True
rf.valid_modes = self.MODES
rf.valid_characters = self.VALID_CHARS
rf.valid_name_length = self.LENGTH_NAME
rf.valid_duplexes = ["", "-", "+", "off"]
rf.valid_tmodes = ['', 'Tone', 'TSQL', 'DTCS', 'Cross']
rf.valid_cross_modes = [
"Tone->Tone",
"DTCS->",
"->DTCS",
"Tone->DTCS",
"DTCS->Tone",
"->Tone",
"DTCS->DTCS"]
rf.valid_skips = self.SKIP_VALUES
rf.valid_dtcs_codes = self.DTCS_CODES
rf.memory_bounds = (0, 127)
rf.valid_power_levels = self.POWER_LEVELS
rf.valid_bands = self.VALID_BANDS
rf.valid_tuning_steps = STEPS
return rf
MEM_FORMAT = """
// #seekto 0x0000;
struct {
lbcd rxfreq[4];
lbcd txfreq[4];
ul16 rxtone;
ul16 txtone;
u8 unknown0:4,
scode:4;
u8 unknown1;
u8 unknown2:7,
lowpower:1;
u8 unknown3:1,
wide:1,
unknown4:2,
bcl:1,
scan:1,
pttid:2;
} memory[128];
#seekto 0x0B00;
struct {
u8 code[5];
u8 unused[11];
} pttid[15];
#seekto 0x0C80;
struct {
u8 unknown222[5];
u8 unknown333[5];
u8 alarm[5];
u8 unknown1;
u8 unknown555[5];
u8 unknown666[5];
u8 unknown777[5];
u8 unknown2;
u8 unknown60606[5];
u8 unknown70707[5];
u8 code[5];
u8 unused1:6,
aniid:2;
u8 unknown[2];
u8 dtmfon;
u8 dtmfoff;
} ani;
#seekto 0x0E20;
struct {
u8 unused01:4,
squelch:4;
u8 unused02;
u8 unused03;
u8 unused04:5,
save:3;
u8 unused05:4,
vox:4;
u8 unused06;
u8 unused07:4,
abr:4;
u8 unused08:7,
tdr:1;
u8 unused09:7,
beep:1;
u8 unused10:2,
timeout:6;
u8 unused11[4];
u8 unused12:6,
voice:2;
u8 unused13;
u8 unused14:6,
dtmfst:2;
u8 unused15;
u8 unused16:6,
screv:2;
u8 unused17:6,
pttid:2;
u8 unused18:2,
pttlt:6;
u8 unused19:6,
mdfa:2;
u8 unused20:6,
mdfb:2;
u8 unused21;
u8 unused22:7,
sync:1;
u8 unused23[4];
u8 unused24:6,
wtled:2;
u8 unused25:6,
rxled:2;
u8 unused26:6,
txled:2;
u8 unused27:6,
almod:2;
u8 unused28:7,
dbptt:1;
u8 unused29:6,
tdrab:2;
u8 unused30:7,
ste:1;
u8 unused31:4,
rpste:4;
u8 unused32:4,
rptrl:4;
u8 unused33:7,
ponmsg:1;
u8 unused34:7,
roger:1;
u8 unused35:6,
rtone:2;
u8 unused36;
u8 unused37:6,
rogerrx:2;
u8 unused38;
u8 displayab:1,
unknown1:2,
fmradio:1,
alarm:1,
unknown2:1,
reset:1,
menu:1;
u8 unused39;
u8 workmode;
u8 keylock;
u8 cht;
} settings;
#seekto 0x0E76;
struct {
u8 unused1:1,
mrcha:7;
u8 unused2:1,
mrchb:7;
} wmchannel;
struct vfo {
u8 unknown0[8];
u8 freq[8];
u8 unknown1;
u8 offset[4];
u8 unknown2;
ul16 rxtone;
ul16 txtone;
u8 unused1:7,
band:1;
u8 unknown3;
u8 unused2:2,
sftd:2,
scode:4;
u8 unknown4;
u8 unused3:1,
step:3,
unused4:4;
u8 txpower:1,
widenarr:1,
unknown5:4,
txpower3:2;
};
#seekto 0x0F00;
struct {
struct vfo a;
struct vfo b;
} vfo;
#seekto 0x0F4E;
u16 fm_presets;
#seekto 0x1000;
struct {
char name[7];
u8 unknown1[9];
} names[128];
#seekto 0x1ED0;
struct {
char line1[7];
char line2[7];
} sixpoweron_msg;
#seekto 0x1EE0;
struct {
char line1[7];
char line2[7];
} poweron_msg;
#seekto 0x1EF0;
struct {
char line1[7];
char line2[7];
} firmware_msg;
struct squelch {
u8 sql0;
u8 sql1;
u8 sql2;
u8 sql3;
u8 sql4;
u8 sql5;
u8 sql6;
u8 sql7;
u8 sql8;
u8 sql9;
};
#seekto 0x1F60;
struct {
struct squelch vhf;
u8 unknown1[6];
u8 unknown2[16];
struct squelch uhf;
} squelch;
"""
@classmethod
def get_prompts(cls):
rp = chirp_common.RadioPrompts()
rp.experimental = \
('The BTech GMRS-V1 driver is a beta version.\n'
'\n'
'Please save an unedited copy of your first successful\n'
'download to a CHIRP Radio Images(*.img) file.'
)
rp.pre_download = _(
"Follow these instructions to download your info:\n"
"1 - Turn off your radio\n"
"2 - Connect your interface cable\n"
"3 - Turn on your radio\n"
"4 - Do the download of your radio data\n")
rp.pre_upload = _(
"Follow this instructions to upload your info:\n"
"1 - Turn off your radio\n"
"2 - Connect your interface cable\n"
"3 - Turn on your radio\n"
"4 - Do the upload of your radio data\n")
return rp
def process_mmap(self):
"""Process the mem map into the mem object"""
self._memobj = bitwise.parse(self.MEM_FORMAT, self._mmap)
def get_memory(self, number):
_mem = self._memobj.memory[number]
_nam = self._memobj.names[number]
mem = chirp_common.Memory()
mem.number = number
if _mem.get_raw()[:1] == b"\xff":
mem.empty = True
return mem
mem.freq = int(_mem.rxfreq) * 10
if _mem.txfreq.get_raw() == b"\xFF\xFF\xFF\xFF":
mem.duplex = "off"
mem.offset = 0
elif int(_mem.rxfreq) == int(_mem.txfreq):
mem.duplex = ""
mem.offset = 0
else:
mem.duplex = int(_mem.rxfreq) > int(_mem.txfreq) and "-" or "+"
mem.offset = abs(int(_mem.rxfreq) - int(_mem.txfreq)) * 10
for char in _nam.name:
if str(char) == "\xFF":
char = " " # The OEM software may have 0xFF mid-name
mem.name += str(char)
mem.name = mem.name.rstrip()
dtcs_pol = ["N", "N"]
if _mem.txtone in [0, 0xFFFF]:
txmode = ""
elif _mem.txtone >= 0x0258:
txmode = "Tone"
mem.rtone = int(_mem.txtone) / 10.0
elif _mem.txtone <= 0x0258:
txmode = "DTCS"
if _mem.txtone > 0x69:
index = _mem.txtone - 0x6A
dtcs_pol[0] = "R"
else:
index = _mem.txtone - 1
mem.dtcs = self.DTCS_CODES[index]
else:
LOG.warn("Bug: txtone is %04x" % _mem.txtone)
if _mem.rxtone in [0, 0xFFFF]:
rxmode = ""
elif _mem.rxtone >= 0x0258:
rxmode = "Tone"
mem.ctone = int(_mem.rxtone) / 10.0
elif _mem.rxtone <= 0x0258:
rxmode = "DTCS"
if _mem.rxtone >= 0x6A:
index = _mem.rxtone - 0x6A
dtcs_pol[1] = "R"
else:
index = _mem.rxtone - 1
mem.rx_dtcs = self.DTCS_CODES[index]
else:
LOG.warn("Bug: rxtone is %04x" % _mem.rxtone)
if txmode == "Tone" and not rxmode:
mem.tmode = "Tone"
elif txmode == rxmode and txmode == "Tone" and mem.rtone == mem.ctone:
mem.tmode = "TSQL"
elif txmode == rxmode and txmode == "DTCS" and mem.dtcs == mem.rx_dtcs:
mem.tmode = "DTCS"
elif rxmode or txmode:
mem.tmode = "Cross"
mem.cross_mode = "%s->%s" % (txmode, rxmode)
mem.dtcs_polarity = "".join(dtcs_pol)
if not _mem.scan:
mem.skip = "S"
levels = self.POWER_LEVELS
try:
mem.power = levels[_mem.lowpower]
except IndexError:
LOG.error("Radio reported invalid power level %s (in %s)" %
(_mem.power, levels))
mem.power = levels[0]
mem.mode = _mem.wide and "FM" or "NFM"
mem.extra = RadioSettingGroup("Extra", "extra")
rs = RadioSetting("bcl", "BCL",
RadioSettingValueBoolean(_mem.bcl))
mem.extra.append(rs)
rs = RadioSetting("pttid", "PTT ID",
RadioSettingValueList(self.PTTID_LIST,
current_index=_mem.pttid))
mem.extra.append(rs)
rs = RadioSetting("scode", "S-CODE",
RadioSettingValueList(self.SCODE_LIST,
current_index=_mem.scode))
mem.extra.append(rs)
immutable = []
# Original GMRS-V1 models (pre-2017 GMRS rules)
# line1 is a valid string, so safe to encode here
if str(self._memobj.firmware_msg.line1).encode() in self._is_orig:
if mem.freq in GMRS_FREQS_ORIG:
if mem.number >= 0 and mem.number <= 22:
GMRS_FREQ = GMRS_FREQS_ORIG[mem.number]
mem.freq = GMRS_FREQ
immutable = ["empty", "freq"]
if mem.number >= 0 and mem.number <= 6:
mem.duplex = ''
mem.offset = 0
immutable += ["duplex", "offset"]
elif mem.number >= 7 and mem.number <= 14:
mem.duplex = ''
mem.offset = 0
immutable += ["duplex", "offset"]
elif mem.number >= 15 and mem.number <= 22:
mem.duplex = '+'
mem.offset = 5000000
immutable += ["duplex", "offset"]
else:
if mem.number > 22:
mem.duplex = 'off'
immutable = ["duplex", "offset"]
# GMRS-V1 models supporting 2017 GMRS rules
else:
if mem.freq in GMRS_FREQS_2017:
if mem.number >= 1 and mem.number <= 30:
GMRS_FREQ = GMRS_FREQS_2017[mem.number - 1]
mem.freq = GMRS_FREQ
immutable = ["empty", "freq"]
if mem.number >= 1 and mem.number <= 7:
mem.duplex = ''
mem.offset = 0
immutable += ["duplex", "offset"]
elif mem.number >= 8 and mem.number <= 14:
mem.duplex = ''
mem.offset = 0
mem.mode = "NFM"
mem.power = self.POWER_LEVELS[1]
immutable += ["duplex", "offset", "mode", "power"]
elif mem.number >= 15 and mem.number <= 22:
mem.duplex = ''
mem.offset = 0
immutable += ["duplex", "offset"]
elif mem.number >= 23 and mem.number <= 30:
mem.duplex = '+'
mem.offset = 5000000
immutable += ["duplex", "offset"]
else:
if mem.number < 1 or mem.number > 30:
mem.duplex = 'off'
immutable = ["duplex", "offset"]
mem.immutable = immutable
return mem
def set_memory(self, mem):
_mem = self._memobj.memory[mem.number]
_nam = self._memobj.names[mem.number]
if mem.empty:
_mem.set_raw("\xff" * 16)
_nam.set_raw("\xff" * 16)
return
_mem.set_raw("\x00" * 16)
_mem.rxfreq = mem.freq / 10
if mem.duplex == "off":
for i in range(0, 4):
_mem.txfreq[i].set_raw("\xFF")
elif mem.duplex == "split":
_mem.txfreq = mem.offset / 10
elif mem.duplex == "+":
_mem.txfreq = (mem.freq + mem.offset) / 10
elif mem.duplex == "-":
_mem.txfreq = (mem.freq - mem.offset) / 10
else:
_mem.txfreq = mem.freq / 10
_namelength = self.get_features().valid_name_length
for i in range(_namelength):
try:
_nam.name[i] = mem.name[i]
except IndexError:
_nam.name[i] = "\xFF"
rxmode = txmode = ""
if mem.tmode == "Tone":
_mem.txtone = int(mem.rtone * 10)
_mem.rxtone = 0
elif mem.tmode == "TSQL":
_mem.txtone = int(mem.ctone * 10)
_mem.rxtone = int(mem.ctone * 10)
elif mem.tmode == "DTCS":
rxmode = txmode = "DTCS"
_mem.txtone = self.DTCS_CODES.index(mem.dtcs) + 1
_mem.rxtone = self.DTCS_CODES.index(mem.dtcs) + 1
elif mem.tmode == "Cross":
txmode, rxmode = mem.cross_mode.split("->", 1)
if txmode == "Tone":
_mem.txtone = int(mem.rtone * 10)
elif txmode == "DTCS":
_mem.txtone = self.DTCS_CODES.index(mem.dtcs) + 1
else:
_mem.txtone = 0
if rxmode == "Tone":
_mem.rxtone = int(mem.ctone * 10)
elif rxmode == "DTCS":
_mem.rxtone = self.DTCS_CODES.index(mem.rx_dtcs) + 1
else:
_mem.rxtone = 0
else:
_mem.rxtone = 0
_mem.txtone = 0
if txmode == "DTCS" and mem.dtcs_polarity[0] == "R":
_mem.txtone += 0x69
if rxmode == "DTCS" and mem.dtcs_polarity[1] == "R":
_mem.rxtone += 0x69
_mem.scan = mem.skip != "S"
_mem.wide = mem.mode == "FM"
if mem.power:
_mem.lowpower = self.POWER_LEVELS.index(mem.power)
else:
_mem.lowpower = 0
# extra settings
if len(mem.extra) > 0:
# there are setting, parse
for setting in mem.extra:
setattr(_mem, setting.get_name(), setting.value)
def get_settings(self):
"""Translate the bit in the mem_struct into settings in the UI"""
_mem = self._memobj
basic = RadioSettingGroup("basic", "Basic Settings")
advanced = RadioSettingGroup("advanced", "Advanced Settings")
other = RadioSettingGroup("other", "Other Settings")
work = RadioSettingGroup("work", "Work Mode Settings")
fm_preset = RadioSettingGroup("fm_preset", "FM Preset")
dtmfe = RadioSettingGroup("dtmfe", "DTMF Encode Settings")
service = RadioSettingGroup("service", "Service Settings")
top = RadioSettings(basic, advanced, other, work, fm_preset, dtmfe,
service)
# Basic settings
if _mem.settings.squelch > 0x09:
val = 0x00
else:
val = _mem.settings.squelch
rs = RadioSetting("settings.squelch", "Squelch",
RadioSettingValueList(
LIST_OFF1TO9, current_index=val))
basic.append(rs)
if _mem.settings.save > 0x04:
val = 0x00
else:
val = _mem.settings.save
rs = RadioSetting("settings.save", "Battery Saver",
RadioSettingValueList(
LIST_SAVE, current_index=val))
basic.append(rs)
if _mem.settings.vox > 0x0A:
val = 0x00
else:
val = _mem.settings.vox
rs = RadioSetting("settings.vox", "Vox",
RadioSettingValueList(
LIST_OFF1TO10, current_index=val))
basic.append(rs)
if _mem.settings.abr > 0x0A:
val = 0x00
else:
val = _mem.settings.abr
rs = RadioSetting("settings.abr", "Backlight Timeout",
RadioSettingValueList(
LIST_OFF1TO10, current_index=val))
basic.append(rs)
rs = RadioSetting("settings.tdr", "Dual Watch",
RadioSettingValueBoolean(_mem.settings.tdr))
basic.append(rs)
rs = RadioSetting("settings.beep", "Beep",
RadioSettingValueBoolean(_mem.settings.beep))
basic.append(rs)
if _mem.settings.timeout > 0x27:
val = 0x03
else:
val = _mem.settings.timeout
rs = RadioSetting("settings.timeout", "Timeout Timer",
RadioSettingValueList(
LIST_TIMEOUT, current_index=val))
basic.append(rs)
if _mem.settings.voice > 0x02:
val = 0x01
else:
val = _mem.settings.voice
rs = RadioSetting("settings.voice", "Voice Prompt",
RadioSettingValueList(
LIST_VOICE, current_index=val))
basic.append(rs)
rs = RadioSetting(
"settings.dtmfst", "DTMF Sidetone",
RadioSettingValueList(
LIST_DTMFST, current_index=_mem.settings.dtmfst))
basic.append(rs)
if _mem.settings.screv > 0x02:
val = 0x01
else:
val = _mem.settings.screv
rs = RadioSetting("settings.screv", "Scan Resume",
RadioSettingValueList(
LIST_RESUME, current_index=val))
basic.append(rs)
rs = RadioSetting(
"settings.pttid", "When to send PTT ID",
RadioSettingValueList(
LIST_PTTID, current_index=_mem.settings.pttid))
basic.append(rs)
if _mem.settings.pttlt > 0x32:
val = 0x05
else:
val = _mem.settings.pttlt
rs = RadioSetting("pttlt", "PTT ID Delay",
RadioSettingValueInteger(0, 50, val))
basic.append(rs)
rs = RadioSetting(
"settings.mdfa", "Display Mode (A)",
RadioSettingValueList(
LIST_MODE, current_index=_mem.settings.mdfa))
basic.append(rs)
rs = RadioSetting(
"settings.mdfb", "Display Mode (B)",
RadioSettingValueList(
LIST_MODE, current_index=_mem.settings.mdfb))
basic.append(rs)
rs = RadioSetting("settings.sync", "Sync A & B",
RadioSettingValueBoolean(_mem.settings.sync))
basic.append(rs)
rs = RadioSetting("settings.wtled", "Standby LED Color",
RadioSettingValueList(
LIST_COLOR, current_index=_mem.settings.wtled))
basic.append(rs)
rs = RadioSetting("settings.rxled", "RX LED Color",
RadioSettingValueList(
LIST_COLOR, current_index=_mem.settings.rxled))
basic.append(rs)
rs = RadioSetting("settings.txled", "TX LED Color",
RadioSettingValueList(
LIST_COLOR, current_index=_mem.settings.txled))
basic.append(rs)
val = _mem.settings.almod
rs = RadioSetting("settings.almod", "Alarm Mode",
RadioSettingValueList(
LIST_ALMOD, current_index=val))
basic.append(rs)
rs = RadioSetting("settings.dbptt", "Double PTT",
RadioSettingValueBoolean(_mem.settings.dbptt))
basic.append(rs)
if _mem.settings.tdrab > 0x02:
val = 0x00
else:
val = _mem.settings.tdrab
rs = RadioSetting("settings.tdrab", "Dual Watch TX Priority",
RadioSettingValueList(
LIST_OFFAB, current_index=val))
basic.append(rs)
rs = RadioSetting("settings.ste", "Squelch Tail Eliminate (HT to HT)",
RadioSettingValueBoolean(_mem.settings.ste))
basic.append(rs)
if _mem.settings.rpste > 0x0A:
val = 0x00
else:
val = _mem.settings.rpste
rs = RadioSetting("settings.rpste",
"Squelch Tail Eliminate (repeater)",
RadioSettingValueList(
LIST_RPSTE, current_index=val))
basic.append(rs)
if _mem.settings.rptrl > 0x0A:
val = 0x00
else:
val = _mem.settings.rptrl
rs = RadioSetting("settings.rptrl", "STE Repeater Delay",
RadioSettingValueList(
LIST_STEDELAY, current_index=val))
basic.append(rs)
rs = RadioSetting(
"settings.ponmsg", "Power-On Message",
RadioSettingValueList(
LIST_PONMSG, current_index=_mem.settings.ponmsg))
basic.append(rs)
rs = RadioSetting("settings.roger", "Roger Beep",
RadioSettingValueBoolean(_mem.settings.roger))
basic.append(rs)
rs = RadioSetting(
"settings.rtone", "Tone Burst Frequency",
RadioSettingValueList(
LIST_RTONE, current_index=_mem.settings.rtone))
basic.append(rs)
rs = RadioSetting("settings.rogerrx", "Roger Beep (RX)",
RadioSettingValueList(
LIST_OFFAB, current_index=_mem.settings.rogerrx))
basic.append(rs)
# Advanced settings
rs = RadioSetting("settings.reset", "RESET Menu",
RadioSettingValueBoolean(_mem.settings.reset))
advanced.append(rs)
rs = RadioSetting("settings.menu", "All Menus",
RadioSettingValueBoolean(_mem.settings.menu))
advanced.append(rs)
rs = RadioSetting("settings.fmradio", "Broadcast FM Radio",
RadioSettingValueBoolean(_mem.settings.fmradio))
advanced.append(rs)
rs = RadioSetting("settings.alarm", "Alarm Sound",
RadioSettingValueBoolean(_mem.settings.alarm))
advanced.append(rs)
# Other settings
def _filter(name):
filtered = ""
for char in str(name):
if char in chirp_common.CHARSET_ASCII:
filtered += char
else:
filtered += " "
return filtered
_msg = _mem.firmware_msg
val = RadioSettingValueString(0, 7, _filter(_msg.line1))
val.set_mutable(False)
rs = RadioSetting("firmware_msg.line1", "Firmware Message 1", val)
other.append(rs)
val = RadioSettingValueString(0, 7, _filter(_msg.line2))
val.set_mutable(False)
rs = RadioSetting("firmware_msg.line2", "Firmware Message 2", val)
other.append(rs)
_msg = _mem.sixpoweron_msg
val = RadioSettingValueString(0, 7, _filter(_msg.line1))
val.set_mutable(False)
rs = RadioSetting("sixpoweron_msg.line1", "6+Power-On Message 1", val)
other.append(rs)
val = RadioSettingValueString(0, 7, _filter(_msg.line2))
val.set_mutable(False)
rs = RadioSetting("sixpoweron_msg.line2", "6+Power-On Message 2", val)
other.append(rs)
_msg = _mem.poweron_msg
rs = RadioSetting("poweron_msg.line1", "Power-On Message 1",
RadioSettingValueString(
0, 7, _filter(_msg.line1)))
other.append(rs)
rs = RadioSetting("poweron_msg.line2", "Power-On Message 2",
RadioSettingValueString(
0, 7, _filter(_msg.line2)))
other.append(rs)
# Work mode settings
rs = RadioSetting("settings.displayab", "Display",
RadioSettingValueList(
LIST_AB, current_index=_mem.settings.displayab))
work.append(rs)
rs = RadioSetting("settings.workmode", "VFO/MR Mode",
RadioSettingValueList(
LIST_WORKMODE,
current_index=_mem.settings.workmode))
work.append(rs)
rs = RadioSetting("settings.keylock", "Keypad Lock",
RadioSettingValueBoolean(_mem.settings.keylock))
work.append(rs)
rs = RadioSetting("wmchannel.mrcha", "MR A Channel",
RadioSettingValueInteger(0, 127,
_mem.wmchannel.mrcha))
work.append(rs)
rs = RadioSetting("wmchannel.mrchb", "MR B Channel",
RadioSettingValueInteger(0, 127,
_mem.wmchannel.mrchb))
work.append(rs)
def my_validate(value):
value = chirp_common.parse_freq(value)
msg = ("Can't be less than %i.0000")
if value > 99000000 and value < 130 * 1000000:
raise InvalidValueError(msg % (130))
msg = ("Can't be between %i.9975-%i.0000")
if (179 + 1) * 1000000 <= value and value < 400 * 1000000:
raise InvalidValueError(msg % (179, 400))
msg = ("Can't be greater than %i.9975")
if value > 99000000 and value > (520 + 1) * 1000000:
raise InvalidValueError(msg % (520))
return chirp_common.format_freq(value)
def apply_freq(setting, obj):
value = chirp_common.parse_freq(str(setting.value)) / 10
for i in range(7, -1, -1):
obj.freq[i] = value % 10
value /= 10
val1a = RadioSettingValueString(0, 10,
bfc.bcd_decode_freq(_mem.vfo.a.freq))
val1a.set_validate_callback(my_validate)
rs = RadioSetting("vfo.a.freq", "VFO A Frequency", val1a)
rs.set_apply_callback(apply_freq, _mem.vfo.a)
work.append(rs)
val1b = RadioSettingValueString(0, 10,
bfc.bcd_decode_freq(_mem.vfo.b.freq))
val1b.set_validate_callback(my_validate)
rs = RadioSetting("vfo.b.freq", "VFO B Frequency", val1b)
rs.set_apply_callback(apply_freq, _mem.vfo.b)
work.append(rs)
rs = RadioSetting("vfo.a.step", "VFO A Tuning Step",
RadioSettingValueList(
LIST_STEP, current_index=_mem.vfo.a.step))
work.append(rs)
rs = RadioSetting("vfo.b.step", "VFO B Tuning Step",
RadioSettingValueList(
LIST_STEP, current_index=_mem.vfo.b.step))
work.append(rs)
# broadcast FM settings
_fm_presets = self._memobj.fm_presets
if _fm_presets <= 108.0 * 10 - 650:
preset = _fm_presets / 10.0 + 65
elif _fm_presets >= 65.0 * 10 and _fm_presets <= 108.0 * 10:
preset = _fm_presets / 10.0
else:
preset = 76.0
rs = RadioSetting("fm_presets", "FM Preset(MHz)",
RadioSettingValueFloat(65, 108.0, preset, 0.1, 1))
fm_preset.append(rs)
# DTMF settings
def apply_code(setting, obj, length):
code = []
for j in range(0, length):
try:
code.append(DTMF_CHARS.index(str(setting.value)[j]))
except IndexError:
code.append(0xFF)
obj.code = code
for i in range(0, 15):
_codeobj = self._memobj.pttid[i].code
_code = "".join([DTMF_CHARS[x] for x in _codeobj if int(x) < 0x1F])
val = RadioSettingValueString(0, 5, _code, False)
val.set_charset(DTMF_CHARS)
pttid = RadioSetting("pttid/%i.code" % i,
"Signal Code %i" % (i + 1), val)
pttid.set_apply_callback(apply_code, self._memobj.pttid[i], 5)
dtmfe.append(pttid)
if _mem.ani.dtmfon > 0xC3:
val = 0x03
else:
val = _mem.ani.dtmfon
rs = RadioSetting("ani.dtmfon", "DTMF Speed (on)",
RadioSettingValueList(LIST_DTMFSPEED,
current_index=val))
dtmfe.append(rs)
if _mem.ani.dtmfoff > 0xC3:
val = 0x03
else:
val = _mem.ani.dtmfoff
rs = RadioSetting("ani.dtmfoff", "DTMF Speed (off)",
RadioSettingValueList(LIST_DTMFSPEED,
current_index=val))
dtmfe.append(rs)
_codeobj = self._memobj.ani.code
_code = "".join([DTMF_CHARS[x] for x in _codeobj if int(x) < 0x1F])
val = RadioSettingValueString(0, 5, _code, False)
val.set_charset(DTMF_CHARS)
rs = RadioSetting("ani.code", "ANI Code", val)
rs.set_apply_callback(apply_code, self._memobj.ani, 5)
dtmfe.append(rs)
rs = RadioSetting("ani.aniid", "When to send ANI ID",
RadioSettingValueList(LIST_PTTID,
current_index=_mem.ani.aniid))
dtmfe.append(rs)
def apply_alarmcode(setting, obj, length):
code = []
for j in range(0, length):
try:
code.append(DTMF_CHARS.index(str(setting.value)[j]))
except IndexError:
code.append(0xFF)
obj.alarm = code
_codeobj = self._memobj.ani.alarm
_code = "".join([DTMF_CHARS[x] for x in _codeobj if int(x) < 0x1F])
val = RadioSettingValueString(0, 5, _code, False)
val.set_charset(DTMF_CHARS)
rs = RadioSetting("ani.alarm", "Alarm Code", val)
rs.set_apply_callback(apply_alarmcode, self._memobj.ani, 5)
dtmfe.append(rs)
# Service settings
for band in ["vhf", "uhf"]:
for index in range(0, 10):
key = "squelch.%s.sql%i" % (band, index)
if band == "vhf":
_obj = self._memobj.squelch.vhf
elif band == "uhf":
_obj = self._memobj.squelch.uhf
val = RadioSettingValueInteger(0, 123,
getattr(_obj, "sql%i" % (
index)))
if index == 0:
val.set_mutable(False)
name = "%s Squelch %i" % (band.upper(), index)
rs = RadioSetting(key, name, val)
service.append(rs)
return top
chirp-20261030/chirp/drivers/gmrsv2.py 0000644 0001750 0001750 00000107102 15264365036 016422 0 ustar hibby hibby # Copyright 2022:
# * Jim Unroe KC9HI,
#
# 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 .
import logging
from chirp.drivers import baofeng_common as bfc
from chirp import chirp_common, directory
from chirp import bitwise
from chirp import bandplan_na
from chirp.settings import RadioSettingGroup, RadioSetting, \
RadioSettingValueBoolean, RadioSettingValueList, \
RadioSettingValueString, RadioSettingValueInteger, \
RadioSettingValueFloat, RadioSettings, \
InvalidValueError
LOG = logging.getLogger(__name__)
# #### MAGICS #########################################################
# BTECH GMRS-V2 magic string
MSTRING_GMRSV2 = b"\x50\x5F\x20\x21\x12\x15\x4D"
# BTECH MURS-V2 magic string
MSTRING_MURSV2 = b"\x50\x5F\x20\x21\x12\x15\x4D"
# #### ID strings #####################################################
# BTECH GMRS-V2
GMRSV2_fp1 = b"US32419" # original
# BTECH MURS-V2
MURSV2_fp1 = b"USM2403" # original
DTMF_CHARS = "0123456789 *#ABCD"
STEPS = [2.5, 5.0, 6.25, 10.0, 12.5, 20.0, 25.0, 50.0]
LIST_AB = ["A", "B"]
LIST_ALMOD = ["Off", "Site", "Tone", "Code"]
LIST_BANDWIDTH = ["Wide", "Narrow"]
LIST_COLOR = ["Off", "Blue", "Orange", "Purple"]
LIST_DTMFSPEED = ["%s ms" % x for x in range(50, 2010, 10)]
LIST_DTMFST = ["Off", "DT-ST", "ANI-ST", "DT+ANI"]
LIST_MODE = ["Channel", "Name", "Frequency"]
LIST_OFF1TO9 = ["Off"] + list("123456789")
LIST_OFF1TO10 = LIST_OFF1TO9 + ["10"]
LIST_OFFAB = ["Off"] + LIST_AB
LIST_RESUME = ["TO", "CO", "SE"]
LIST_PONMSG = ["Full", "Message"]
LIST_PTTID = ["Off", "BOT", "EOT", "Both"]
LIST_SCODE = ["%s" % x for x in range(1, 16)]
LIST_RPSTE = ["Off"] + ["%s" % x for x in range(1, 11)]
LIST_RTONE = ["1000 Hz", "1450 Hz", "1750 Hz", "2100 Hz"]
LIST_SAVE = ["Off", "1:1", "1:2", "1:3", "1:4"]
LIST_SHIFTD = ["Off", "+", "-"]
LIST_STEDELAY = ["Off"] + ["%s ms" % x for x in range(100, 1100, 100)]
LIST_STEP = [str(x) for x in STEPS]
LIST_TIMEOUT = ["%s sec" % x for x in range(15, 615, 15)]
LIST_TXPOWER = ["High", "Low"]
LIST_VOICE = ["Off", "English", "Chinese"]
LIST_WORKMODE = ["Frequency", "Channel"]
class GMRSV2base(bfc.BaofengCommonHT):
"""BTech GMRS-V2base"""
VENDOR = ""
MODEL = ""
_fw_ver_start = 0x1EF0
_recv_block_size = 0x40
_mem_size = 0x2000
_ack_block = True
_ranges = [(0x0000, 0x1EF0),
(0x1F60, 0x1F70),
(0x1F80, 0x1F90),
(0x1FC0, 0x1FD0)]
_send_block_size = 0x10
MODES = ["NFM", "FM"]
VALID_CHARS = chirp_common.CHARSET_ALPHANUMERIC + \
"!@#$%^&*()+-=[]:\";'<>?,./"
LENGTH_NAME = 7
SKIP_VALUES = ["", "S"]
DTCS_CODES = tuple(sorted(chirp_common.DTCS_CODES + (645,)))
POWER_LEVELS = [chirp_common.PowerLevel("High", watts=5.00),
chirp_common.PowerLevel("Low", watts=0.50)]
VALID_BANDS = [(130000000, 180000000),
(400000000, 521000000)]
PTTID_LIST = LIST_PTTID
SCODE_LIST = LIST_SCODE
def get_features(self):
"""Get the radio's features"""
rf = chirp_common.RadioFeatures()
rf.has_settings = True
rf.has_bank = False
rf.has_tuning_step = False
rf.can_odd_split = False
rf.has_name = True
rf.has_offset = True
rf.has_mode = True
rf.has_dtcs = True
rf.has_rx_dtcs = True
rf.has_dtcs_polarity = True
rf.has_ctone = True
rf.has_cross = True
rf.valid_modes = self.MODES
rf.valid_characters = self.VALID_CHARS
rf.valid_name_length = self.LENGTH_NAME
rf.valid_duplexes = ["", "+", "off"]
rf.valid_tmodes = ['', 'Tone', 'TSQL', 'DTCS', 'Cross']
rf.valid_cross_modes = [
"Tone->Tone",
"DTCS->",
"->DTCS",
"Tone->DTCS",
"DTCS->Tone",
"->Tone",
"DTCS->DTCS"]
rf.valid_skips = self.SKIP_VALUES
rf.valid_dtcs_codes = self.DTCS_CODES
rf.memory_bounds = (0, 199)
rf.valid_power_levels = self.POWER_LEVELS
rf.valid_bands = self.VALID_BANDS
rf.valid_tuning_steps = STEPS
return rf
MEM_FORMAT = """
// #seekto 0x0000;
struct {
lbcd rxfreq[4];
lbcd txfreq[4];
ul16 rxtone;
ul16 txtone;
u8 unknown0:4,
scode:4;
u8 unknown1;
u8 unknown2:7,
lowpower:1;
u8 unknown3:1,
wide:1,
unknown4:2,
bcl:1,
scan:1,
pttid:2;
} memory[200];
#seekto 0x0D00;
struct {
u8 code[5];
u8 unused[11];
} pttid[15];
// #seekto 0x0DF0;
struct {
u8 dtmfon;
u8 dtmfoff;
u8 unknown1:4,
unknown2:2,
aniid:2;
u8 alarm[5];
u8 code[5];
} ani;
#seekto 0x0E20;
struct {
u8 unused01:4,
squelch:4;
u8 unused02;
u8 unused03;
u8 unused04:5,
save:3;
u8 unused05:4,
vox:4;
u8 unused06;
u8 unused07:4,
abr:4;
u8 unused08:7,
tdr:1;
u8 unused09:7,
beep:1;
u8 unused10:2,
timeout:6;
u8 unused11[4];
u8 unused12:6,
voice:2;
u8 unused13;
u8 unused14:6,
dtmfst:2;
u8 unused15;
u8 unused16:6,
screv:2;
u8 unused17;
u8 unused18:2,
pttlt:6;
u8 unused19:6,
mdfa:2;
u8 unused20:6,
mdfb:2;
u8 unused21;
u8 unused22:7,
sync:1;
u8 unused23[4];
u8 unused24:6,
wtled:2;
u8 unused25:6,
rxled:2;
u8 unused26:6,
txled:2;
u8 unused27:6,
almod:2;
u8 unused28:7,
dbptt:1;
u8 unused29:6,
tdrab:2;
u8 unused30:7,
ste:1;
u8 unused31:4,
rpste:4;
u8 unused32:4,
rptrl:4;
u8 unused33:7,
ponmsg:1;
u8 unused34:7,
roger:1;
u8 unused35:6,
rtone:2;
u8 unused36;
u8 unused37:6,
rogerrx:2;
u8 unused38;
u8 displayab:1,
unknown1:7;
u8 unused39[2];
u8 keylock;
u8 cht;
u8 unused40[5];
u8 unknown3:3,
fmradio:1,
alarm:1,
unknown2:1,
reset:1,
menu:1;
u8 unused41;
u8 unknown4:7,
workmode:1;
} settings;
#seekto 0x0E76;
struct {
u8 unused1:1,
mrcha:7;
u8 unused2:1,
mrchb:7;
} wmchannel;
struct vfo {
u8 unknown0[8];
u8 freq[8];
u8 unknown1;
u8 offset[4];
u8 unknown2;
ul16 rxtone;
ul16 txtone;
u8 unused1:7,
band:1;
u8 unknown3;
u8 unused2:2,
sftd:2,
unused5:4;
u8 unknown4;
u8 unused3:1,
step:3,
unused4:4;
u8 txpower:1,
widenarr:1,
unknown5:4,
txpower3:2;
};
#seekto 0x0F00;
struct {
struct vfo a;
struct vfo b;
} vfo;
#seekto 0x0F4E;
u16 fm_presets;
#seekto 0x1000;
struct {
char name[7];
u8 unknown1[9];
} names[200];
#seekto 0x1ED0;
struct {
char line1[7];
char line2[7];
} sixpoweron_msg;
#seekto 0x1EE0;
struct {
char line1[7];
char line2[7];
} poweron_msg;
#seekto 0x1EF0;
struct {
char line1[7];
char line2[7];
} firmware_msg;
struct squelch {
u8 sql0;
u8 sql1;
u8 sql2;
u8 sql3;
u8 sql4;
u8 sql5;
u8 sql6;
u8 sql7;
u8 sql8;
u8 sql9;
};
#seekto 0x1F60;
struct {
struct squelch vhf;
u8 unknown1[6];
u8 unknown2[16];
struct squelch uhf;
} squelch;
"""
@classmethod
def get_prompts(cls):
rp = chirp_common.RadioPrompts()
rp.experimental = \
('The BTech ' + cls.MODEL + ' driver is a beta version.\n'
'\n'
'Please save an unedited copy of your first successful\n'
'download to a CHIRP Radio Images(*.img) file.'
)
rp.pre_download = _(
"Follow these instructions to download your info:\n"
"1 - Turn off your radio\n"
"2 - Connect your interface cable\n"
"3 - Turn on your radio\n"
"4 - Do the download of your radio data\n")
rp.pre_upload = _(
"Follow this instructions to upload your info:\n"
"1 - Turn off your radio\n"
"2 - Connect your interface cable\n"
"3 - Turn on your radio\n"
"4 - Do the upload of your radio data\n")
return rp
def process_mmap(self):
"""Process the mem map into the mem object"""
self._memobj = bitwise.parse(self.MEM_FORMAT, self._mmap)
def get_memory(self, number):
_mem = self._memobj.memory[number]
_nam = self._memobj.names[number]
mem = chirp_common.Memory()
mem.number = number
if _mem.get_raw()[:1] == b"\xff":
mem.empty = True
return mem
mem.freq = int(_mem.rxfreq) * 10
if self._is_txinh(_mem):
# TX freq not set
mem.duplex = "off"
mem.offset = 0
else:
# TX freq set
offset = (int(_mem.txfreq) * 10) - mem.freq
if offset != 0:
if offset > 0:
mem.duplex = "+"
mem.offset = 5000000
else:
mem.duplex = ""
mem.offset = 0
for char in _nam.name:
if str(char) == "\xFF":
char = " " # The OEM software may have 0xFF mid-name
mem.name += str(char)
mem.name = mem.name.rstrip()
dtcs_pol = ["N", "N"]
if _mem.txtone in [0, 0xFFFF]:
txmode = ""
elif _mem.txtone >= 0x0258:
txmode = "Tone"
mem.rtone = int(_mem.txtone) / 10.0
elif _mem.txtone <= 0x0258:
txmode = "DTCS"
if _mem.txtone > 0x69:
index = _mem.txtone - 0x6A
dtcs_pol[0] = "R"
else:
index = _mem.txtone - 1
mem.dtcs = self.DTCS_CODES[index]
else:
LOG.warn("Bug: txtone is %04x" % _mem.txtone)
if _mem.rxtone in [0, 0xFFFF]:
rxmode = ""
elif _mem.rxtone >= 0x0258:
rxmode = "Tone"
mem.ctone = int(_mem.rxtone) / 10.0
elif _mem.rxtone <= 0x0258:
rxmode = "DTCS"
if _mem.rxtone >= 0x6A:
index = _mem.rxtone - 0x6A
dtcs_pol[1] = "R"
else:
index = _mem.rxtone - 1
mem.rx_dtcs = self.DTCS_CODES[index]
else:
LOG.warn("Bug: rxtone is %04x" % _mem.rxtone)
if txmode == "Tone" and not rxmode:
mem.tmode = "Tone"
elif txmode == rxmode and txmode == "Tone" and mem.rtone == mem.ctone:
mem.tmode = "TSQL"
elif txmode == rxmode and txmode == "DTCS" and mem.dtcs == mem.rx_dtcs:
mem.tmode = "DTCS"
elif rxmode or txmode:
mem.tmode = "Cross"
mem.cross_mode = "%s->%s" % (txmode, rxmode)
mem.dtcs_polarity = "".join(dtcs_pol)
if not _mem.scan:
mem.skip = "S"
levels = self.POWER_LEVELS
try:
mem.power = levels[_mem.lowpower]
except IndexError:
LOG.error("Radio reported invalid power level %s (in %s)" %
(_mem.power, levels))
mem.power = levels[0]
mem.mode = _mem.wide and "FM" or "NFM"
mem.extra = RadioSettingGroup("Extra", "extra")
rs = RadioSetting("bcl", "BCL",
RadioSettingValueBoolean(_mem.bcl))
mem.extra.append(rs)
rs = RadioSetting("pttid", "PTT ID",
RadioSettingValueList(self.PTTID_LIST,
current_index=_mem.pttid))
mem.extra.append(rs)
rs = RadioSetting("scode", "S-CODE",
RadioSettingValueList(self.SCODE_LIST,
current_index=_mem.scode))
mem.extra.append(rs)
immutable = []
if self.MODEL == "GMRS-V2":
if mem.freq in bandplan_na.ALL_GMRS_FREQS:
if mem.freq in bandplan_na.GMRS_LOW:
# Non-repeater GMRS channels (limit duplex)
mem.duplex = ''
mem.offset = 0
immutable = ["duplex", "offset"]
elif mem.freq in bandplan_na.GMRS_HHONLY:
# Non-repeater GMRS hand-held only channels
# (limit duplex, power)
mem.duplex = ''
mem.offset = 0
mem.mode = "NFM"
mem.power = self.POWER_LEVELS[1]
immutable = ["duplex", "offset", "mode", "power"]
elif mem.freq in bandplan_na.GMRS_HIRPT:
# GMRS repeater channels, always either simplex or +5 MHz
if mem.duplex == '':
mem.offset = 0
if mem.duplex == '+':
mem.offset = 5000000
else:
# Not a GMRS channel, so restrict duplex since it will be
# forced to off.
if mem.freq not in bandplan_na.ALL_GMRS_FREQS:
mem.duplex = 'off'
mem.offset = 0
immutable = ["duplex", "offset"]
if self.MODEL == "MURS-V2":
if mem.freq in bandplan_na.ALL_MURS_FREQS:
if mem.freq in bandplan_na.MURS_NFM:
# Narrow only MURS channels (limit mode)
mem.duplex = ''
mem.offset = 0
mem.mode = "NFM"
immutable = ["duplex", "offset", "mode"]
if mem.freq in bandplan_na.MURS_FM:
# Narrow or wide MURS channels
mem.duplex = ''
mem.offset = 0
immutable = ["duplex", "offset"]
else:
# Not a MURS channel, so restrict duplex since it will be
# forced to off.
if mem.freq not in bandplan_na.ALL_MURS_FREQS:
mem.duplex = 'off'
mem.offset = 0
immutable = ["duplex", "offset"]
mem.immutable = immutable
return mem
def check_set_memory_immutable_policy(self, existing, new):
existing.immutable = []
super().check_set_memory_immutable_policy(existing, new)
def set_memory(self, mem):
_mem = self._memobj.memory[mem.number]
_nam = self._memobj.names[mem.number]
if mem.empty:
_mem.set_raw("\xff" * 16)
_nam.set_raw("\xff" * 16)
return
_mem.set_raw("\x00" * 16)
_mem.rxfreq = mem.freq / 10
if mem.duplex == "off":
for i in range(0, 4):
_mem.txfreq[i].set_raw("\xFF")
elif mem.duplex == "+":
offset = 5000000
_mem.txfreq = (mem.freq + offset) / 10
else:
_mem.txfreq = mem.freq / 10
_namelength = self.get_features().valid_name_length
for i in range(_namelength):
try:
_nam.name[i] = mem.name[i]
except IndexError:
_nam.name[i] = "\xFF"
rxmode = txmode = ""
if mem.tmode == "Tone":
_mem.txtone = int(mem.rtone * 10)
_mem.rxtone = 0
elif mem.tmode == "TSQL":
_mem.txtone = int(mem.ctone * 10)
_mem.rxtone = int(mem.ctone * 10)
elif mem.tmode == "DTCS":
rxmode = txmode = "DTCS"
_mem.txtone = self.DTCS_CODES.index(mem.dtcs) + 1
_mem.rxtone = self.DTCS_CODES.index(mem.dtcs) + 1
elif mem.tmode == "Cross":
txmode, rxmode = mem.cross_mode.split("->", 1)
if txmode == "Tone":
_mem.txtone = int(mem.rtone * 10)
elif txmode == "DTCS":
_mem.txtone = self.DTCS_CODES.index(mem.dtcs) + 1
else:
_mem.txtone = 0
if rxmode == "Tone":
_mem.rxtone = int(mem.ctone * 10)
elif rxmode == "DTCS":
_mem.rxtone = self.DTCS_CODES.index(mem.rx_dtcs) + 1
else:
_mem.rxtone = 0
else:
_mem.rxtone = 0
_mem.txtone = 0
if txmode == "DTCS" and mem.dtcs_polarity[0] == "R":
_mem.txtone += 0x69
if rxmode == "DTCS" and mem.dtcs_polarity[1] == "R":
_mem.rxtone += 0x69
_mem.scan = mem.skip != "S"
_mem.wide = mem.mode == "FM"
if mem.power:
_mem.lowpower = self.POWER_LEVELS.index(mem.power)
else:
_mem.lowpower = 0
# extra settings
if len(mem.extra) > 0:
# there are setting, parse
for setting in mem.extra:
setattr(_mem, setting.get_name(), setting.value)
else:
# there are no extra settings, load defaults
_mem.bcl = 0
_mem.pttid = 0
_mem.scode = 0
def get_settings(self):
"""Translate the bit in the mem_struct into settings in the UI"""
_mem = self._memobj
basic = RadioSettingGroup("basic", "Basic Settings")
advanced = RadioSettingGroup("advanced", "Advanced Settings")
other = RadioSettingGroup("other", "Other Settings")
work = RadioSettingGroup("work", "Work Mode Settings")
fm_preset = RadioSettingGroup("fm_preset", "FM Preset")
dtmfe = RadioSettingGroup("dtmfe", "DTMF Encode Settings")
service = RadioSettingGroup("service", "Service Settings")
top = RadioSettings(basic, advanced, other, work, fm_preset, dtmfe,
service)
# Basic settings
if _mem.settings.squelch > 0x09:
val = 0x00
else:
val = _mem.settings.squelch
rs = RadioSetting("settings.squelch", "Squelch",
RadioSettingValueList(
LIST_OFF1TO9, current_index=val))
basic.append(rs)
if _mem.settings.save > 0x04:
val = 0x00
else:
val = _mem.settings.save
rs = RadioSetting("settings.save", "Battery Saver",
RadioSettingValueList(
LIST_SAVE, current_index=val))
basic.append(rs)
if _mem.settings.vox > 0x0A:
val = 0x00
else:
val = _mem.settings.vox
rs = RadioSetting("settings.vox", "Vox",
RadioSettingValueList(
LIST_OFF1TO10, current_index=val))
basic.append(rs)
if _mem.settings.abr > 0x0A:
val = 0x00
else:
val = _mem.settings.abr
rs = RadioSetting("settings.abr", "Backlight Timeout",
RadioSettingValueList(
LIST_OFF1TO10, current_index=val))
basic.append(rs)
rs = RadioSetting("settings.tdr", "Dual Watch",
RadioSettingValueBoolean(_mem.settings.tdr))
basic.append(rs)
rs = RadioSetting("settings.beep", "Beep",
RadioSettingValueBoolean(_mem.settings.beep))
basic.append(rs)
if _mem.settings.timeout > 0x27:
val = 0x03
else:
val = _mem.settings.timeout
rs = RadioSetting("settings.timeout", "Timeout Timer",
RadioSettingValueList(
LIST_TIMEOUT, current_index=val))
basic.append(rs)
if _mem.settings.voice > 0x02:
val = 0x01
else:
val = _mem.settings.voice
rs = RadioSetting("settings.voice", "Voice Prompt",
RadioSettingValueList(
LIST_VOICE, current_index=val))
basic.append(rs)
rs = RadioSetting(
"settings.dtmfst", "DTMF Sidetone",
RadioSettingValueList(
LIST_DTMFST, current_index=_mem.settings.dtmfst))
basic.append(rs)
if _mem.settings.screv > 0x02:
val = 0x01
else:
val = _mem.settings.screv
rs = RadioSetting("settings.screv", "Scan Resume",
RadioSettingValueList(
LIST_RESUME, current_index=val))
basic.append(rs)
if _mem.settings.pttlt > 0x32:
val = 0x05
else:
val = _mem.settings.pttlt
rs = RadioSetting("pttlt", "PTT ID Delay",
RadioSettingValueInteger(0, 50, val))
basic.append(rs)
rs = RadioSetting(
"settings.mdfa", "Display Mode (A)",
RadioSettingValueList(
LIST_MODE, current_index=_mem.settings.mdfa))
basic.append(rs)
rs = RadioSetting(
"settings.mdfb", "Display Mode (B)",
RadioSettingValueList(
LIST_MODE, current_index=_mem.settings.mdfb))
basic.append(rs)
rs = RadioSetting("settings.sync", "Sync A & B",
RadioSettingValueBoolean(_mem.settings.sync))
basic.append(rs)
rs = RadioSetting("settings.wtled", "Standby LED Color",
RadioSettingValueList(
LIST_COLOR, current_index=_mem.settings.wtled))
basic.append(rs)
rs = RadioSetting("settings.rxled", "RX LED Color",
RadioSettingValueList(
LIST_COLOR, current_index=_mem.settings.rxled))
basic.append(rs)
rs = RadioSetting("settings.txled", "TX LED Color",
RadioSettingValueList(
LIST_COLOR, current_index=_mem.settings.txled))
basic.append(rs)
val = _mem.settings.almod
rs = RadioSetting("settings.almod", "Alarm Mode",
RadioSettingValueList(
LIST_ALMOD, current_index=val))
basic.append(rs)
rs = RadioSetting("settings.dbptt", "Double PTT",
RadioSettingValueBoolean(_mem.settings.dbptt))
basic.append(rs)
if _mem.settings.tdrab > 0x02:
val = 0x00
else:
val = _mem.settings.tdrab
rs = RadioSetting("settings.tdrab", "Dual Watch TX Priority",
RadioSettingValueList(
LIST_OFFAB, current_index=val))
basic.append(rs)
rs = RadioSetting("settings.ste", "Squelch Tail Eliminate (HT to HT)",
RadioSettingValueBoolean(_mem.settings.ste))
basic.append(rs)
if _mem.settings.rpste > 0x0A:
val = 0x00
else:
val = _mem.settings.rpste
rs = RadioSetting("settings.rpste",
"Squelch Tail Eliminate (repeater)",
RadioSettingValueList(
LIST_RPSTE, current_index=val))
basic.append(rs)
if _mem.settings.rptrl > 0x0A:
val = 0x00
else:
val = _mem.settings.rptrl
rs = RadioSetting("settings.rptrl", "STE Repeater Delay",
RadioSettingValueList(
LIST_STEDELAY, current_index=val))
basic.append(rs)
rs = RadioSetting(
"settings.ponmsg", "Power-On Message",
RadioSettingValueList(
LIST_PONMSG, current_index=_mem.settings.ponmsg))
basic.append(rs)
rs = RadioSetting("settings.roger", "Roger Beep",
RadioSettingValueBoolean(_mem.settings.roger))
basic.append(rs)
rs = RadioSetting(
"settings.rtone", "Tone Burst Frequency",
RadioSettingValueList(
LIST_RTONE, current_index=_mem.settings.rtone))
basic.append(rs)
rs = RadioSetting("settings.rogerrx", "Roger Beep (RX)",
RadioSettingValueList(
LIST_OFFAB, current_index=_mem.settings.rogerrx))
basic.append(rs)
# Advanced settings
rs = RadioSetting("settings.reset", "RESET Menu",
RadioSettingValueBoolean(_mem.settings.reset))
advanced.append(rs)
rs = RadioSetting("settings.menu", "All Menus",
RadioSettingValueBoolean(_mem.settings.menu))
advanced.append(rs)
rs = RadioSetting("settings.fmradio", "Broadcast FM Radio",
RadioSettingValueBoolean(_mem.settings.fmradio))
advanced.append(rs)
rs = RadioSetting("settings.alarm", "Alarm Sound",
RadioSettingValueBoolean(_mem.settings.alarm))
advanced.append(rs)
# Other settings
def _filter(name):
filtered = ""
for char in str(name):
if char in chirp_common.CHARSET_ASCII:
filtered += char
else:
filtered += " "
return filtered
_msg = _mem.firmware_msg
val = RadioSettingValueString(0, 7, _filter(_msg.line1))
val.set_mutable(False)
rs = RadioSetting("firmware_msg.line1", "Firmware Message 1", val)
other.append(rs)
val = RadioSettingValueString(0, 7, _filter(_msg.line2))
val.set_mutable(False)
rs = RadioSetting("firmware_msg.line2", "Firmware Message 2", val)
other.append(rs)
_msg = _mem.sixpoweron_msg
val = RadioSettingValueString(0, 7, _filter(_msg.line1))
val.set_mutable(False)
rs = RadioSetting("sixpoweron_msg.line1", "6+Power-On Message 1", val)
other.append(rs)
val = RadioSettingValueString(0, 7, _filter(_msg.line2))
val.set_mutable(False)
rs = RadioSetting("sixpoweron_msg.line2", "6+Power-On Message 2", val)
other.append(rs)
_msg = _mem.poweron_msg
rs = RadioSetting("poweron_msg.line1", "Power-On Message 1",
RadioSettingValueString(
0, 7, _filter(_msg.line1)))
other.append(rs)
rs = RadioSetting("poweron_msg.line2", "Power-On Message 2",
RadioSettingValueString(
0, 7, _filter(_msg.line2)))
other.append(rs)
# Work mode settings
rs = RadioSetting("settings.displayab", "Display",
RadioSettingValueList(
LIST_AB, current_index=_mem.settings.displayab))
work.append(rs)
rs = RadioSetting("settings.workmode", "VFO/MR Mode",
RadioSettingValueList(
LIST_WORKMODE,
current_index=_mem.settings.workmode))
work.append(rs)
rs = RadioSetting("settings.keylock", "Keypad Lock",
RadioSettingValueBoolean(_mem.settings.keylock))
work.append(rs)
rs = RadioSetting("wmchannel.mrcha", "MR A Channel",
RadioSettingValueInteger(0, 127,
_mem.wmchannel.mrcha))
work.append(rs)
rs = RadioSetting("wmchannel.mrchb", "MR B Channel",
RadioSettingValueInteger(0, 127,
_mem.wmchannel.mrchb))
work.append(rs)
def my_validate(value):
value = chirp_common.parse_freq(value)
msg = ("Can't be less than %i.0000")
if value > 99000000 and value < 130 * 1000000:
raise InvalidValueError(msg % (130))
msg = ("Can't be between %i.9975-%i.0000")
if (179 + 1) * 1000000 <= value and value < 400 * 1000000:
raise InvalidValueError(msg % (179, 400))
msg = ("Can't be greater than %i.9975")
if value > 99000000 and value > (520 + 1) * 1000000:
raise InvalidValueError(msg % (520))
return chirp_common.format_freq(value)
def apply_freq(setting, obj):
value = chirp_common.parse_freq(str(setting.value)) / 10
for i in range(7, -1, -1):
obj.freq[i] = value % 10
value /= 10
val1a = RadioSettingValueString(0, 10,
bfc.bcd_decode_freq(_mem.vfo.a.freq))
val1a.set_validate_callback(my_validate)
rs = RadioSetting("vfo.a.freq", "VFO A Frequency", val1a)
rs.set_apply_callback(apply_freq, _mem.vfo.a)
work.append(rs)
val1b = RadioSettingValueString(0, 10,
bfc.bcd_decode_freq(_mem.vfo.b.freq))
val1b.set_validate_callback(my_validate)
rs = RadioSetting("vfo.b.freq", "VFO B Frequency", val1b)
rs.set_apply_callback(apply_freq, _mem.vfo.b)
work.append(rs)
rs = RadioSetting("vfo.a.step", "VFO A Tuning Step",
RadioSettingValueList(
LIST_STEP, current_index=_mem.vfo.a.step))
work.append(rs)
rs = RadioSetting("vfo.b.step", "VFO B Tuning Step",
RadioSettingValueList(
LIST_STEP, current_index=_mem.vfo.b.step))
work.append(rs)
# broadcast FM settings
_fm_presets = self._memobj.fm_presets
if _fm_presets <= 108.0 * 10 - 650:
preset = _fm_presets / 10.0 + 65
elif _fm_presets >= 65.0 * 10 and _fm_presets <= 108.0 * 10:
preset = _fm_presets / 10.0
else:
preset = 76.0
rs = RadioSetting("fm_presets", "FM Preset(MHz)",
RadioSettingValueFloat(65, 108.0, preset, 0.1, 1))
fm_preset.append(rs)
# DTMF settings
def apply_code(setting, obj, length):
code = []
for j in range(0, length):
try:
code.append(DTMF_CHARS.index(str(setting.value)[j]))
except IndexError:
code.append(0xFF)
obj.code = code
for i in range(0, 15):
_codeobj = self._memobj.pttid[i].code
_code = "".join([DTMF_CHARS[x] for x in _codeobj if int(x) < 0x1F])
val = RadioSettingValueString(0, 5, _code, False)
val.set_charset(DTMF_CHARS)
pttid = RadioSetting("pttid/%i.code" % i,
"Signal Code %i" % (i + 1), val)
pttid.set_apply_callback(apply_code, self._memobj.pttid[i], 5)
dtmfe.append(pttid)
if _mem.ani.dtmfon > 0xC3:
val = 0x03
else:
val = _mem.ani.dtmfon
rs = RadioSetting("ani.dtmfon", "DTMF Speed (on)",
RadioSettingValueList(LIST_DTMFSPEED,
current_index=val))
dtmfe.append(rs)
if _mem.ani.dtmfoff > 0xC3:
val = 0x03
else:
val = _mem.ani.dtmfoff
rs = RadioSetting("ani.dtmfoff", "DTMF Speed (off)",
RadioSettingValueList(LIST_DTMFSPEED,
current_index=val))
dtmfe.append(rs)
_codeobj = self._memobj.ani.code
_code = "".join([DTMF_CHARS[x] for x in _codeobj if int(x) < 0x1F])
val = RadioSettingValueString(0, 5, _code, False)
val.set_charset(DTMF_CHARS)
rs = RadioSetting("ani.code", "ANI Code", val)
rs.set_apply_callback(apply_code, self._memobj.ani, 5)
dtmfe.append(rs)
rs = RadioSetting("ani.aniid", "When to send ANI ID",
RadioSettingValueList(LIST_PTTID,
current_index=_mem.ani.aniid))
dtmfe.append(rs)
def apply_alarmcode(setting, obj, length):
code = []
for j in range(0, length):
try:
code.append(DTMF_CHARS.index(str(setting.value)[j]))
except IndexError:
code.append(0xFF)
obj.alarm = code
_codeobj = self._memobj.ani.alarm
_code = "".join([DTMF_CHARS[x] for x in _codeobj if int(x) < 0x1F])
val = RadioSettingValueString(0, 5, _code, False)
val.set_charset(DTMF_CHARS)
rs = RadioSetting("ani.alarm", "Alarm Code", val)
rs.set_apply_callback(apply_alarmcode, self._memobj.ani, 5)
dtmfe.append(rs)
# Service settings
for band in ["vhf", "uhf"]:
for index in range(0, 10):
key = "squelch.%s.sql%i" % (band, index)
if band == "vhf":
_obj = self._memobj.squelch.vhf
elif band == "uhf":
_obj = self._memobj.squelch.uhf
val = RadioSettingValueInteger(0, 123,
getattr(_obj, "sql%i" % (
index)))
if index == 0:
val.set_mutable(False)
name = "%s Squelch %i" % (band.upper(), index)
rs = RadioSetting(key, name, val)
service.append(rs)
return top
@classmethod
def match_model(cls, filedata, filename):
# This radio has always been post-metadata, so never do
# old-school detection
return False
@directory.register
class GMRSV2(GMRSV2base):
"""BTech GMRS-V2"""
VENDOR = "BTECH"
MODEL = "GMRS-V2"
_fileid = [GMRSV2_fp1, ]
_magic = [MSTRING_GMRSV2, ]
_magic_response_length = 8
POWER_LEVELS = [chirp_common.PowerLevel("High", watts=5.00),
chirp_common.PowerLevel("Low", watts=0.50)]
def validate_memory(self, mem):
msgs = super().validate_memory(mem)
_msg_duplex = 'Duplex must be "off" for this frequency'
_msg_offset = 'Only simplex or +5 MHz offset allowed on GMRS'
if mem.freq not in bandplan_na.ALL_GMRS_FREQS:
if mem.duplex != "off":
msgs.append(chirp_common.ValidationWarning(_msg_duplex))
if mem.freq in bandplan_na.GMRS_HIRPT:
if mem.duplex and mem.offset != 5000000:
msgs.append(chirp_common.ValidationWarning(_msg_offset))
return msgs
@directory.register
class MURSV2(GMRSV2base):
"""BTech MURS-V2"""
VENDOR = "BTECH"
MODEL = "MURS-V2"
_fileid = [MURSV2_fp1, ]
_magic = [MSTRING_MURSV2, ]
_magic_response_length = 8
POWER_LEVELS = [chirp_common.PowerLevel("High", watts=2.00),
chirp_common.PowerLevel("Low", watts=0.50)]
def validate_memory(self, mem):
msgs = super().validate_memory(mem)
_msg_duplex = 'Duplex must be "off" for this frequency'
_msg_offset = 'Only simplex or +5 MHz offset allowed on GMRS'
if mem.freq not in bandplan_na.ALL_MURS_FREQS:
if mem.duplex != "off":
msgs.append(chirp_common.ValidationWarning(_msg_duplex))
return msgs
chirp-20261030/chirp/drivers/h777.py 0000644 0001750 0001750 00000123554 15266622247 015711 0 ustar hibby hibby # -*- coding: utf-8 -*-
# Copyright 2013 Andrew Morgan
#
# 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 .
import time
import struct
import unittest
import logging
from chirp import chirp_common, directory, memmap
from chirp import bitwise, errors, util
from chirp.settings import RadioSetting, RadioSettingGroup, \
RadioSettingValueInteger, RadioSettingValueList, \
RadioSettingValueBoolean, RadioSettingValueMap, \
RadioSettingValueString, RadioSettings
LOG = logging.getLogger(__name__)
MEM_FORMAT_SETTINGS = """
#seekto 0x02B0;
struct {
u8 voiceprompt;
u8 voicelanguage;
u8 scan;
u8 vox;
u8 voxlevel;
u8 voxinhibitonrx;
u8 lowvolinhibittx;
u8 highvolinhibittx;
u8 alarm;
u8 fmradio;
} settings;
"""
MEM_FORMAT = """
#seekto 0x0010;
struct {
lbcd rxfreq[4];
lbcd txfreq[4];
lbcd rxtone[2];
lbcd txtone[2];
u8 unknown3:1,
unknown2:1,
unknown1:1,
skip:1,
highpower:1,
narrow:1,
beatshift:1,
bcl:1;
u8 unknown4[3];
} memory[16];
""" + MEM_FORMAT_SETTINGS
H777_SETTINGS2 = """
#seekto 0x03C0;
struct {
u8 unused:6,
batterysaver:1,
beep:1;
u8 squelchlevel;
u8 sidekeyfunction;
u8 timeouttimer;
u8 unused2[3];
u8 unused3:7,
scanmode:1;
} settings2;
"""
BF1900_SETTINGS2 = """
#seekto 0x03C0;
struct {
u8 unused:6,
batterysaver:1,
beep:1;
u8 squelchlevel;
u8 scanmode;
u8 timeouttimer;
u8 unused2[4];
} settings2;
"""
CMD_ACK = b"\x06"
BLOCK_SIZE = 0x08
UPLOAD_BLOCKS = [list(range(0x0000, 0x0110, 8)),
list(range(0x02b0, 0x02c0, 8)),
list(range(0x0380, 0x03e0, 8))]
VOICE_LIST = ["English", "Chinese"]
TIMEOUTTIMER_LIST = ["Off", "30 seconds", "60 seconds", "90 seconds",
"120 seconds", "150 seconds", "180 seconds",
"210 seconds", "240 seconds", "270 seconds",
"300 seconds"]
DTCS_FLAG = 0x80
DTCS_REV_FLAG = 0x40
def _h777_enter_programming_mode(serial, radio_cls):
# increase default timeout from .25 to .5 for all serial communications
serial.timeout = 0.5
try:
serial.write(b"\x02")
time.sleep(0.1)
serial.write(radio_cls.PROGRAM_CMD)
ack = serial.read(1)
except Exception as e:
LOG.warning('Failed to send program command: %s', e)
raise errors.RadioError("Error communicating with radio")
if not ack:
raise errors.RadioNoResponse()
elif ack != CMD_ACK:
LOG.warning('Ack from program command was %r, expected %r',
ack, CMD_ACK)
raise errors.RadioError("Radio refused to enter programming mode")
try:
serial.write(b"\x02")
# At least one version of the Baofeng BF-888S has a consistent
# ~0.33s delay between sending the first five bytes of the
# version data and the last three bytes. We need to raise the
# timeout so that the read doesn't finish early.
ident = serial.read(8)
except Exception as e:
LOG.warning('Failed to read ident: %s', e)
raise errors.RadioError("Error communicating with radio")
if ident:
LOG.info('Radio identified with:\n%s', util.hexprint(ident))
try:
serial.write(CMD_ACK)
ack = serial.read(1)
if ack != CMD_ACK:
raise errors.RadioError("Bad ACK after reading ident")
except:
raise errors.RadioError('No ACK after reading ident')
return ident
raise errors.RadioNoResponse()
def _h777_exit_programming_mode(serial):
try:
serial.write(b"E")
except Exception as e:
LOG.warning('Failed to send exit command: %s', e)
raise errors.RadioError("Radio refused to exit programming mode")
def _h777_read_block(radio, block_addr, block_size):
serial = radio.pipe
cmd = struct.pack(">cHb", b'R', block_addr, BLOCK_SIZE)
expectedresponse = b"W" + cmd[1:]
LOG.debug("Reading block %04x..." % (block_addr))
try:
serial.write(cmd)
response = serial.read(4 + BLOCK_SIZE)
if response[:4] != expectedresponse:
LOG.warning('Got %r expected %r' % (response[:4],
expectedresponse))
raise Exception("Error reading block %04x." % (block_addr))
block_data = response[4:]
serial.write(CMD_ACK)
ack = serial.read(1)
except:
raise errors.RadioError("Failed to read block at %04x" % block_addr)
if ack != CMD_ACK:
raise Exception("No ACK reading block %04x." % (block_addr))
return block_data
def _h777_write_block(radio, block_addr, block_size):
serial = radio.pipe
cmd = struct.pack(">cHb", b'W', block_addr, BLOCK_SIZE)
data = radio.get_mmap().get_byte_compatible()[block_addr:block_addr + 8]
radio.pipe.log('Writing %i block at %04x' % (BLOCK_SIZE, block_addr))
try:
serial.write(cmd + data)
# Time required to write data blocks varies between individual
# radios of the Baofeng BF-888S model. The longest seen is
# ~0.31s.
if serial.read(1) != CMD_ACK:
raise Exception("No ACK")
except:
raise errors.RadioError("Failed to send block "
"to radio at %04x" % block_addr)
def _h777_enter_single_programming_mode(radio):
ident = _h777_enter_programming_mode(radio.pipe, radio.__class__)
if not ident:
raise errors.RadioNoResponse()
if not any(rc_ident in ident for rc_ident in radio.IDENT):
LOG.warning('Expected %s for %s but got:\n%s',
radio.IDENT, radio.__class__.__name__,
util.hexprint(ident))
raise errors.RadioError('Incorrect model')
def do_download(radio):
LOG.debug("download")
if len(radio.detected_models()) <= 1:
LOG.debug('Entering programming mode for %s', radio.__class__.__name__)
_h777_enter_single_programming_mode(radio)
else:
LOG.debug('Already in programming mode for %s',
radio.__class__.__name__)
radio._pre_download()
data = b""
status = chirp_common.Status()
status.msg = "Cloning from radio"
status.cur = 0
status.max = radio._memsize
for addr in range(0, radio._memsize, BLOCK_SIZE):
status.cur = addr + BLOCK_SIZE
radio.status_fn(status)
radio.pipe.log('Reading %i block at %04x' % (BLOCK_SIZE, addr))
block = _h777_read_block(radio, addr, BLOCK_SIZE)
data += block
_h777_exit_programming_mode(radio.pipe)
return memmap.MemoryMapBytes(data)
def do_upload(radio):
status = chirp_common.Status()
status.msg = "Uploading to radio"
_h777_enter_single_programming_mode(radio)
status.cur = 0
status.max = radio._memsize
for start_addr, end_addr in radio._ranges:
for addr in range(start_addr, end_addr, BLOCK_SIZE):
status.cur = addr + BLOCK_SIZE
radio.status_fn(status)
_h777_write_block(radio, addr, BLOCK_SIZE)
_h777_exit_programming_mode(radio.pipe)
class ArcshellAR5(chirp_common.Alias):
VENDOR = 'Arcshell'
MODEL = 'AR-5'
class ArcshellAR6(chirp_common.Alias):
VENDOR = 'Arcshell'
MODEL = 'AR-6'
class GV8SAlias(chirp_common.Alias):
VENDOR = 'Greaval'
MODEL = 'GV-8S'
class GV9SAlias(chirp_common.Alias):
VENDOR = 'Greaval'
MODEL = 'GV-9S'
class A8SAlias(chirp_common.Alias):
VENDOR = 'Ansoko'
MODEL = 'A-8S'
class TenwayTW325Alias(chirp_common.Alias):
VENDOR = 'Tenway'
MODEL = 'TW-325'
@directory.register
class H777Radio(chirp_common.CloneModeRadio):
"""HST H-777"""
# VENDOR = "Heng Shun Tong (�顺通)"
# MODEL = "H-777"
VENDOR = "Baofeng"
MODEL = "BF-888"
PROGRAM_CMD = b'PROGRAM'
IDENT = [b"P3107", ]
BAUD_RATE = 9600
# TODO: Is it 1 watt?
POWER_LEVELS = [chirp_common.PowerLevel("Low", watts=1.00),
chirp_common.PowerLevel("High", watts=5.00)]
VALID_BANDS = (400000000, 490000000)
MAX_VOXLEVEL = 5
ALIASES = [ArcshellAR5, ArcshellAR6, GV8SAlias, GV9SAlias, A8SAlias,
TenwayTW325Alias]
SIDEKEYFUNCTION_LIST = ["Off", "Monitor", "Transmit Power", "Alarm"]
SCANMODE_LIST = ["Carrier", "Time"]
# This code currently requires that ranges start at 0x0000
# and are continuous. In the original program 0x0388 and 0x03C8
# are only written (all bytes 0xFF), not read.
# _ranges = [
# (0x0000, 0x0110),
# (0x02B0, 0x02C0),
# (0x0380, 0x03E0)
# ]
# Memory starts looping at 0x1000... But not every 0x1000.
_ranges = [
(0x0000, 0x0110),
(0x0380, 0x03E0),
(0x02B0, 0x02C0),
]
_memsize = 0x03E0
_has_fm = True
_has_sidekey = True
_has_scanmodes = True
_has_scramble = True
@classmethod
def match_model(cls, filedata, filename):
return cls._memsize and len(filedata) == cls._memsize
def get_features(self):
rf = chirp_common.RadioFeatures()
rf.has_settings = True
rf.valid_modes = ["NFM", "FM"] # 12.5 kHz, 25 kHz.
rf.valid_skips = ["", "S"]
rf.valid_tmodes = ["", "Tone", "TSQL", "DTCS", "Cross"]
rf.valid_duplexes = ["", "-", "+", "split", "off"]
rf.can_odd_split = True
rf.has_rx_dtcs = True
rf.has_ctone = True
rf.has_cross = True
rf.valid_cross_modes = [
"Tone->Tone",
"DTCS->",
"->DTCS",
"Tone->DTCS",
"DTCS->Tone",
"->Tone",
"DTCS->DTCS"]
rf.has_tuning_step = False
rf.has_bank = False
rf.has_name = False
rf.memory_bounds = (1, 16)
rf.valid_bands = [self.VALID_BANDS]
rf.valid_power_levels = self.POWER_LEVELS
rf.valid_tuning_steps = [2.5, 5.0, 6.25, 10.0, 12.5, 15.0, 20.0, 25.0,
50.0, 100.0]
return rf
def process_mmap(self):
self._memobj = bitwise.parse(MEM_FORMAT + H777_SETTINGS2, self._mmap)
def sync_in(self):
self._mmap = do_download(self)
self.process_mmap()
def sync_out(self):
do_upload(self)
def get_raw_memory(self, number):
return repr(self._memobj.memory[number - 1])
def _decode_tone(self, memval):
memval[1].ignore_bits(DTCS_FLAG | DTCS_REV_FLAG)
is_dtcs = memval[1].get_bits(DTCS_FLAG)
is_rev = memval[1].get_bits(DTCS_REV_FLAG)
if memval.get_raw() == b"\xFF\xFF":
return '', None, None
elif is_dtcs:
return 'DTCS', int(memval), 'R' if is_rev else 'N'
else:
tone = int(memval) / 10.0
valid_tones = self.get_features().valid_tones
if tone not in valid_tones:
closest = min(valid_tones, key=lambda value: abs(value - tone))
LOG.error(
"Radio reports unsupported tone %.1f Hz; using %.1f Hz",
tone, closest)
tone = closest
return 'Tone', tone, None
def _encode_tone(self, memval, mode, value, pol):
memval[1].ignore_bits(DTCS_FLAG | DTCS_REV_FLAG)
if mode == '':
memval.fill_raw(b'\xFF')
elif mode == 'Tone':
memval[1].clr_bits(DTCS_FLAG | DTCS_REV_FLAG)
memval.set_value(int(value * 10))
elif mode == 'DTCS':
memval[1].set_bits(DTCS_FLAG)
if pol == 'R':
memval[1].set_bits(DTCS_REV_FLAG)
else:
memval[1].clr_bits(DTCS_REV_FLAG)
memval.set_value(value)
else:
raise Exception("Internal error: invalid mode `%s'" % mode)
def get_memory(self, number):
_mem = self._memobj.memory[number - 1]
mem = chirp_common.Memory()
mem.number = number
mem.freq = int(_mem.rxfreq) * 10
# We'll consider any blank (i.e. 0 MHz frequency) to be empty
if mem.freq == 0:
mem.empty = True
return mem
if _mem.rxfreq.get_raw() == b"\xFF\xFF\xFF\xFF":
mem.freq = 0
mem.empty = True
return mem
if _mem.txfreq.get_raw() == b"\xFF\xFF\xFF\xFF":
mem.duplex = "off"
mem.offset = 0
elif int(_mem.rxfreq) == int(_mem.txfreq):
mem.duplex = ""
mem.offset = 0
else:
mem.duplex = int(_mem.rxfreq) > int(_mem.txfreq) and "-" or "+"
mem.offset = abs(int(_mem.rxfreq) - int(_mem.txfreq)) * 10
mem.mode = not _mem.narrow and "FM" or "NFM"
mem.power = self.POWER_LEVELS[_mem.highpower]
mem.skip = _mem.skip and "S" or ""
txtone = self._decode_tone(_mem.txtone)
rxtone = self._decode_tone(_mem.rxtone)
chirp_common.split_tone_decode(mem, txtone, rxtone)
mem.extra = RadioSettingGroup("Extra", "extra")
rs = RadioSetting("bcl", "Busy Channel Lockout",
RadioSettingValueBoolean(not _mem.bcl))
rs.set_doc('Prevents transmitting on a channel that is already in '
'use. Pressing PTT on a busy channel sounds an alert '
'tone instead of keying the transmitter.')
mem.extra.append(rs)
if self._has_scramble:
rs = RadioSetting("beatshift", "Beat Shift(scramble)",
RadioSettingValueBoolean(not _mem.beatshift))
mem.extra.append(rs)
return mem
def set_memory(self, mem):
# Get a low-level memory object mapped to the image
_mem = self._memobj.memory[mem.number - 1]
if mem.empty:
_mem.set_raw("\xFF" * (_mem.size() // 8))
return
_mem.rxfreq = mem.freq / 10
if mem.duplex == "off":
for i in range(0, 4):
_mem.txfreq[i].set_raw("\xFF")
elif mem.duplex == "split":
_mem.txfreq = mem.offset / 10
elif mem.duplex == "+":
_mem.txfreq = (mem.freq + mem.offset) / 10
elif mem.duplex == "-":
_mem.txfreq = (mem.freq - mem.offset) / 10
else:
_mem.txfreq = mem.freq / 10
txtone, rxtone = chirp_common.split_tone_encode(mem)
self._encode_tone(_mem.txtone, *txtone)
self._encode_tone(_mem.rxtone, *rxtone)
_mem.narrow = 'N' in mem.mode
_mem.highpower = mem.power == self.POWER_LEVELS[1]
_mem.skip = mem.skip == "S"
for setting in mem.extra:
# All channel extras are stored inverted
setattr(_mem, setting.get_name(), not int(setting.value))
self._clear_unknown_bits(_mem)
def _clear_unknown_bits(self, _mem):
# When set to one, official programming software (BF-480) shows always
# "WFM", even if we choose "NFM". Therefore, for compatibility
# purposes, we will set these to zero.
_mem.unknown1 = 0
_mem.unknown2 = 0
_mem.unknown3 = 0
def _pre_download(self):
"""Called in programming mode, before the memory is read"""
pass
def get_settings(self):
_settings = self._memobj.settings
basic = RadioSettingGroup("basic", "Basic Settings")
top = RadioSettings(basic)
# TODO: Check that all these settings actually do what they
# say they do.
rs = RadioSetting("voiceprompt", "Voice prompt",
RadioSettingValueBoolean(_settings.voiceprompt))
rs.set_doc('Announces the channel number and other operations out '
'loud as you use the radio. It can be intrusive in quiet '
'surroundings.')
basic.append(rs)
rs = RadioSetting("voicelanguage", "Voice language",
RadioSettingValueList(
VOICE_LIST,
current_index=_settings.voicelanguage))
rs.set_doc('Selects the language the spoken voice prompts use. It '
'has no effect unless Voice prompts are enabled.')
basic.append(rs)
rs = RadioSetting("scan", "Scan",
RadioSettingValueBoolean(_settings.scan))
rs.set_doc('Set the channel selector to channel 16 to scan '
'channels 1-15. Scanning stops when a signal is '
'detected and resumes once it clears. At least two '
'channels are required, and scanning is unavailable '
'while the FM radio function is active')
basic.append(rs)
if self._has_scanmodes:
rs = RadioSetting("settings2.scanmode", "Scan mode",
RadioSettingValueList(
self.SCANMODE_LIST,
current_index=(
self._memobj.settings2.scanmode)))
basic.append(rs)
rs = RadioSetting("vox", "VOX",
RadioSettingValueBoolean(_settings.vox))
rs.set_doc('Voice operated transmit. The radio starts transmitting '
'when you speak towards the microphone, so hands-free '
'conversation is possible without pressing PTT.')
basic.append(rs)
rs = RadioSetting("voxlevel", "VOX level",
RadioSettingValueInteger(
1, self.MAX_VOXLEVEL, _settings.voxlevel + 1))
rs.set_doc('Sets the VOX sensitivity. Too sensitive a setting keys '
'the transmitter on the noise around the radio, too '
'insensitive a setting fails to pick up your voice.')
basic.append(rs)
rs = RadioSetting("voxinhibitonrx", "Inhibit VOX on receive",
RadioSettingValueBoolean(_settings.voxinhibitonrx))
basic.append(rs)
rs = RadioSetting("lowvolinhibittx", "Low voltage inhibit transmit",
RadioSettingValueBoolean(_settings.lowvolinhibittx))
basic.append(rs)
rs = RadioSetting("highvolinhibittx", "High voltage inhibit transmit",
RadioSettingValueBoolean(_settings.highvolinhibittx))
basic.append(rs)
if self.PROGRAM_CMD == b'PWPG970': # Baofeng 1900 series
rs = RadioSetting("alarm", "Alarm",
RadioSettingValueList(
self.ALARM_LIST,
current_index=_settings.alarm))
else:
rs = RadioSetting("alarm", "Alarm",
RadioSettingValueBoolean(_settings.alarm))
rs.set_doc('Emergency alarm triggered by the side key. Depending '
'on the model this either sounds a local siren or also '
'alerts other radios on the channel')
basic.append(rs)
# TODO: This should probably be called “FM Broadcast Band Radio�
# or something. I'm not sure if the model actually has one though.
if self._has_fm:
rs = RadioSetting("fmradio", "FM function",
RadioSettingValueBoolean(_settings.fmradio))
rs.set_doc('Broadcast FM radio receiver. The radio cannot scan '
'while this is active')
basic.append(rs)
rs = RadioSetting("settings2.beep", "Beep",
RadioSettingValueBoolean(
self._memobj.settings2.beep))
rs.set_doc('Sounds a short confirmation tone as you operate the '
'radio. Turn it off when the radio has to be used '
'discreetly.')
basic.append(rs)
rs = RadioSetting("settings2.batterysaver", "Battery saver",
RadioSettingValueBoolean(
self._memobj.settings2.batterysaver))
rs.set_doc('Cuts standby power consumption by switching the receiver '
'off and on in a repeating cycle once no signal has been '
'received for a few seconds. It considerably extends '
'battery life, at the cost of occasionally clipping the '
'first moment of an incoming transmission.')
basic.append(rs)
rs = RadioSetting("settings2.squelchlevel", "Squelch level",
RadioSettingValueInteger(
0, 9, self._memobj.settings2.squelchlevel))
rs.set_doc('Sets how strong a received signal must be before the '
'speaker unmutes. 0 leaves the squelch open so you hear '
'noise all the time, higher values reject more weak '
'signals and noise. Raise this value if the speaker keeps '
'unmuting on interference, lower it if distant stations '
'are being cut off.')
basic.append(rs)
if self._has_sidekey:
rs = RadioSetting("settings2.sidekeyfunction", "Side key function",
RadioSettingValueList(
self.SIDEKEYFUNCTION_LIST,
current_index=(
self._memobj.settings2.sidekeyfunction)))
basic.append(rs)
rs = RadioSetting(
"settings2.timeouttimer", "Timeout timer",
RadioSettingValueList(
TIMEOUTTIMER_LIST,
current_index=self._memobj.settings2.timeouttimer))
rs.set_doc('Limits how long a single transmission may last. When '
'the time runs out the radio stops transmitting until '
'PTT is released, which prevents overheating and stops a '
'stuck PTT from blocking the channel for everyone else.')
basic.append(rs)
return top
def set_settings(self, settings):
for element in settings:
if not isinstance(element, RadioSetting):
self.set_settings(element)
continue
else:
try:
if "." in element.get_name():
bits = element.get_name().split(".")
obj = self._memobj
for bit in bits[:-1]:
obj = getattr(obj, bit)
setting = bits[-1]
else:
obj = self._memobj.settings
setting = element.get_name()
if element.has_apply_callback():
LOG.debug("Using apply callback")
element.run_apply_callback()
elif setting == "voxlevel":
setattr(obj, setting, int(element.value) - 1)
else:
LOG.debug("Setting %s = %s" % (setting, element.value))
setattr(obj, setting, element.value)
except Exception:
LOG.debug(element.get_name())
raise
@directory.register
class RetevisH777(H777Radio):
VENDOR = 'Retevis'
MODEL = 'H777'
ALIASES = []
@classmethod
def detect_from_serial(cls, pipe):
cls_to_ident = {rc: rc.IDENT for rc in cls.detected_models()}
for ident_rclass in reversed(cls.detected_models()):
ident = _h777_enter_programming_mode(pipe, ident_rclass)
for rclass, idents in cls_to_ident.items():
if any(rc_ident in ident for rc_ident in idents):
LOG.debug('Detected %s', rclass.__name__)
return rclass
LOG.debug('Ident was %r, idents were %r', ident, idents)
LOG.info('Did not identify radio as %s', rclass.__name__)
time.sleep(0.5)
_h777_exit_programming_mode(pipe)
time.sleep(1)
raise errors.RadioError('Failed to identify with radio.')
@classmethod
def match_model(cls, filedata, filename):
# This radio has always been post-metadata, so never do
# old-school detection
return False
MP31_MEM_FORMAT = """
#seekto 0x0000;
struct {
lbcd rxfreq[4];
lbcd txfreq[4];
lbcd rxtone[2];
lbcd txtone[2];
u8 unknown3:1,
unknown2:1,
unknown1:1,
skip:1,
highpower:1,
narrow:1,
beatshift:1,
bcl:1;
u8 unknown4[3];
} memory[38];
"""
MP31_SETTINGS2 = """
#seekto 0x026B;
struct {
u8 squelchlevel;
u8 batterysaver;
u8 voxdelay;
u8 timeouttimer;
u8 scanmode;
u8 beep;
u8 sidekey;
u8 rxemergency;
} settings2;
"""
@directory.register
class MP31Radio(H777Radio):
"""Baofeng MP31"""
VENDOR = "Baofeng"
MODEL = "MP31"
IDENT = [b"P3107\xf7\x00\x00"]
_ranges = [(0x0000, 0x0400)]
_memsize = 0x0400
_has_sidekey = False
ALIASES = []
def process_mmap(self):
self._memobj = bitwise.parse(
MP31_MEM_FORMAT + MP31_SETTINGS2 + MEM_FORMAT_SETTINGS, self._mmap)
def get_features(self):
rf = super().get_features()
rf.memory_bounds = (1, 38)
return rf
@classmethod
def match_model(cls, filedata, filename):
return False
class H777TestCase(unittest.TestCase):
def setUp(self):
self.driver = H777Radio(None)
self.testdata = bitwise.parse("lbcd foo[2];",
memmap.MemoryMap("\x00\x00"))
def test_decode_tone_dtcs_normal(self):
mode, value, pol = self.driver._decode_tone(8023)
self.assertEqual('DTCS', mode)
self.assertEqual(23, value)
self.assertEqual('N', pol)
def test_decode_tone_dtcs_rev(self):
mode, value, pol = self.driver._decode_tone(12023)
self.assertEqual('DTCS', mode)
self.assertEqual(23, value)
self.assertEqual('R', pol)
def test_decode_tone_tone(self):
mode, value, pol = self.driver._decode_tone(885)
self.assertEqual('Tone', mode)
self.assertEqual(88.5, value)
self.assertEqual(None, pol)
def test_decode_tone_none(self):
mode, value, pol = self.driver._decode_tone(16665)
self.assertEqual('', mode)
self.assertEqual(None, value)
self.assertEqual(None, pol)
def test_encode_tone_dtcs_normal(self):
self.driver._encode_tone(self.testdata.foo, 'DTCS', 23, 'N')
self.assertEqual(8023, int(self.testdata.foo))
def test_encode_tone_dtcs_rev(self):
self.driver._encode_tone(self.testdata.foo, 'DTCS', 23, 'R')
self.assertEqual(12023, int(self.testdata.foo))
def test_encode_tone(self):
self.driver._encode_tone(self.testdata.foo, 'Tone', 88.5, 'N')
self.assertEqual(885, int(self.testdata.foo))
def test_encode_tone_none(self):
self.driver._encode_tone(self.testdata.foo, '', 67.0, 'N')
self.assertEqual(16665, int(self.testdata.foo))
def test_mp31_38_channels(self):
radio = MP31Radio(None)
rf = radio.get_features()
self.assertEqual(rf.memory_bounds, (1, 38))
def test_mp31_memsize(self):
radio = MP31Radio(None)
self.assertEqual(radio._memsize, 0x0400)
def test_mp31_ranges(self):
radio = MP31Radio(None)
self.assertEqual(radio._ranges, [(0x0000, 0x0400)])
def test_mp31_ident(self):
radio = MP31Radio(None)
self.assertIn(b"P3107\xf7\x00\x00", radio.IDENT)
@directory.register
class ROGA2SRadio(H777Radio):
VENDOR = "Radioddity"
MODEL = "GA-2S"
ALIASES = []
_has_fm = False
SIDEKEYFUNCTION_LIST = ["Off", "Monitor", "Unused", "Alarm"]
@classmethod
def match_model(cls, filedata, filename):
# This model is only ever matched via metadata
return False
@directory.register
class H777PlusRadio(H777Radio):
VENDOR = "Retevis"
MODEL = "H777 Plus"
ALIASES = []
_has_fm = False
_has_scanmodes = False
@classmethod
def match_model(cls, filedata, filename):
# This model is only ever matched via metadata
return False
@directory.register
class BFM4Radio(H777Radio):
VENDOR = "Baofeng"
MODEL = "BF-M4"
ALIASES = []
_has_fm = False
@classmethod
def match_model(cls, filedata, filename):
# This model is only ever matched via metadata
return False
@directory.register
class MT8SRadio(H777Radio):
VENDOR = "MaxTalker"
MODEL = "MT-8S"
ALIASES = []
_has_fm = False
@classmethod
def match_model(cls, filedata, filename):
# This model is only ever matched via metadata
return False
@directory.register
class BF1901Radio(H777Radio):
VENDOR = "Baofeng"
MODEL = "BF-1901"
PROGRAM_CMD = b'PWPG970'
ALIASES = []
VALID_BANDS = (400000000, 520000000)
MAX_VOXLEVEL = 9
SCANMODE_LIST = ["Time", "Carrier", "Search"]
ALARM_LIST = ["Local", "Remote"]
_has_fm = True
_has_sidekey = False
_has_scanmodes = True
_has_scramble = False
def process_mmap(self):
self._memobj = bitwise.parse(MEM_FORMAT + BF1900_SETTINGS2, self._mmap)
@classmethod
def match_model(cls, filedata, filename):
# This model is only ever matched via metadata
return False
@directory.register
class BF1904Radio(BF1901Radio):
VENDOR = "Baofeng"
MODEL = "BF-1904"
ALIASES = []
# TODO: Is it 1 watt?
POWER_LEVELS = [chirp_common.PowerLevel("Low", watts=1.00),
chirp_common.PowerLevel("High", watts=10.00)]
@classmethod
def match_model(cls, filedata, filename):
# This model is only ever matched via metadata
return False
@directory.register
class BF1909Radio(BF1901Radio):
VENDOR = "Baofeng"
MODEL = "BF-1909"
ALIASES = []
IDENT = [b"P320h",
b"P3107" + b"\xF4" + b"AM",
]
_ranges = [
(0x0000, 0x0110),
(0x0250, 0x0260),
(0x02B0, 0x02C0),
(0x03C0, 0x03E0),
]
_memsize = 0x03F0
# TODO: Is it 1 watt?
POWER_LEVELS = [chirp_common.PowerLevel("Low", watts=1.00),
chirp_common.PowerLevel("High", watts=10.00)]
@classmethod
def match_model(cls, filedata, filename):
# This model is only ever matched via metadata
return False
@directory.register
class MAVERICKRA100Radio(BFM4Radio):
VENDOR = "Maverick"
MODEL = "RA-100"
ALIASES = []
@classmethod
def match_model(cls, filedata, filename):
# This model is only ever matched via metadata
return False
@directory.register
class MAVERICKRA425Radio(BF1904Radio):
VENDOR = "Maverick"
MODEL = "RA-425"
ALIASES = []
_has_fm = False
@classmethod
def match_model(cls, filedata, filename):
# This model is only ever matched via metadata
return False
ZA758_MEM_FORMAT = """
#seekto 0x0010;
struct {
lbcd rxfreq[4];
lbcd txfreq[4];
lbcd rxtone[2];
lbcd txtone[2];
u8 pttid:1,
compander:1,
scramble:1,
skip:1,
highpower:1,
narrow:1,
unknown1:1,
bcl:1;
u8 unknown2[3];
} memory[16];
#seekto 0x03A0;
u8 bot_code[15];
#seekto 0x03C0;
struct {
u8 unknown0:1,
scanmode:1,
pttid:2,
voice:2,
batterysaver:1,
beep:1;
u8 squelch;
u8 sk1short;
u8 tot;
u8 vox;
u8 sk2short;
u8 priority;
u8 voxdelay;
u8 sk1long;
u8 sk2long;
} settings;
#seekto 0x03E0;
u8 eot_code[15];
"""
ZA758_SIDEKEY_LIST = ["Off", "Monitor Momentary", "High/Low Power", "Scan",
"VOX", "Talkaround", "Reverse", "Show Power",
"Show Battery", "Call Ring", "Emergency Alarm",
"Channel Announce", "PTT ID", "Squelch Level",
"Compander", "Scrambler"]
# The factory software does not offer these functions on a short press
ZA758_SHORTPRESS_EXCLUDE = (2, 5, 6, 7)
ZA758_VOICE_LIST = ["Off", "Chinese", "English"]
ZA758_PTTID_LIST = ["Off", "Begin of TX", "End of TX", "Both"]
ZA758_VOX_LIST = ["Off"] + ["%i" % x for x in range(1, 10)]
ZA758_VOXDELAY_LIST = ["%.1f seconds" % (x / 10) for x in range(5, 31, 5)]
ZA758_DTMF_CHARS = "0123456789ABCD"
# Flags byte for a new channel: busy lock, scramble, compander and PTT ID
# off; narrow, low power. Scan add is then set from the memory, so a new
# scanned channel matches the factory channels (0xE7).
ZA758_NEW_FLAGS = 0xF7
@directory.register
class ZartekZA758Radio(H777Radio):
"""Zartek ZA-758"""
VENDOR = "Zartek"
MODEL = "ZA-758"
PROGRAM_CMD = b"LROGRAM"
IDENT = [b"SMP558\x00\x00"]
ALIASES = []
# Low is the 500 mW licence-free limit the radio ships with.
# TODO: High has not been measured; 2 W is the rating of the ZA-748.
POWER_LEVELS = [chirp_common.PowerLevel("Low", watts=0.50),
chirp_common.PowerLevel("High", watts=2.00)]
VALID_BANDS = (400000000, 470000000)
# Do not upload the unmapped 0x0110-0x039F and 0x03F0-0x03FF or the
# model fingerprint at 0x03D0
_ranges = [
(0x0000, 0x0110),
(0x03A0, 0x03D0),
(0x03E0, 0x03F0),
]
_memsize = 0x0400
_has_scramble = False
@classmethod
def match_model(cls, filedata, filename):
# This model is only ever matched via metadata
return False
def process_mmap(self):
self._memobj = bitwise.parse(ZA758_MEM_FORMAT, self._mmap)
def _pre_download(self):
# Before reading, the factory software requests the 6-byte read
# password and acknowledges it. The password is only checked by
# the software, so it is not compared here.
try:
self.pipe.write(b"\x05")
password = self.pipe.read(6)
if len(password) != 6:
raise errors.RadioError(
"Radio did not send its password block")
self.pipe.write(CMD_ACK)
ack = self.pipe.read(1)
except errors.RadioError:
raise
except Exception as e:
LOG.warning('Failed to read password block: %s', e)
raise errors.RadioError("Error communicating with radio")
if ack != CMD_ACK:
raise errors.RadioError("No ACK after reading password block")
def _clear_unknown_bits(self, _mem):
# Bits the BF-888 clears hold scramble, compander and PTT ID here
pass
def get_memory(self, number):
mem = super().get_memory(number)
if mem.empty:
return mem
_mem = self._memobj.memory[number - 1]
rs = RadioSetting("scramble", "Scramble",
RadioSettingValueBoolean(not _mem.scramble))
rs.set_doc('Inverts the audio so only radios with scramble enabled '
'on the same channel can understand it.')
mem.extra.append(rs)
rs = RadioSetting("compander", "Compander",
RadioSettingValueBoolean(not _mem.compander))
rs.set_doc('Compresses audio on transmit and expands it on receive '
'to reduce noise. Radios on the channel should match.')
mem.extra.append(rs)
rs = RadioSetting("pttid", "PTT ID",
RadioSettingValueBoolean(not _mem.pttid))
rs.set_doc('Sends the PTT ID DTMF codes on this channel, at the '
'times chosen by the PTT ID setting.')
mem.extra.append(rs)
return mem
def set_memory(self, mem):
_mem = self._memobj.memory[mem.number - 1]
if (not mem.empty and
_mem.rxfreq.get_raw() in (b"\xFF" * 4, b"\x00" * 4)):
# A blank channel's flags are not usable defaults; erased ones
# are all set, which means high power
_mem.set_raw(b"\xFF" * 12 + bytes([ZA758_NEW_FLAGS]) +
b"\xFF" * 3)
super().set_memory(mem)
def _get_dtmf_code(self, code):
# Digits are stored one per byte; the first non-digit ends the code
digits = ""
for value in code:
if int(value) >= len(ZA758_DTMF_CHARS):
break
digits += ZA758_DTMF_CHARS[int(value)]
return digits
def get_settings(self):
_settings = self._memobj.settings
basic = RadioSettingGroup("basic", "Basic Settings")
keys = RadioSettingGroup("keys", "Side Keys")
dtmf = RadioSettingGroup("dtmf", "PTT ID")
top = RadioSettings(basic, keys, dtmf)
rs = RadioSetting("squelch", "Squelch level",
RadioSettingValueInteger(
0, 9, int(_settings.squelch)))
rs.set_doc('Signal strength needed to open the speaker. Higher '
'levels block weak signals and noise; 0 leaves the '
'squelch open.')
basic.append(rs)
rs = RadioSetting("tot", "Time-out timer",
RadioSettingValueList(
TIMEOUTTIMER_LIST,
current_index=int(_settings.tot)))
rs.set_doc('Limits how long one transmission can last. When the '
'time runs out the radio stops transmitting until PTT '
'is released.')
basic.append(rs)
rs = RadioSetting("vox", "VOX level",
RadioSettingValueList(
ZA758_VOX_LIST,
current_index=int(_settings.vox)))
rs.set_doc('Voice-operated transmit. 9 is the most sensitive and '
'1 needs the loudest voice to start transmitting.')
basic.append(rs)
rs = RadioSetting("voxdelay", "VOX delay",
RadioSettingValueList(
ZA758_VOXDELAY_LIST,
current_index=int(_settings.voxdelay)))
rs.set_doc('How long the radio keeps transmitting after you stop '
'speaking in VOX mode.')
basic.append(rs)
rs = RadioSetting("voice", "Voice announcement",
RadioSettingValueList(
ZA758_VOICE_LIST,
current_index=int(_settings.voice)))
rs.set_doc('Announces the channel number and battery level out '
'loud in the chosen language.')
basic.append(rs)
rs = RadioSetting("beep", "Beep tone",
RadioSettingValueBoolean(_settings.beep))
rs.set_doc('Sounds a beep when keys are pressed.')
basic.append(rs)
rs = RadioSetting("batterysaver", "Battery saver",
RadioSettingValueBoolean(_settings.batterysaver))
rs.set_doc('Periodically turns the receiver off while the channel '
'is quiet to extend battery life.')
basic.append(rs)
rs = RadioSetting("scanmode", "Scan mode",
RadioSettingValueList(
self.SCANMODE_LIST,
current_index=_settings.scanmode))
rs.set_doc('Carrier resumes scanning when the signal stops; Time '
'resumes after a few seconds even if it continues.')
basic.append(rs)
rs = RadioSetting("priority", "Priority channel",
RadioSettingValueMap(
[("Off", 0xFF)] +
[("%i" % (x + 1), x) for x in range(16)],
int(_settings.priority)))
rs.set_doc('Channel that is checked more often while scanning.')
basic.append(rs)
short_keys = [(name, i) for i, name in enumerate(ZA758_SIDEKEY_LIST)
if i not in ZA758_SHORTPRESS_EXCLUDE]
long_keys = [(name, i) for i, name in enumerate(ZA758_SIDEKEY_LIST)]
for name, label, options in (
("sk1short", "Side key 1 (short press)", short_keys),
("sk1long", "Side key 1 (long press)", long_keys),
("sk2short", "Side key 2 (short press)", short_keys),
("sk2long", "Side key 2 (long press)", long_keys)):
rs = RadioSetting(name, label,
RadioSettingValueMap(
options, int(getattr(_settings, name))))
rs.set_doc('Function of the side key.')
keys.append(rs)
rs = RadioSetting("pttid", "PTT ID",
RadioSettingValueList(
ZA758_PTTID_LIST,
current_index=_settings.pttid))
rs.set_doc('When to send the DTMF ID codes on channels that have '
'PTT ID enabled.')
dtmf.append(rs)
def apply_code(setting, obj, name):
# The code is shown only up to its first non-digit byte, so
# leave it alone unless it was edited
if not setting.changed():
return
value = str(setting.value).strip()
code = [ZA758_DTMF_CHARS.index(c) for c in value]
setattr(obj, name, code + [0xFF] * (15 - len(code)))
for name, label in (("bot_code", "Begin of TX code"),
("eot_code", "End of TX code")):
rs = RadioSetting(name, label,
RadioSettingValueString(
0, 15,
self._get_dtmf_code(
getattr(self._memobj, name)),
False, ZA758_DTMF_CHARS))
rs.set_doc('DTMF digits (0-9, A-D) sent as this radio\'s ID.')
rs.set_apply_callback(apply_code, self._memobj, name)
dtmf.append(rs)
return top
chirp-20261030/chirp/drivers/hf90.py 0000644 0001750 0001750 00000045452 15172631605 015755 0 ustar hibby hibby # Copyright 2023 Finn Thain
#
# 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 RadioSettingValueList, RadioSettingValueBoolean
from chirp.settings import RadioSettingValueInteger, RadioSettings
import logging
LOG = logging.getLogger(__name__)
HF90_MEM_FORMAT = """
#seekto 0x1800;
struct {
u8 type:1, f_p_program:1, unknown1:1, auto_tune:1,
tx_power:2, unknown2:1, mode_toggle:1;
u8 unknown3;
u24 pin;
u16 selcall_id;
u8 unknown4;
} settings;
struct {
u8 unknown5:3, tx_equals_rx:1, usb:1, scan:1, selcall:1, alarm:1;
ul16 rx0_15;
u8 tx0_3:4, rx16_19:4;
ul16 tx4_19;
ul16 vnum;
} memory[255];
"""
HF90_CONFIG_ADDR = 0x1800
HF90_CONFIG_LEN = 0x800
def to_ihex(addr, kind, data):
rec = [len(data) & 0xff, (addr >> 8) & 0xff, addr & 0xff, kind & 0xff]
for d in data:
rec.append(d)
csum = 0
for n in rec:
csum += n
rec.append((-csum) & 0xff)
return rec
def reset_buffer(pipe):
pipe.reset_input_buffer()
timeout = pipe.timeout
pipe.timeout = 0.05
junk = pipe.read(32)
count = len(junk)
if count > 0:
LOG.debug("reset_buffer: discarded %d bytes" % count)
LOG.debug(util.hexprint(junk))
junk = pipe.read(1)
count = len(junk)
pipe.timeout = timeout
if count > 0:
raise errors.RadioError("Unable to clean serial buffer. "
"Please check your serial port selection.")
class HF90StyleRadio(chirp_common.CloneModeRadio,
chirp_common.ExperimentalRadio):
"""Base class for HF-90 radio variants"""
VENDOR = "Q-MAC"
MODEL = "HF-90"
MODES = ["LSB", "USB"]
DUPLEX = ["", "+", "-", "split", "off"]
BAUD_RATE = 4800
HF90_POWER_LEVELS = ["Low", "High"]
_memsize = HF90_CONFIG_ADDR + HF90_CONFIG_LEN
_lower = 1
_upper = 255
def _send(self, data):
LOG.debug("PC->R: (%2i)\n%s" % (len(data), util.hexprint(data)))
self.pipe.write(data)
echo = self.pipe.read(len(data))
if echo != data:
LOG.error("Bad echo: (%2i)\n%s" % (len(data), util.hexprint(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 _serial_prompt(self):
reset_buffer(self.pipe)
self._send(b'\r')
resp = self._read(3)
if not resp:
raise errors.RadioNoResponse()
return resp == b'\r\n#'
def _download_chunk(self, addr):
if addr % 16:
raise Exception("Addr 0x%04x not on 16-byte boundary" % addr)
# Response format:
# 1. Four-digit address, followed by a space
# 2. 16 pairs of hex digits, each pair followed by a space
# 3. One additional space
# 4. 16 char ASCII representation
# 5. \r\n
resp_len = 5 + 48 + 1 + 16 + 2
resp = self._read(resp_len).strip()
if b' ' not in resp:
raise errors.RadioError("Incorrect data received from radio")
words = resp.split(b' ')
if addr != int(words[0], 16):
raise errors.RadioError("Incorrect address received from radio")
data = []
for hh in words[1:17]:
data.append(int(hh, 16))
if len(data) != 16:
LOG.debug(data)
raise errors.RadioError("Radio sent %d bytes" % len(data))
return bytes(data)
def _download(self, limit):
self.pipe.baudrate = self.BAUD_RATE
if not self._serial_prompt():
raise errors.RadioError("Did not get initial prompt from radio")
data = b'\x00' * HF90_CONFIG_ADDR
cmd = b'D%04X%04X' % (HF90_CONFIG_ADDR, limit - HF90_CONFIG_ADDR)
self._send(cmd)
resp = self._read(2)
if resp != b'\r\n':
raise errors.RadioError("Incorrect separator received from radio")
for addr in range(HF90_CONFIG_ADDR, limit, 16):
data += self._download_chunk(addr)
if self.status_fn:
status = chirp_common.Status()
status.cur = addr + 16 - HF90_CONFIG_ADDR
status.max = limit - HF90_CONFIG_ADDR
status.msg = "Downloading from radio"
self.status_fn(status)
resp = self._read(3)
if resp != b'\r\n#':
raise errors.RadioError("Incorrect response from radio")
return memmap.MemoryMapBytes(data)
def _upload_chunk(self, addr):
if addr % 16:
raise Exception("Addr 0x%04x not on 16-byte boundary" % addr)
data = self._mmap.get_byte_compatible()[addr:addr + 16]
ihex_rec = to_ihex(addr, 0, list(data[0:16]))
cmd = b':' + b''.join(b'%02X' % b for b in ihex_rec)
LOG.debug(cmd)
self._send(cmd)
resp = self._read(4)
if resp != b'\x13\x11\r\n':
LOG.debug(util.hexprint(resp))
raise errors.RadioError("Did not receive ack from radio")
def _upload(self, limit):
self.pipe.baudrate = self.BAUD_RATE
if not self._serial_prompt():
raise errors.RadioError("Did not get initial prompt from radio")
timeout = self.pipe.timeout
self.pipe.timeout = 1
self._send(b'L0000')
resp = self._read(4)
if resp != b'\r\n\r\n':
raise errors.RadioError("Did not get initial response from radio")
for addr in range(HF90_CONFIG_ADDR, limit, 16):
self._upload_chunk(addr)
if self.status_fn:
status = chirp_common.Status()
status.cur = addr + 16 - HF90_CONFIG_ADDR
status.max = limit - HF90_CONFIG_ADDR
status.msg = "Uploading to radio"
self.status_fn(status)
self._send(b':00000001FF\r')
resp = self._read(5)
if resp != b'\r\n\r\n#':
raise errors.RadioError("Did not get final response from radio")
# Reset radio
# self._send(b'E')
self.pipe.timeout = timeout
def process_mmap(self):
self._memobj = bitwise.parse(HF90_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):
if self._memobj is None:
self.process_mmap()
highest_nonempty = None
lowest_empty = None
for i in range(self._lower, self._upper + 1):
m = self.get_memory(i)
if m.empty:
if lowest_empty is None:
lowest_empty = i
else:
highest_nonempty = i
msgs = self.validate_memory(m)
if msgs:
raise Exception("Memory %d failed validation: %s" %
(i, ", ".join(msgs)))
if highest_nonempty is None:
raise Exception("Cannot upload with no channels defined")
if isinstance(self, EarlyHF90Radio):
if lowest_empty is not None and lowest_empty < highest_nonempty:
raise Exception(
"Cannot upload with gaps between defined channels")
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 - 1])
@classmethod
def match_model(cls, filedata, filename):
return False
@classmethod
def get_prompts(cls):
rp = chirp_common.RadioPrompts()
rp.experimental = ("This radio driver is currently under development."
"\nThere are no known issues with it, but you "
"should proceed with caution.\n")
rp.pre_download = ("Follow these instructions to down/upload:\n"
"1. Turn off your radio.\n"
"2. Connect the serial interface cable.\n"
"3. Turn on your radio.\n"
"4. Hold down 'CHAN UP' and 'CHAN DOWN' buttons "
"until 'RS-232' is displayed.\n"
"5. Click OK to proceed.\n"
"6. After the process completes, cycle power on "
"the radio to exit RS-232 mode.\n"
"Please refer to the CHIRP wiki for information "
"about HF-90 serial port compatibility issues.\n")
rp.pre_upload = rp.pre_download
return rp
def get_features(self):
rf = chirp_common.RadioFeatures()
rf.can_odd_split = True
rf.has_bank = False
rf.has_ctone = False
rf.has_dtcs = False
rf.has_dtcs_polarity = False
rf.has_name = False
rf.has_nostep_tuning = True
rf.has_settings = True
rf.has_tuning_step = False
rf.memory_bounds = (self._lower, self._upper)
rf.valid_bands = self.BANDS
rf.valid_cross_modes = []
rf.valid_duplexes = list(self.DUPLEX)
rf.valid_modes = list(self.MODES)
rf.valid_skips = []
rf.valid_tmodes = []
rf.valid_tones = []
return rf
def apply_txpower(self, setting, obj):
v = setting.value
LOG.debug("apply_power: %s" % v)
obj.tx_power = 3 * self.HF90_POWER_LEVELS.index(str(v))
def apply_type(self, setting, obj):
v = setting.value
LOG.debug("apply_type: %s %d" % (v, obj.type))
obj.type = self.HF90_TYPES.index(str(v))
def apply_selcall_id(self, setting, obj):
v = setting.value
LOG.debug("apply_selcall_id: %s %x" % (v, obj.selcall_id))
if isinstance(self, EarlyHF90Radio):
# BCD, big endian, left aligned, "f" padded
s = "%-4d" % int(v)
s = s.replace(" ", "f")
else:
# BCD, big endian, right aligned, "0" padded
s = "%04d" % int(v)
obj.selcall_id = int(s[0:4], 16)
def _get_selcall_id(self, _settings):
s = "%04x" % _settings.selcall_id
try:
i = int(s.rstrip("f"))
except ValueError:
i = 9999
return i
def get_settings(self):
_settings = self._memobj.settings
LOG.debug("get_settings: %s %s" % (type(self), self.MODEL))
grp = RadioSettingGroup("settings", "Settings")
rs = RadioSetting("type", "Type",
RadioSettingValueList(
self.HF90_TYPES,
current_index=_settings.type))
rs.set_apply_callback(self.apply_type, _settings)
grp.append(rs)
if isinstance(self, EarlyHF90Radio):
rs = RadioSetting("selcall_id", "Selcall ID (0 for standard type)",
RadioSettingValueInteger(
0, 9999,
self._get_selcall_id(_settings)))
rs.set_apply_callback(self.apply_selcall_id, _settings)
grp.append(rs)
else:
rs = RadioSetting("selcall_id", "Selcall ID",
RadioSettingValueInteger(
1, 9999,
self._get_selcall_id(_settings)))
rs.set_apply_callback(self.apply_selcall_id, _settings)
grp.append(rs)
rs = RadioSetting("f_p_program", "Front panel programming",
RadioSettingValueBoolean(_settings.f_p_program))
grp.append(rs)
rs = RadioSetting("auto_tune", "Auto tuning",
RadioSettingValueBoolean(_settings.auto_tune))
grp.append(rs)
rs = RadioSetting("mode_toggle", "Mode toggling",
RadioSettingValueBoolean(_settings.mode_toggle))
grp.append(rs)
hp = int(_settings.tx_power == 3)
rs = RadioSetting("tx_power", "Tx Power",
RadioSettingValueList(self.HF90_POWER_LEVELS,
current_index=hp))
rs.set_apply_callback(self.apply_txpower, _settings)
grp.append(rs)
return RadioSettings(grp)
def set_settings(self, uisettings):
_settings = self._memobj.settings
for element in uisettings:
if not isinstance(element, RadioSetting):
self.set_settings(element)
continue
if not element.changed():
continue
try:
name = element.get_name()
value = element.value
if element.has_apply_callback():
LOG.debug("Using apply callback")
element.run_apply_callback()
else:
setattr(_settings, name, value)
LOG.debug("Setting %s: %s" % (name, value))
except Exception:
LOG.debug(element.get_name())
raise
def get_memory(self, number):
_mem = self._memobj.memory[number - 1]
mem = chirp_common.Memory()
mem.number = number
_rx = _mem.rx0_15 | (_mem.rx16_19 << 16)
_tx = _mem.tx0_3 | (_mem.tx4_19 << 4)
mem.empty = _rx == 0 or _mem.vnum == 0
if not mem.empty:
if (_tx == _rx) ^ _mem.tx_equals_rx:
LOG.warn("get_memory(%d): tx_equals_rx incorrect" % number)
if 0 < _rx * 100 < self._min_rx_freq:
LOG.warn("get_memory(%d): rx frequency too low" % number)
if _rx * 100 >= self._max_freq:
LOG.warn("get_memory(%d): rx frequency too high" % number)
if 0 < _tx * 100 < self._min_tx_freq:
LOG.warn("get_memory(%d): tx frequency too low" % number)
if _tx * 100 >= self._max_freq:
LOG.warn("get_memory(%d): tx frequency too high" % number)
mem.freq = int(_rx) * 100
if _tx == 0:
mem.duplex = "off"
mem.offset = 0
elif _tx > _rx:
mem.duplex = "+"
mem.offset = (int(_tx) - int(_rx)) * 100
elif _tx < _rx:
mem.duplex = "-"
mem.offset = (int(_rx) - int(_tx)) * 100
else:
mem.duplex = ""
mem.offset = 0
if _mem.usb:
mem.mode = "USB"
else:
mem.mode = "LSB"
n = _mem.vnum & 0xff
if n == 0:
n = number
rsg = RadioSettingGroup("Extra", "extra")
rs = RadioSetting("vnum", "Number",
RadioSettingValueInteger(self._lower,
self._upper, n))
rsg.append(rs)
rs = RadioSetting("selcall", "Selcall",
RadioSettingValueBoolean(_mem.selcall))
rsg.append(rs)
rs = RadioSetting("scan", "Scan",
RadioSettingValueBoolean(_mem.scan))
rsg.append(rs)
mem.extra = rsg
return mem
def _get_tx_freq(self, mem):
if mem.duplex == "":
return mem.freq
elif mem.duplex == "split":
return mem.offset
elif mem.duplex == "+":
return mem.freq + mem.offset
elif mem.duplex == "-":
return mem.freq - mem.offset
return 0
def set_memory(self, mem):
_mem = self._memobj.memory[mem.number - 1]
_mem.set_raw("\x00" * 8)
if mem.empty:
return
rx = int(mem.freq / 100)
tx = int(self._get_tx_freq(mem) / 100)
_mem.tx_equals_rx = tx == rx
_mem.rx0_15 = (rx >> 0) & 0xffff
_mem.rx16_19 = (rx >> 16) & 0xf
_mem.tx0_3 = (tx >> 0) & 0xf
_mem.tx4_19 = (tx >> 4) & 0xffff
_mem.usb = mem.mode == "USB"
_mem.vnum = mem.number
for setting in mem.extra:
setattr(_mem, setting.get_name(), int(setting.value))
def validate_memory(self, mem):
msgs = chirp_common.CloneModeRadio.validate_memory(self, mem)
if 0 < self._get_tx_freq(mem) < self._min_tx_freq:
msgs.append(chirp_common.ValidationWarning(
"tx frequency too low"))
if mem.extra and isinstance(self, EarlyHF90Radio):
if self._memobj.settings.selcall_id == 0x0fff:
if mem.extra["scan"].value or mem.extra["selcall"].value:
msgs.append(chirp_common.ValidationWarning(
"standard type but scan or selcall enabled"))
else:
if mem.extra["scan"].value and not mem.extra["selcall"].value:
msgs.append(chirp_common.ValidationWarning(
"scan enabled but selcall disabled"))
return msgs
# EarlyHF90Radio imitates version 2 of the dealer software whereas
# LateHF90Radio imitates version 3. It seems both are needed because
# version 3 warns that it is incompatible with firmware revisions below V301.
# Unfortunately, the author does not have access to the technical information
# required to better accommodate the quirks of the various firmware revisions.
@directory.register
class EarlyHF90Radio(HF90StyleRadio):
"""Base class for early HF-90 radios"""
VARIANT = "v300 or earlier"
_min_rx_freq = 2000000
_min_tx_freq = 2000000
_max_freq = 30000000
BANDS = [(_min_rx_freq, _max_freq)]
HF90_TYPES = ["HF-90A (Australia)", "HF-90E (Export)"]
@directory.register
class LateHF90Radio(HF90StyleRadio):
"""Base class for late HF-90 radios"""
VARIANT = "v301 or later"
_min_rx_freq = 500000
_min_tx_freq = 1800000
_max_freq = 30000000
# band is split into two pieces to avoid unit test failures
BANDS = [(_min_rx_freq, _min_tx_freq), (_min_tx_freq, _max_freq)]
HF90_TYPES = ["Standard", "Advanced"]
if __name__ == "__main__":
import sys
import serial
s = serial.Serial(port=sys.argv[1], baudrate=4800, timeout=0.1)
reset_buffer(s)
s.write(b"\r")
print(s.read(32))
print(s.out_waiting)
chirp-20261030/chirp/drivers/hg_uv98.py 0000644 0001750 0001750 00000056366 15163702041 016477 0 ustar hibby hibby # -*- coding: utf-8 -*-
# Copyright 2022 Masen Furer
#
# 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 .
#
# Lanchonlh HG-UV98 driver written by
# Masen Furer
# With assistance from
# KG7KMV and Bartłomiej Zieliński
# Based on the implementation of Kenwood TK-8102
import logging
import struct
from chirp import chirp_common, directory, memmap, errors, util
from chirp import bitwise
from chirp import kenwood_tone
from chirp.settings import RadioSettingGroup, RadioSetting
from chirp.settings import RadioSettingValueBoolean, RadioSettingValueList
from chirp.settings import RadioSettings
LOG = logging.getLogger(__name__)
MEM_FORMAT = """
struct {
lbcd rx_freq[4];
lbcd tx_freq[4];
ul16 rxtone;
ul16 txtone;
u8 unknown1:6,
wide:1,
highpower:1;
u8 unknown2:5,
bcl:1,
scan:1,
unknown3:1;
u8 unknown4[2];
} memory[130];
#seekto 0x0a00;
struct {
u8 abr; // 0x0a00
u8 save;
u8 ch_a_step;
u8 ch_b_step;
u8 vox_grd;
u8 ch_a_sql;
u8 ch_b_sql;
u8 roger;
u8 ch_a_v_m;
u8 ch_b_v_m;
u8 ch_a_ch_mdf;
u8 ch_b_ch_mdf;
u8 tdr;
u8 unknown5[3];
u8 unknown6[5]; // 0x0a10
u8 english;
u8 beep;
u8 voice;
u8 night_mode;
u8 abr_lv; // backlight level
u8 tot;
u8 toa;
u8 vox_dly;
u8 sc_rev;
u8 lockmode;
u8 autolock;
u8 unknown7; // 0x0a20
u8 pf1_short;
u8 pf1_long;
u8 pf2_short;
u8 pf2_long;
u8 top_short;
u8 top_long;
u8 rpt_rct;
u8 sc_qt;
u8 pri_ch;
u8 pri_scn;
u8 unknown8;
u8 aprs_rx_band;
u8 ch_a_mute;
u8 ch_b_mute;
u8 unknown9[7]; // 0x0a30
u8 tx_priority;
u8 aprs_rx_popup;
u8 aprs_rx_tone;
u8 aprs_tx_tone;
u8 unknown10;
u8 auto_lock_dly;
u8 menu_dly;
u8 beacon_exit_dly;
u8 unknown11;
u8 unknown12[2]; // 0x0a40
u8 ch_a_mem_ch;
u8 ch_b_mem_ch;
u8 unknown13[12];
} settings;
#seekto 0x1000;
struct {
char name[11];
u8 unknown[5];
} name[128];
struct {
char callsign[9];
u8 null;
} aprs;
#seekto 0x1f80;
struct {
ul32 unknown[8];
} unknown_settings;
"""
BOUNDS = [(136000000, 174000000), (400000000, 500000000)]
OFFSETS = [600000, 5000000]
MAX_CHANNELS = 128
MAX_NAME = 8
NAME_FIELD_SIZE = 11
CHUNK_SIZE = 64
MAX_ADDR = 0x2000
POWER_LEVELS = [chirp_common.PowerLevel("Low", watts=1),
chirp_common.PowerLevel("High", watts=5)]
MODES = ["FM", "NFM"]
SPECIAL_CHANNELS = ['VFO-A', 'VFO-B']
# Settings maps
ABR_LIST = [str(v) for v in range(0, 151, 5)]
STEP_LIST = ["5.0", "6.25", "10.0", "12.5", "25.0", "50.0", "100.0"]
VOX_LIST = ["OFF", "1", "2", "3", "4", "5", "6", "7", "8", "9"]
SQL_LIST = [str(v) for v in range(0, 10)]
ROGER_LIST = ["OFF", "BEGIN", "END", "BOTH"]
MDF_LIST = ["NUM+FREQ", "NUMBER", "NAME"]
VM_LIST = ["VFO", "MEMORY"]
LANG_LIST = ["CHINESE", "ENGLISH"]
ABR_LV_LIST = [str(v) for v in range(1, 11)]
TOT_LIST = [str(v) for v in range(0, 601, 15)]
TOA_LIST = ["OFF"] + ["{} S".format(v) for v in range(1, 11)]
VOX_DLY_LIST = [str(v) for v in range(1, 11)]
SC_REV_LIST = ["TIME", "BUSY", "HOLD"]
LOCKMODE_LIST = ["KEY", "KEY+DIAL", "KEY+DIAL+PTT"]
AUTOLOCK_LIST = ["AUTO", "Manual"]
PF1_LIST = ["BACK LIGHT", "SCAN", "SQUELCH", "TORCH", "BAND A/B"]
PF2_LIST = ["BEACON", "LIST", "TORCH", "BACK LIGHT"]
TOP_LIST = ["ALERT", "REMOTE ALERT", "TORCH", "TXP", "CH MDF", "OFF_NET_KEY"]
SC_QT_LIST = ["Decode", "Encode", "Decode+Encode"]
APRS_RX_LIST = ["OFF", "BAND A", "BAND B"]
TX_PRIORITY_LIST = ["VOICE", "APRS"]
AUTOLOCK_DLY_LIST = ["{} S".format(v) for v in range(5, 31)]
BEACON_EXIT_DLY_LIST = MENU_DLY_LIST = AUTOLOCK_DLY_LIST
def make_frame(cmd, addr, length, data=b""):
if not isinstance(data, bytes):
data = data.decode("ascii")
return struct.pack(">BHB", ord(cmd), addr, length) + data
def send(radio, frame):
LOG.debug("%04i P>R: %s" % (len(frame), util.hexprint(frame)))
radio.pipe.write(frame)
def recv(radio, readdata=True):
hdr = radio.pipe.read(4)
cmd, addr, length = struct.unpack(">BHB", hdr)
if readdata:
data = radio.pipe.read(length)
LOG.debug(" PTone",
"DTCS->",
"->DTCS",
"Tone->DTCS",
"DTCS->Tone",
"->Tone",
"DTCS->DTCS"]
rf.valid_power_levels = POWER_LEVELS
rf.valid_skips = ["", "S"]
rf.valid_bands = [(136000000, 174000000), (400000000, 500000000)]
rf.valid_name_length = 8
rf.valid_special_chans = SPECIAL_CHANNELS
rf.memory_bounds = (1, self._upper)
return rf
def sync_in(self):
try:
self._mmap = do_download(self)
except errors.RadioError:
self.pipe.write(b"\x45")
raise
except Exception as e:
raise errors.RadioError("Failed to download from radio: %s" % e)
self.process_mmap()
def process_mmap(self):
self._memobj = bitwise.parse(MEM_FORMAT, self._mmap)
def sync_out(self):
try:
do_upload(self)
except errors.RadioError:
self.pipe.write(b"\x45")
raise
except Exception as e:
raise errors.RadioError("Failed to upload to radio: %s" % e)
def get_raw_memory(self, number):
return repr(self._memobj.memory[number - 1])
def get_memory(self, number):
mem = chirp_common.Memory()
if isinstance(number, str):
mem.number = MAX_CHANNELS + SPECIAL_CHANNELS.index(number) + 1
mem.extd_number = number
elif number > MAX_CHANNELS:
mem.number = number
else:
mem.number = number
_name = self._memobj.name[mem.number - 1]
_mem = self._memobj.memory[mem.number - 1]
if mem.number > MAX_CHANNELS:
mem.immutable = ['name']
else:
mem.name, _, _ = _name.name.get_raw().partition(b"\xFF")
mem.name = mem.name.decode('ascii').rstrip()
if _mem.get_raw()[:4] == b"\xFF\xFF\xFF\xFF":
mem.empty = True
return mem
mem.freq = int(_mem.rx_freq) * 10
offset = (int(_mem.tx_freq) * 10) - mem.freq
if offset < 0:
mem.offset = abs(offset)
mem.duplex = "-"
elif offset > 0:
mem.offset = offset
mem.duplex = "+"
else:
mem.offset = 0
self._tone_model.get_tone(_mem, mem)
mem.power = POWER_LEVELS[_mem.highpower]
mem.mode = MODES[_mem.wide]
mem.skip = not _mem.scan and "S" or ""
mem.extra = RadioSettingGroup("all", "All Settings")
bcl = RadioSetting("bcl", "Busy Channel Lockout",
RadioSettingValueBoolean(bool(_mem.bcl)))
mem.extra.append(bcl)
return mem
def set_memory(self, mem):
_mem = self._memobj.memory[mem.number - 1]
if mem.empty:
_mem.set_raw(b"\xFF" * 16)
return
if mem.number < 129:
_name = self._memobj.name[mem.number - 1]
_namelength = self.get_features().valid_name_length
for i in range(NAME_FIELD_SIZE):
try:
_name.name[i] = mem.name[i]
except IndexError:
_name.name[i] = "\xFF"
# clear reserved fields
_mem.unknown1 = 0xFF
_mem.unknown2 = 0xFF
_mem.unknown3 = 0xFF
_mem.unknown4 = (0xFF, 0xFF)
_mem.rx_freq = mem.freq / 10
mem_offset = mem.offset or offset_for(mem.freq)
if mem.duplex == "+":
_mem.tx_freq = (mem.freq + mem_offset) / 10
elif mem.duplex == "-":
_mem.tx_freq = (mem.freq - mem_offset) / 10
else:
_mem.tx_freq = mem.freq / 10
self._tone_model.set_tone(mem, _mem)
_mem.highpower = mem.power == POWER_LEVELS[1]
_mem.wide = mem.mode == "NFM"
_mem.scan = mem.skip != "S"
for setting in mem.extra:
setattr(_mem, setting.get_name(), setting.value)
def get_settings(self):
_mem = self._memobj
_settings = _mem.settings
basic = RadioSettingGroup("basic", "Basic")
display = RadioSettingGroup("display", "Display")
scan = RadioSettingGroup("scan", "Scan")
buttons = RadioSettingGroup("buttons", "Buttons")
vfo = RadioSettingGroup("vfo", "VFO")
advanced = RadioSettingGroup("advanced", "Advanced")
aprs = RadioSettingGroup("aprs", "APRS")
top = RadioSettings(basic, display, scan, buttons, vfo, advanced, aprs)
basic.append(
RadioSetting("save", "Power Save",
RadioSettingValueBoolean(_settings.save)))
basic.append(
RadioSetting("roger", "Roger Beep",
RadioSettingValueList(ROGER_LIST,
current_index=_settings.roger)))
basic.append(
RadioSetting("beep", "System Beep",
RadioSettingValueBoolean(_settings.beep)))
basic.append(
RadioSetting("tot", "Timeout Timer (sec)",
RadioSettingValueList(TOT_LIST,
current_index=_settings.tot)))
basic.append(
RadioSetting("toa", "Timeout Timer Alarm",
RadioSettingValueList(TOA_LIST,
current_index=_settings.toa)))
basic.append(
RadioSetting("lockmode", "Lock Mode",
RadioSettingValueList(
LOCKMODE_LIST,
current_index=_settings.lockmode)))
basic.append(
RadioSetting("autolock", "Auto Lock",
RadioSettingValueList(
AUTOLOCK_LIST,
current_index=_settings.autolock)))
basic.append(
RadioSetting("auto_lock_dly", "Auto Lock Delay",
RadioSettingValueList(
AUTOLOCK_DLY_LIST,
current_index=_settings.auto_lock_dly)))
display.append(
RadioSetting("abr", "Screen Save",
RadioSettingValueList(ABR_LIST,
current_index=_settings.abr)))
display.append(
RadioSetting("abr_lv", "Back Light Brightness",
RadioSettingValueList(ABR_LV_LIST,
current_index=_settings.abr_lv)))
display.append(
RadioSetting("night_mode", "Night Mode (Light on Dark)",
RadioSettingValueBoolean(_settings.night_mode)))
display.append(
RadioSetting("menu_dly", "Menu Delay",
RadioSettingValueList(
MENU_DLY_LIST,
current_index=_settings.menu_dly)))
display.append(
RadioSetting("english", "Language",
RadioSettingValueList(LANG_LIST,
current_index=_settings.english)))
scan.append(
RadioSetting("pri_scn", "Priority Scan",
RadioSettingValueBoolean(_settings.pri_scn)))
scan.append(
RadioSetting("pri_ch", "Priority Channel",
RadioSettingValueChannel(self, _settings.pri_ch)))
scan.append(
RadioSetting("sc_rev", "Scan Resume",
RadioSettingValueList(SC_REV_LIST,
current_index=_settings.sc_rev)))
scan.append(
RadioSetting("sc_qt", "Code Save",
RadioSettingValueList(SC_QT_LIST,
current_index=_settings.sc_qt)))
buttons.append(
RadioSetting("pf1_short", "PF1 (Side, Upper) Button Short Press",
RadioSettingValueList(PF1_LIST,
current_index=_settings.pf1_short)))
buttons.append(
RadioSetting("pf1_long", "PF1 (Side, Upper) Button Long Press",
RadioSettingValueList(PF1_LIST,
current_index=_settings.pf1_long)))
buttons.append(
RadioSetting("pf2_short", "PF2 (Side, Lower) Button Short Press",
RadioSettingValueList(PF2_LIST,
current_index=_settings.pf2_short)))
buttons.append(
RadioSetting("pf2_long", "PF2 (Side, Lower) Button Long Press",
RadioSettingValueList(PF2_LIST,
current_index=_settings.pf2_long)))
buttons.append(
RadioSetting("top_short", "Top Button Short Press",
RadioSettingValueList(TOP_LIST,
current_index=_settings.top_short)))
buttons.append(
RadioSetting("top_long", "Top Button Long Press",
RadioSettingValueList(TOP_LIST,
current_index=_settings.top_long)))
vfo.append(
RadioSetting("tdr", "VFO B Enabled",
RadioSettingValueBoolean(_settings.tdr)))
vfo.append(
RadioSetting("ch_a_step", "VFO Frequency Step (A)",
RadioSettingValueList(
STEP_LIST,
current_index=_settings.ch_a_step)))
vfo.append(
RadioSetting("ch_b_step", "VFO Frequency Step (B)",
RadioSettingValueList(
STEP_LIST,
current_index=_settings.ch_b_step)))
vfo.append(
RadioSetting("ch_a_sql", "Squelch (A)",
RadioSettingValueList(SQL_LIST,
current_index=_settings.ch_a_sql)))
vfo.append(
RadioSetting("ch_b_sql", "Squelch (B)",
RadioSettingValueList(SQL_LIST,
current_index=_settings.ch_b_sql)))
vfo.append(
RadioSetting("ch_a_mem_ch", "Memory Channel (A)",
RadioSettingValueChannel(self,
_settings.ch_a_mem_ch)))
vfo.append(
RadioSetting("ch_b_mem_ch", "Memory Channel (B)",
RadioSettingValueChannel(self,
_settings.ch_b_mem_ch)))
vfo.append(
RadioSetting("ch_a_ch_mdf", "Memory Display Format (A)",
RadioSettingValueList(
MDF_LIST,
current_index=_settings.ch_a_ch_mdf)))
vfo.append(
RadioSetting("ch_b_ch_mdf", "Memory Display Format (B)",
RadioSettingValueList(
MDF_LIST,
current_index=_settings.ch_b_ch_mdf)))
vfo.append(
RadioSetting("ch_a_v_m", "VFO/MEM (A)",
RadioSettingValueList(
VM_LIST, current_index=_settings.ch_a_v_m)))
vfo.append(
RadioSetting("ch_b_v_m", "VFO/MEM (B)",
RadioSettingValueList(
VM_LIST, current_index=_settings.ch_b_v_m)))
advanced.append(
RadioSetting("vox_grd", "VOX Sensitivity",
RadioSettingValueList(
VOX_LIST, current_index=_settings.vox_grd)))
advanced.append(
RadioSetting("vox_dly", "VOX Delay",
RadioSettingValueList(
VOX_DLY_LIST, current_index=_settings.vox_dly)))
advanced.append(
RadioSetting("voice", "Voice Assist",
RadioSettingValueBoolean(_settings.voice)))
advanced.append(
RadioSetting("rpt_rct", "RPT Roger",
RadioSettingValueBoolean(_settings.rpt_rct)))
aprs.append(
RadioSetting("aprs_rx_band", "RX Band",
RadioSettingValueList(
APRS_RX_LIST,
current_index=_settings.aprs_rx_band)))
aprs.append(
RadioSetting("ch_a_mute", "Band A Mute",
RadioSettingValueBoolean(_settings.ch_a_mute)))
aprs.append(
RadioSetting("ch_b_mute", "Band B Mute",
RadioSettingValueBoolean(_settings.ch_b_mute)))
aprs.append(
RadioSetting("tx_priority", "TX Priority",
RadioSettingValueList(
TX_PRIORITY_LIST,
current_index=_settings.tx_priority)))
aprs.append(
RadioSetting("aprs_rx_popup", "APRS Popup",
RadioSettingValueBoolean(_settings.aprs_rx_popup)))
aprs.append(
RadioSetting("aprs_rx_tone", "RX Tone",
RadioSettingValueBoolean(_settings.aprs_rx_tone)))
aprs.append(
RadioSetting("aprs_tx_tone", "TX Tone",
RadioSettingValueBoolean(_settings.aprs_tx_tone)))
aprs.append(
RadioSetting("beacon_exit_dly", "Beacon Message Delay",
RadioSettingValueList(
BEACON_EXIT_DLY_LIST,
current_index=_settings.beacon_exit_dly)))
return top
def set_settings(self, settings):
_mem = self._memobj
_settings = _mem.settings
for element in settings:
if not isinstance(element, RadioSetting):
self.set_settings(element)
continue
name = element.get_name()
value = element.value
if hasattr(_settings, name):
setattr(_settings, name, value)
chirp-20261030/chirp/drivers/hobbypcb.py 0000644 0001750 0001750 00000027520 15172631605 016773 0 ustar hibby hibby # Copyright 2016 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 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 .
import logging
import time
from chirp import chirp_common, directory, errors
from chirp.settings import RadioSetting, RadioSettingGroup, \
RadioSettingValueInteger, RadioSettingValueList, \
RadioSettingValueBoolean, RadioSettingValueString, \
RadioSettings
LOG = logging.getLogger(__name__)
BAUDS = [1200, 4800, 9600, 19200, 38400, 57600]
POWER_LEVELS = [chirp_common.PowerLevel('Low', dBm=10),
chirp_common.PowerLevel('High', dBm=24)]
TONE_MODES = ['', 'Tone', 'TSQL', '']
def detect_baudrate(radio):
bauds = list(BAUDS)
bauds.remove(radio.pipe.baudrate)
bauds.insert(0, radio.pipe.baudrate)
for baud in bauds:
radio.pipe.baudrate = baud
radio.pipe.timeout = 0.5
radio.pipe.write(b'\rFW?\r')
resp = radio.pipe.read(2)
if resp.strip().startswith(b'FW'):
resp += radio.pipe.read(16)
LOG.info('HobbyPCB %r at baud rate %i' % (resp.strip(), baud))
return baud
@directory.register
class HobbyPCBRSUV3Radio(chirp_common.LiveRadio):
"""HobbyPCB RS-UV3"""
VENDOR = "HobbyPCB"
MODEL = "RS-UV3"
BAUD_RATE = 19200
def __init__(self, *args, **kwargs):
super(HobbyPCBRSUV3Radio, self).__init__(*args, **kwargs)
if self.pipe:
baud = detect_baudrate(self)
if not baud:
raise errors.RadioNoResponse()
def _cmd(self, command, rsize=None):
command = command.encode()
LOG.debug('> %s' % command)
self.pipe.write(b'%s\r' % command)
resp = b''
if rsize is None:
complete = lambda: time.sleep(0.1) is None
elif rsize == 0:
rsize = 1
complete = lambda: resp.endswith(b'\r')
else:
complete = lambda: len(resp) >= rsize
while not complete():
chunk = self.pipe.read(rsize)
if not chunk:
break
resp += chunk
LOG.debug('< %r [%i]' % (resp, len(resp)))
return resp.decode().strip()
def get_features(self):
rf = chirp_common.RadioFeatures()
rf.has_bank = False
rf.has_name = False
rf.has_cross = False
rf.has_dtcs = False
rf.has_rx_dtcs = False
rf.has_dtcs_polarity = False
rf.has_tuning_step = False
rf.has_mode = False
rf.has_settings = True
rf.memory_bounds = (1, 9) # This radio supports memories 0-9
rf.valid_bands = [(144000000, 148000000),
(220000000, 222000000),
(440000000, 450000000),
]
rf.valid_tmodes = TONE_MODES
rf.valid_power_levels = POWER_LEVELS
return rf
def get_memory(self, number):
_mem = self._cmd('CP%i' % number, 33).split('\r')
LOG.debug('Memory elements: %r' % _mem)
mem = chirp_common.Memory()
mem.number = number
mem.freq = int(_mem[0]) * 1000
txfreq = int(_mem[1]) * 1000
mem.offset = abs(txfreq - mem.freq)
if mem.freq < txfreq:
mem.duplex = '+'
elif mem.freq > txfreq:
mem.duplex = '-'
else:
mem.duplex = ''
mem.ctone = int(_mem[2]) / 100.0
mem.rtone = mem.ctone
mem.tmode = TONE_MODES[int(_mem[3])]
mem.power = POWER_LEVELS[int(_mem[5])]
return mem
def set_memory(self, mem):
if mem.tmode in ['', 'Tone']:
tone = mem.rtone * 100
else:
tone = mem.ctone * 100
if mem.duplex == '+':
self._cmd('FT%06i' % ((mem.freq + mem.offset) / 1000))
self._cmd('FR%06i' % (mem.freq / 1000))
elif mem.duplex == '-':
self._cmd('FT%06i' % ((mem.freq - mem.offset) / 1000))
self._cmd('FR%06i' % (mem.freq / 1000))
else:
self._cmd('FS%06i' % (mem.freq / 1000))
self._cmd('TM%i' % TONE_MODES.index(mem.tmode))
self._cmd('TF%05i' % tone)
self._cmd('PW%i' % POWER_LEVELS.index(mem.power))
self._cmd('ST%i' % mem.number)
def get_settings(self):
def _get(cmd):
return self._cmd('%s?' % cmd, 0).split(':')[1].strip()
cw = RadioSettingGroup('beacon', 'Beacon Settings')
cl = RadioSetting('CL%15s', 'CW Callsign',
RadioSettingValueString(0, 15,
_get('CL')))
cw.append(cl)
cf = RadioSetting('CF%4i', 'CW Audio Frequency',
RadioSettingValueInteger(400, 1300,
int(_get('CF'))))
cw.append(cf)
cs = RadioSetting('CS%02i', 'CW Speed',
RadioSettingValueInteger(5, 25,
int(_get('CS'))))
cw.append(cs)
bc = RadioSetting('BC%03i', 'CW Beacon Timer',
RadioSettingValueInteger(0, 600,
int(_get('BC'))))
cw.append(bc)
bm = RadioSetting('BM%15s', 'Beacon Message',
RadioSettingValueString(0, 15,
_get('BM')))
cw.append(bm)
bt = RadioSetting('BT%03i', 'Beacon Timer',
RadioSettingValueInteger(0, 600,
int(_get('BT'))))
cw.append(bt)
it = RadioSetting('IT%03i', 'CW ID Timer',
RadioSettingValueInteger(0, 500,
int(_get('IT'))))
cw.append(it)
tg = RadioSetting('TG%7s', 'CW Timeout Message',
RadioSettingValueString(0, 7,
_get('TG')))
cw.append(tg)
io = RadioSettingGroup('io', 'IO')
af = RadioSetting('AF%i', 'Arduino LPF',
RadioSettingValueBoolean(_get('AF') == 'ON'))
io.append(af)
input_pin = ['OFF', 'SQ OPEN', 'PTT']
ai = RadioSetting('AI%i', 'Arduino Input Pin',
RadioSettingValueList(
input_pin,
current_index=int(_get('AI'))))
io.append(ai)
output_pin = ['LOW', 'SQ OPEN', 'DTMF DETECT', 'TX ON', 'CTCSS DET',
'HIGH']
ao = RadioSetting('AO%i', 'Arduino Output Pin',
RadioSettingValueList(
output_pin,
current_index=int(_get('AO'))))
io.append(ao)
bauds = [str(x) for x in BAUDS]
b1 = RadioSetting('B1%i', 'Arduino Baudrate',
RadioSettingValueList(
bauds,
current_index=int(_get('B1'))))
io.append(b1)
b2 = RadioSetting('B2%i', 'Main Baudrate',
RadioSettingValueList(
bauds,
current_index=int(_get('B2'))))
io.append(b2)
dtmf = RadioSettingGroup('dtmf', 'DTMF Settings')
dd = RadioSetting('DD%04i', 'DTMF Tone Duration',
RadioSettingValueInteger(50, 2000,
int(_get('DD'))))
dtmf.append(dd)
dr = RadioSetting('DR%i', 'DTMF Tone Detector',
RadioSettingValueBoolean(_get('DR') == 'ON'))
dtmf.append(dr)
gt = RadioSetting('GT%02i', 'DTMF/CW Tone Gain',
RadioSettingValueInteger(0, 15,
int(_get('GT'))))
dtmf.append(gt)
sd = RadioSetting('SD%i', 'DTMF/CW Side Tone',
RadioSettingValueBoolean(_get('SD') == 'ON'))
dtmf.append(sd)
general = RadioSettingGroup('general', 'General')
dp = RadioSetting('DP%i', 'Pre-Emphasis',
RadioSettingValueBoolean(_get('DP') == 'ON'))
general.append(dp)
fw = RadioSetting('_fw', 'Firmware Version',
RadioSettingValueString(0, 20,
_get('FW')))
general.append(fw)
gm = RadioSetting('GM%02i', 'Mic Gain',
RadioSettingValueInteger(0, 15,
int(_get('GM'))))
general.append(gm)
hp = RadioSetting('HP%i', 'Audio High-Pass Filter',
RadioSettingValueBoolean(_get('HP') == 'ON'))
general.append(hp)
ht = RadioSetting('HT%04i', 'Hang Time',
RadioSettingValueInteger(0, 5000,
int(_get('HT'))))
general.append(ht)
ledmode = ['OFF', 'ON', 'SQ OPEN', 'BATT CHG STAT']
ld = RadioSetting('LD%i', 'LED Mode',
RadioSettingValueList(
ledmode,
current_index=int(_get('LD'))))
general.append(ld)
sq = RadioSetting('SQ%i', 'Squelch Level',
RadioSettingValueInteger(0, 9,
int(_get('SQ'))))
general.append(sq)
to = RadioSetting('TO%03i', 'Timeout Timer',
RadioSettingValueInteger(0, 600,
int(_get('TO'))))
general.append(to)
vu = RadioSetting('VU%02i', 'Receiver Audio Volume',
RadioSettingValueInteger(0, 39,
int(_get('VU'))))
general.append(vu)
rc = RadioSetting('RC%i', 'Current Channel',
RadioSettingValueInteger(0, 9, 0))
rc.set_doc('Choosing one of these values causes the radio '
'to change to the selected channel. The radio '
'cannot tell CHIRP what channel is selected.')
general.append(rc)
return RadioSettings(general, cw, io, dtmf)
def set_settings(self, settings):
def _set(thing):
# Try to only set something if it's new
query = '%s?' % thing[:2]
cur = self._cmd(query, 0)
if cur.strip():
cur = cur.split()[1].strip()
new = thing[2:].strip()
if cur in ['ON', 'OFF']:
cur = int(cur == 'ON')
new = int(new)
elif cur.isdigit():
cur = int(cur)
new = int(new)
if new != cur:
LOG.info('Setting %s (%r != %r)' % (thing, cur, new))
self._cmd(thing)
time.sleep(1)
for group in settings:
for setting in group:
if setting.get_name().startswith('_'):
LOG.debug('Skipping %s' % setting)
continue
cmd = setting.get_name()
value = setting.value.get_value()
if hasattr(setting.value, '_options'):
value = setting.value._options.index(value)
fullcmd = (cmd % value).strip()
_set(fullcmd)
chirp-20261030/chirp/drivers/ic208.py 0000644 0001750 0001750 00000017565 15257672455 016054 0 ustar hibby hibby # 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.drivers import icf
from chirp import chirp_common, directory, bitwise
MEM_FORMAT = """
struct memory {
u24 freq;
u16 offset;
u8 power:2,
rtone:6;
u8 duplex:2,
ctone:6;
u8 unknown1:1,
dtcs:7;
u8 tuning_step:4,
unknown2:4;
u8 unknown3;
u8 alt_mult:1,
unknown4:1,
is_fm:1,
is_wide:1,
unknown5:2,
tmode:2;
u16 dtcs_polarity:2,
usealpha:1,
empty:1,
name1:6,
name2:6;
u24 name3:6,
name4:6,
name5:6,
name6:6;
};
struct memory memory[510];
struct {
u8 unknown1:1,
empty:1,
pskip:1,
skip:1,
bank:4;
} flags[512];
struct memory call[2];
"""
MODES = ["AM", "FM", "NFM", "NAM"]
TMODES = ["", "Tone", "TSQL", "DTCS"]
DUPLEX = ["", "", "-", "+"]
DTCS_POL = ["NN", "NR", "RN", "RR"]
STEPS = [5.0, 10.0, 12.5, 15, 20.0, 25.0, 30.0, 50.0, 100.0, 200.0]
POWER = [chirp_common.PowerLevel("High", watts=50),
chirp_common.PowerLevel("Low", watts=5),
chirp_common.PowerLevel("Mid", watts=15),
]
IC208_SPECIAL = []
for i in range(1, 6):
IC208_SPECIAL.append("%iA" % i)
IC208_SPECIAL.append("%iB" % i)
CHARSET = dict(list(zip([0x00, 0x08, 0x09, 0x0a, 0x0b, 0x0d, 0x0f],
" ()*+-/")) +
list(zip(list(range(0x10, 0x1a)), "0123456789")) +
[(0x1c, '|'), (0x1d, '=')] +
list(zip(list(range(0x21, 0x3b)),
"ABCDEFGHIJKLMNOPQRSTUVWXYZ")))
CHARSET_REV = dict(list(zip(list(CHARSET.values()), list(CHARSET.keys()))))
def get_name(_mem):
"""Decode the name from @_mem"""
def _get_char(val):
try:
return CHARSET[int(val)]
except KeyError:
return "*"
name_bytes = [_mem.name1, _mem.name2, _mem.name3,
_mem.name4, _mem.name5, _mem.name6]
name = ""
for val in name_bytes:
name += _get_char(val)
return name.rstrip()
def set_name(_mem, name):
"""Encode @name in @_mem"""
def _get_index(char):
try:
return CHARSET_REV[char]
except KeyError:
return CHARSET_REV["*"]
name = name.ljust(6)[:6]
_mem.usealpha = bool(name.strip())
# The element override calling convention makes this harder to automate.
# It's just six, so do it manually
_mem.name1 = _get_index(name[0])
_mem.name2 = _get_index(name[1])
_mem.name3 = _get_index(name[2])
_mem.name4 = _get_index(name[3])
_mem.name5 = _get_index(name[4])
_mem.name6 = _get_index(name[5])
@directory.register
class IC208Radio(icf.IcomCloneModeRadio):
"""Icom IC800"""
VENDOR = "Icom"
MODEL = "IC-208H"
_model = "\x26\x32\x00\x01"
_memsize = 0x2600
_endframe = "Icom Inc\x2e30"
_can_hispeed = True
_memories = []
_ranges = [(0x0000, 0x2600, 32)]
@classmethod
def match_model(cls, filedata, filename):
return (super().match_model(filedata, filename) or
(cls._memsize and len(filedata) == cls._memsize))
def get_features(self):
rf = chirp_common.RadioFeatures()
rf.memory_bounds = (1, 500)
rf.has_bank = True
rf.valid_tuning_steps = list(STEPS)
rf.valid_tmodes = list(TMODES)
rf.valid_modes = list(MODES)
rf.valid_duplexes = list(DUPLEX)
rf.valid_power_levels = list(POWER)
rf.valid_skips = ["", "S", "P"]
rf.valid_bands = [(118000000, 174000000),
(230000000, 550000000),
(810000000, 999995000)]
rf.valid_special_chans = ["C1", "C2"] + sorted(IC208_SPECIAL)
rf.valid_characters = "".join(list(CHARSET.values()))
return rf
def get_raw_memory(self, number):
_mem, _flg, index = self._get_memory(number)
return repr(_mem) + repr(_flg)
def process_mmap(self):
self._memobj = bitwise.parse(MEM_FORMAT, self._mmap)
def _get_bank(self, loc):
_flg = self._memobj.flags[loc-1]
if _flg.bank >= 0x0A:
return None
else:
return _flg.bank
def _set_bank(self, loc, bank):
_flg = self._memobj.flags[loc-1]
if bank is None:
_flg.bank = 0x0A
else:
_flg.bank = bank
def _get_memory(self, number):
if isinstance(number, str):
if "A" in number or "B" in number:
index = 501 + IC208_SPECIAL.index(number)
_mem = self._memobj.memory[index - 1]
_flg = self._memobj.flags[index - 1]
else:
index = int(number[1]) - 1
_mem = self._memobj.call[index]
_flg = self._memobj.flags[510 + index]
index = index + -10
elif number <= 0:
index = 10 - abs(number)
_mem = self._memobj.call[index]
_flg = self._memobj.flags[index + 510]
else:
index = number
_mem = self._memobj.memory[number - 1]
_flg = self._memobj.flags[number - 1]
return _mem, _flg, index
def get_memory(self, number):
_mem, _flg, index = self._get_memory(number)
mem = chirp_common.Memory()
mem.number = index
if isinstance(number, str):
mem.extd_number = number
else:
mem.skip = _flg.pskip and "P" or _flg.skip and "S" or ""
if _flg.empty:
mem.empty = True
return mem
mult = _mem.alt_mult and 6250 or 5000
mem.freq = int(_mem.freq) * mult
mem.offset = int(_mem.offset) * 5000
mem.rtone = chirp_common.TONES[_mem.rtone]
mem.ctone = chirp_common.TONES[_mem.ctone]
mem.dtcs = chirp_common.DTCS_CODES[_mem.dtcs]
mem.dtcs_polarity = DTCS_POL[_mem.dtcs_polarity]
mem.duplex = DUPLEX[_mem.duplex]
mem.tmode = TMODES[_mem.tmode]
mem.mode = ((not _mem.is_wide and "N" or "") +
(_mem.is_fm and "FM" or "AM"))
mem.tuning_step = STEPS[_mem.tuning_step]
mem.name = get_name(_mem)
mem.power = POWER[_mem.power]
return mem
def set_memory(self, mem):
_mem, _flg, index = self._get_memory(mem.number)
if mem.empty:
_flg.empty = True
self._set_bank(mem.number, None)
return
if _flg.empty:
_mem.set_raw("\x00" * 16)
_flg.empty = False
_mem.alt_mult = chirp_common.is_fractional_step(mem.freq)
_mem.freq = mem.freq / (_mem.alt_mult and 6250 or 5000)
_mem.offset = mem.offset / 5000
_mem.rtone = chirp_common.TONES.index(mem.rtone)
_mem.ctone = chirp_common.TONES.index(mem.ctone)
_mem.dtcs = chirp_common.DTCS_CODES.index(mem.dtcs)
_mem.dtcs_polarity = DTCS_POL.index(mem.dtcs_polarity)
_mem.duplex = DUPLEX.index(mem.duplex)
_mem.tmode = TMODES.index(mem.tmode)
_mem.is_fm = "FM" in mem.mode
_mem.is_wide = mem.mode[0] != "N"
_mem.tuning_step = STEPS.index(mem.tuning_step)
set_name(_mem, mem.name)
try:
_mem.power = POWER.index(mem.power)
except Exception:
pass
if not isinstance(mem.number, str):
_flg.skip = mem.skip == "S"
_flg.pskip = mem.skip == "P"
chirp-20261030/chirp/drivers/ic2100.py 0000644 0001750 0001750 00000016721 15257672455 016116 0 ustar hibby hibby # 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 .
from chirp.drivers import icf
from chirp import chirp_common, util, directory, bitwise
from chirp.settings import RadioSetting, RadioSettingGroup, \
RadioSettingValueBoolean
MEM_FORMAT = """
struct {
bbcd freq[2];
u8 freq_10khz:4,
freq_1khz:3,
zero:1;
u8 unknown1;
bbcd offset[2];
u8 is_12_5:1,
unknownbit1:1,
anm:1,
unknownbit2:1,
duplex:2,
tmode:2;
u8 ctone;
u8 rtone;
char name[6];
u8 unknown3;
} memory[100];
#seekto 0x0640;
struct {
bbcd freq[2];
u8 freq_10khz:4,
freq_1khz:3,
zero:1;
u8 unknown1;
bbcd offset[2];
u8 is_12_5:1,
unknownbit1:1,
anm:1,
unknownbit2:1,
duplex:2,
tmode:2;
u8 ctone;
u8 rtone;
} special[7];
#seekto 0x0680;
struct {
bbcd freq[2];
u8 freq_10khz:4,
freq_1khz:3,
zero:1;
u8 unknown1;
bbcd offset[2];
u8 is_12_5:1,
unknownbit1:1,
anm:1,
unknownbit2:1,
duplex:2,
tmode:2;
u8 ctone;
u8 rtone;
} call[2];
#seekto 0x06F0;
struct {
u8 flagbits;
} skipflags[14];
#seekto 0x0700;
struct {
u8 flagbits;
} usedflags[14];
"""
TMODES = ["", "Tone", "", "TSQL"]
DUPLEX = ["", "", "+", "-"]
STEPS = [5.0, 10.0, 12.5, 15.0, 20.0, 25.0, 30.0, 50.0]
def _get_special():
special = {"C": 506}
for i in range(0, 3):
ida = "%iA" % (i + 1)
idb = "%iB" % (i + 1)
num = 500 + (i * 2)
special[ida] = num
special[idb] = num + 1
return special
def _get_freq(mem):
freq = (int(mem.freq) * 100000) + \
(mem.freq_10khz * 10000) + \
(mem.freq_1khz * 1000)
if mem.is_12_5:
if chirp_common.is_12_5(freq):
pass
elif mem.freq_1khz == 2:
freq += 500
elif mem.freq_1khz == 5:
freq += 2500
elif mem.freq_1khz == 7:
freq += 500
else:
raise Exception("Unable to resolve 12.5 kHz: %i" % freq)
return freq
def _set_freq(mem, freq):
mem.freq = freq / 100000
mem.freq_10khz = (freq / 10000) % 10
khz = (freq / 1000) % 10
mem.freq_1khz = khz
mem.is_12_5 = chirp_common.is_12_5(freq)
def _get_offset(mem):
raw = bytearray(mem.get_raw())
if raw[5] & 0x0A:
raw[5] = raw[5] & 0xF0
mem.set_raw(raw)
offset = int(mem.offset) * 1000 + 5000
raw[5] = raw[5] | 0x0A
mem.set_raw(bytes(raw))
return offset
else:
return int(mem.offset) * 1000
def _set_offset(mem, offset):
if (offset % 10) == 5000:
extra = 0x0A
offset -= 5000
else:
extra = 0x00
mem.offset = offset / 1000
raw = bytearray(mem.get_raw())
raw[5] = raw[5] | extra
mem.set_raw(bytes(raw))
def _wipe_memory(mem, char):
mem.set_raw(char * (mem.size() / 8))
@directory.register
class IC2100Radio(icf.IcomCloneModeRadio):
"""Icom IC-2100"""
VENDOR = "Icom"
MODEL = "IC-2100H"
_model = "\x20\x88\x00\x01"
_memsize = 2016
_endframe = "Icom Inc\x2e"
_ranges = [(0x0000, 0x07E0, 32)]
@classmethod
def match_model(cls, filedata, filename):
return (super().match_model(filedata, filename) or
(cls._memsize and len(filedata) == cls._memsize))
def get_features(self):
rf = chirp_common.RadioFeatures()
rf.memory_bounds = (1, 100)
rf.has_dtcs = False
rf.has_dtcs_polarity = False
rf.has_bank = False
rf.has_tuning_step = False
rf.has_mode = False
rf.valid_modes = ["FM"]
rf.valid_tmodes = list(TMODES)
rf.valid_duplexes = list(DUPLEX)
rf.valid_tuning_steps = list(STEPS)
rf.valid_bands = [(118000000, 174000000)]
rf.valid_skips = ["", "S"]
rf.valid_special_chans = sorted(_get_special().keys())
rf.valid_characters = chirp_common.CHARSET_UPPER_NUMERIC + '-/'
return rf
def process_mmap(self):
self._memobj = bitwise.parse(MEM_FORMAT, self._mmap)
def get_memory(self, number):
mem = chirp_common.Memory()
if isinstance(number, str):
if number == "C":
number = _get_special()[number]
_mem = self._memobj.call[0]
else:
number = _get_special()[number]
_mem = self._memobj.special[number - 500]
empty = False
else:
number -= 1
_mem = self._memobj.memory[number]
_emt = self._memobj.usedflags[number / 8].flagbits
empty = (1 << (number % 8)) & int(_emt)
if not empty:
mem.name = str(_mem.name).rstrip()
_skp = self._memobj.skipflags[number / 8].flagbits
isskip = (1 << (number % 8)) & int(_skp)
mem.number = number + 1
if number <= 100:
mem.skip = isskip and "S" or ""
else:
mem.extd_number = util.get_dict_rev(_get_special(), number)
mem.immutable = ["number", "skip", "extd_number"]
if empty:
mem.empty = True
return mem
mem.freq = _get_freq(_mem)
mem.offset = _get_offset(_mem)
mem.rtone = chirp_common.TONES[_mem.rtone]
mem.ctone = chirp_common.TONES[_mem.ctone]
mem.tmode = TMODES[_mem.tmode]
mem.duplex = DUPLEX[_mem.duplex]
mem.extra = RadioSettingGroup("Extra", "extra")
rs = RadioSetting("anm", "Alphanumeric Name",
RadioSettingValueBoolean(_mem.anm))
mem.extra.append(rs)
return mem
def set_memory(self, mem):
number = mem.extd_number or mem.number
if number == "C":
_mem = self._memobj.call[0]
elif isinstance(number, str):
_mem = self._memobj.special[_get_special()[number] - 500]
else:
number = number - 1
_mem = self._memobj.memory[number]
_emt = self._memobj.usedflags[number / 8].flagbits
mask = 1 << (number % 8)
if mem.empty:
_emt |= mask
else:
_emt &= ~mask
_skp = self._memobj.skipflags[number / 8].flagbits
if mem.skip == "S":
_skp |= mask
else:
_skp &= ~mask
_mem.name = mem.name.ljust(6)
_mem.anm = mem.name.strip() != ""
_set_freq(_mem, mem.freq)
_set_offset(_mem, mem.offset)
_mem.rtone = chirp_common.TONES.index(mem.rtone)
_mem.ctone = chirp_common.TONES.index(mem.ctone)
_mem.tmode = TMODES.index(mem.tmode)
_mem.duplex = DUPLEX.index(mem.duplex)
for setting in mem.extra:
setattr(_mem, setting.get_name(), setting.value)
def get_raw_memory(self, number):
return repr(self._memobj.memory[number])
chirp-20261030/chirp/drivers/ic2200.py 0000644 0001750 0001750 00000021132 15257672455 016107 0 ustar hibby hibby # 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 .
from chirp.drivers import icf
from chirp import chirp_common, util, directory, bitwise
MEM_FORMAT = """
struct {
ul16 freq;
ul16 offset;
char name[6];
u8 unknown1:2,
rtone:6;
u8 unknown2:2,
ctone:6;
u8 unknown3:1,
dtcs:7;
u8 unknown4:4,
tuning_step:4;
u8 unknown5[3];
u8 unknown6:4,
urcall:4;
u8 r1call:4,
r2call:4;
u8 unknown7:1,
digital_code:7;
u8 is_625:1,
unknown8:1,
mode_am:1,
mode_narrow:1,
power:2,
tmode:2;
u8 dtcs_polarity:2,
duplex:2,
unknown10:4;
u8 unknown11;
u8 mode_dv:1,
unknown12:7;
} memory[207];
#seekto 0x1370;
struct {
u8 unknown:2,
empty:1,
skip:1,
bank:4;
} flags[207];
#seekto 0x15F0;
struct {
char call[8];
} mycalls[6];
struct {
char call[8];
} urcalls[6];
struct {
char call[8];
} rptcalls[6];
"""
TMODES = ["", "Tone", "TSQL", "DTCS"]
DUPLEX = ["", "-", "+"]
DTCSP = ["NN", "NR", "RN", "RR"]
STEPS = [5.0, 10.0, 12.5, 15.0, 20.0, 25.0, 30.0, 50.0]
POWER_LEVELS = [chirp_common.PowerLevel("High", watts=65),
chirp_common.PowerLevel("Low", watts=5),
chirp_common.PowerLevel("MidLow", watts=10),
chirp_common.PowerLevel("Mid", watts=25)]
def _get_special():
special = {"C": 206}
for i in range(0, 3):
ida = "%iA" % (i+1)
idb = "%iB" % (i+1)
num = 200 + i * 2
special[ida] = num
special[idb] = num + 1
return special
def _wipe_memory(mem, char):
mem.set_raw(char * (mem.size() // 8))
@directory.register
class IC2200Radio(icf.IcomCloneModeRadio, chirp_common.IcomDstarSupport):
"""Icom IC-2200"""
VENDOR = "Icom"
MODEL = "IC-2200H"
_model = "\x26\x98\x00\x01"
_memsize = 6848
_endframe = "Icom Inc\x2eD8"
_can_hispeed = True
@classmethod
def match_model(cls, filedata, filename):
return (super().match_model(filedata, filename) or
(cls._memsize and len(filedata) == cls._memsize))
_memories = []
_ranges = [(0x0000, 0x1340, 32),
(0x1340, 0x1360, 16),
(0x1360, 0x136B, 8),
(0x1370, 0x1380, 16),
(0x1380, 0x15E0, 32),
(0x15E0, 0x1600, 16),
(0x1600, 0x1640, 32),
(0x1640, 0x1660, 16),
(0x1660, 0x1680, 32),
(0x16E0, 0x1860, 32),
(0x1880, 0x1AB0, 32),
(0x1AB8, 0x1AC0, 8),
]
MYCALL_LIMIT = (0, 6)
URCALL_LIMIT = (0, 6)
RPTCALL_LIMIT = (0, 6)
def _get_bank(self, loc):
_flag = self._memobj.flags[loc]
if _flag.bank == 0x0A:
return None
else:
return _flag.bank
def _set_bank(self, loc, bank):
_flag = self._memobj.flags[loc]
if bank is None:
_flag.bank = 0x0A
else:
_flag.bank = bank
def get_features(self):
rf = chirp_common.RadioFeatures()
rf.memory_bounds = (0, 199)
rf.valid_modes = ["FM", "NFM", "AM", "NAM", "DV"]
rf.valid_tmodes = list(TMODES)
rf.valid_duplexes = list(DUPLEX)
rf.valid_tuning_steps = list(STEPS)
rf.valid_bands = [(118000000, 174000000)]
rf.valid_skips = ["", "S"]
rf.valid_power_levels = POWER_LEVELS
rf.valid_special_chans = sorted(_get_special().keys())
rf.has_settings = True
return rf
def process_mmap(self):
self._memobj = bitwise.parse(MEM_FORMAT, self._mmap)
def get_memory(self, number):
if isinstance(number, str):
number = _get_special()[number]
_mem = self._memobj.memory[number]
_flag = self._memobj.flags[number]
if _mem.mode_dv and not _flag.empty:
mem = chirp_common.DVMemory()
mem.dv_urcall = \
str(self._memobj.urcalls[_mem.urcall].call).rstrip()
mem.dv_rpt1call = \
str(self._memobj.rptcalls[_mem.r1call].call).rstrip()
mem.dv_rpt2call = \
str(self._memobj.rptcalls[_mem.r2call].call).rstrip()
else:
mem = chirp_common.Memory()
mem.number = number
if number < 200:
mem.skip = _flag.skip and "S" or ""
else:
mem.extd_number = util.get_dict_rev(_get_special(), number)
mem.immutable = ["number", "skip", "bank", "bank_index",
"extd_number"]
if _flag.empty:
mem.empty = True
mem.power = POWER_LEVELS[0]
return mem
mult = _mem.is_625 and 6250 or 5000
mem.freq = (_mem.freq * mult)
mem.offset = (_mem.offset * mult)
mem.rtone = chirp_common.TONES[_mem.rtone]
mem.ctone = chirp_common.TONES[_mem.ctone]
mem.tmode = TMODES[_mem.tmode]
mem.duplex = DUPLEX[_mem.duplex]
mem.mode = _mem.mode_dv and "DV" or _mem.mode_am and "AM" or "FM"
if _mem.mode_narrow:
mem.mode = "N%s" % mem.mode
mem.dtcs = chirp_common.DTCS_CODES[_mem.dtcs]
mem.dtcs_polarity = DTCSP[_mem.dtcs_polarity]
mem.tuning_step = STEPS[_mem.tuning_step]
mem.name = str(_mem.name).replace("\x0E", "").rstrip()
mem.power = POWER_LEVELS[_mem.power]
return mem
def get_memories(self, lo=0, hi=199):
return [m for m in self._memories if m.number >= lo and m.number <= hi]
def set_memory(self, mem):
if isinstance(mem.number, str):
number = _get_special()[mem.number]
else:
number = mem.number
_mem = self._memobj.memory[number]
_flag = self._memobj.flags[number]
was_empty = int(_flag.empty)
_flag.empty = mem.empty
if mem.empty:
_wipe_memory(_mem, "\xFF")
return
if was_empty:
_wipe_memory(_mem, "\x00")
_mem.unknown8 = 0
_mem.is_625 = chirp_common.is_fractional_step(mem.freq)
mult = _mem.is_625 and 6250 or 5000
_mem.freq = mem.freq / mult
_mem.offset = mem.offset / mult
_mem.rtone = chirp_common.TONES.index(mem.rtone)
_mem.ctone = chirp_common.TONES.index(mem.ctone)
_mem.tmode = TMODES.index(mem.tmode)
_mem.duplex = DUPLEX.index(mem.duplex)
_mem.mode_dv = mem.mode == "DV"
_mem.mode_am = mem.mode.endswith("AM")
_mem.mode_narrow = mem.mode.startswith("N")
_mem.dtcs = chirp_common.DTCS_CODES.index(mem.dtcs)
_mem.dtcs_polarity = DTCSP.index(mem.dtcs_polarity)
_mem.tuning_step = STEPS.index(mem.tuning_step)
_mem.name = mem.name.ljust(6)
if mem.power:
_mem.power = POWER_LEVELS.index(mem.power)
else:
_mem.power = 0
if number < 200:
_flag.skip = mem.skip != ""
if isinstance(mem, chirp_common.DVMemory):
urcalls = self.get_urcall_list()
rptcalls = self.get_repeater_call_list()
_mem.urcall = urcalls.index(mem.dv_urcall)
_mem.r1call = rptcalls.index(mem.dv_rpt1call)
_mem.r2call = rptcalls.index(mem.dv_rpt2call)
def get_raw_memory(self, number):
return repr(self._memobj.memory[number])
def get_urcall_list(self):
return [str(x.call).rstrip() for x in self._memobj.urcalls]
def get_repeater_call_list(self):
return [str(x.call).rstrip() for x in self._memobj.rptcalls]
def get_mycall_list(self):
return [str(x.call).rstrip() for x in self._memobj.mycalls]
def set_urcall_list(self, calls):
for i in range(*self.URCALL_LIMIT):
self._memobj.urcalls[i].call = calls[i].ljust(8)
def set_repeater_call_list(self, calls):
for i in range(*self.RPTCALL_LIMIT):
self._memobj.rptcalls[i].call = calls[i].ljust(8)
def set_mycall_list(self, calls):
for i in range(*self.MYCALL_LIMIT):
self._memobj.mycalls[i].call = calls[i].ljust(8)
chirp-20261030/chirp/drivers/ic2300.py 0000644 0001750 0001750 00000033532 15257672455 016117 0 ustar hibby hibby # Copyright 2017 Windsor Schmidt
#
# 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, directory, bitwise
from chirp.drivers import icf
from chirp.settings import RadioSetting, RadioSettingGroup, \
RadioSettingValueList, RadioSettingValueBoolean, RadioSettings
# The Icom IC-2300H is a 65W, 144 MHz mobile transceiver based on the IC-2200H.
# Unlike the IC-2200H, this model does not accept Icom's UT-118 D-STAR board.
#
# A simple USB interface based on a typical FT232RL breakout board was used
# during development of this module. A schematic diagram is as follows:
#
#
# 3.5mm plug from IC-2300H
# sleeve / ring / tip
# --.______________
# | | | \
# |______|___|___/ FT232RL breakout
# --' | | .------------------.
# | +--------| RXD |
# | | D1 | |
# | +--|>|---| TXD | USB/PC
# | | R1 | |-------->
# | +--[_]---| VCC (5V) |
# | | |
# +------------| GND |
# `------------------'
#
# D1: 1N4148 Schottky diode
# R1: 10K ohm resistor
MEM_FORMAT = """
#seekto 0x0000; // channel memories
struct {
ul16 frequency;
ul16 offset;
char name[6];
u8 repeater_tone;
u8 ctcss_tone;
u8 dtcs_code;
u8 tuning_step:4,
tone_mode:4;
u8 unknown1:3,
mode_narrow:1,
power:2,
unknown2:2;
u8 dtcs_polarity:2,
duplex:2,
unknown3:1,
reverse_duplex:1,
tx_inhibit:1,
display_style:1;
} memory[200];
#seekto 0x1340; // channel memory flags
struct {
u8 unknown5:2,
empty:1,
skip:1,
bank:4;
} flags[200];
#seekto 0x1660; // power-on and regular set menu items
struct {
u8 key_beep;
u8 tx_timeout;
u8 auto_repeater;
u8 auto_power_off;
u8 repeater_lockout;
u8 squelch_delay;
u8 squelch_type;
u8 dtmf_speed;
u8 display_type;
u8 unknown6;
u8 tone_burst;
u8 voltage_display;
u8 unknown7;
u8 display_brightness;
u8 display_color;
u8 auto_dimmer;
u8 display_contrast;
u8 scan_pause_timer;
u8 mic_gain;
u8 scan_resume_timer;
u8 weather_alert;
u8 bank_link_enable;
u8 bank_link[10];
} settings;
"""
TUNING_STEPS = [5.0, 6.25, 10.0, 12.5, 15.0, 20.0, 25.0, 30.0, 50.0]
TONE_MODES = ["", "Tone", "TSQL", "DTCS"]
DUPLEX = ["", "-", "+"]
DTCSP = ["NN", "NR", "RN", "RR"]
DTCS_POLARITY = ["NN", "NR", "RN", "RR"]
POWER_LEVELS = [chirp_common.PowerLevel("High", watts=65),
chirp_common.PowerLevel("Low", watts=5),
chirp_common.PowerLevel("MidLow", watts=10),
chirp_common.PowerLevel("Mid", watts=25)]
def _wipe_memory(mem, char):
mem.set_raw(char * (mem.size() // 8))
@directory.register
class IC2300Radio(icf.IcomCloneModeRadio):
"""Icom IC-2300"""
VENDOR = "Icom"
MODEL = "IC-2300H"
_model = "\x32\x51\x00\x01"
_memsize = 6304
_endframe = "Icom Inc.C5\xfd"
_can_hispeed = True
_ranges = [(0x0000, 0x18a0, 32)] # upload entire memory for now
@classmethod
def match_model(cls, filedata, filename):
return (super().match_model(filedata, filename) or
(cls._memsize and len(filedata) == cls._memsize))
def get_features(self):
rf = chirp_common.RadioFeatures()
rf.memory_bounds = (0, 199)
rf.valid_modes = ["FM", "NFM"]
rf.valid_tmodes = list(TONE_MODES)
rf.valid_duplexes = list(DUPLEX)
rf.valid_tuning_steps = list(TUNING_STEPS)
rf.valid_bands = [(136000000, 174000000)] # USA tx range: 144-148 MHz
rf.valid_skips = ["", "S"]
rf.valid_power_levels = POWER_LEVELS
rf.has_settings = True
return rf
def process_mmap(self):
self._memobj = bitwise.parse(MEM_FORMAT, self._mmap)
def _get_bank(self, loc):
_flag = self._memobj.flags[loc]
if _flag.bank == 0x0a:
return None
else:
return _flag.bank
def _set_bank(self, loc, bank):
_flag = self._memobj.flags[loc]
if bank is None:
_flag.bank = 0x0a
else:
_flag.bank = bank
def get_memory(self, number):
_mem = self._memobj.memory[number]
_flag = self._memobj.flags[number]
mem = chirp_common.Memory()
mem.number = number
if _flag.empty:
mem.empty = True
return mem
mult = int(TUNING_STEPS[_mem.tuning_step] * 1000)
mem.freq = (_mem.frequency * mult)
mem.offset = (_mem.offset * mult)
mem.name = str(_mem.name).rstrip()
mem.rtone = chirp_common.TONES[_mem.repeater_tone]
mem.ctone = chirp_common.TONES[_mem.ctcss_tone]
mem.dtcs = chirp_common.DTCS_CODES[_mem.dtcs_code]
mem.tuning_step = TUNING_STEPS[_mem.tuning_step]
mem.tmode = TONE_MODES[_mem.tone_mode]
mem.mode = "NFM" if _mem.mode_narrow else "FM"
mem.dtcs_polarity = DTCS_POLARITY[_mem.dtcs_polarity]
mem.duplex = DUPLEX[_mem.duplex]
mem.skip = "S" if _flag.skip else ""
mem.power = POWER_LEVELS[_mem.power]
# Reverse duplex
mem.extra = RadioSettingGroup("extra", "Extra")
rev = RadioSetting("reverse_duplex", "Reverse duplex",
RadioSettingValueBoolean(bool(_mem.reverse_duplex)))
rev.set_doc("Reverse duplex")
mem.extra.append(rev)
# Tx inhibit
tx_inhibit = RadioSetting("tx_inhibit", "TX inhibit",
RadioSettingValueBoolean(
bool(_mem.tx_inhibit)))
tx_inhibit.set_doc("TX inhibit")
mem.extra.append(tx_inhibit)
# Memory display style
opt = ["Frequency", "Label"]
dsp = RadioSetting("display_style", "Display style",
RadioSettingValueList(
opt, current_index=_mem.display_style))
dsp.set_doc("Memory display style")
mem.extra.append(dsp)
return mem
def get_raw_memory(self, number):
return repr(self._memobj.memory[number])
def set_memory(self, mem):
number = mem.number
_mem = self._memobj.memory[number]
_flag = self._memobj.flags[number]
was_empty = int(_flag.empty)
_flag.empty = mem.empty
if mem.empty:
_wipe_memory(_mem, "\xff")
return
if was_empty:
_wipe_memory(_mem, "\x00")
mult = mem.tuning_step * 1000
_mem.frequency = (mem.freq / mult)
_mem.offset = mem.offset / mult
_mem.name = mem.name.ljust(6)
_mem.repeater_tone = chirp_common.TONES.index(mem.rtone)
_mem.ctcss_tone = chirp_common.TONES.index(mem.ctone)
_mem.dtcs_code = chirp_common.DTCS_CODES.index(mem.dtcs)
_mem.tuning_step = TUNING_STEPS.index(mem.tuning_step)
_mem.tone_mode = TONE_MODES.index(mem.tmode)
_mem.mode_narrow = mem.mode.startswith("N")
_mem.dtcs_polarity = DTCSP.index(mem.dtcs_polarity)
_mem.duplex = DUPLEX.index(mem.duplex)
if mem.power:
_mem.power = POWER_LEVELS.index(mem.power)
else:
_mem.power = POWER_LEVELS[0]
_flag.skip = mem.skip != ""
for setting in mem.extra:
setattr(_mem, setting.get_name(), setting.value)
def get_settings(self):
_settings = self._memobj.settings
basic = RadioSettingGroup("basic", "Basic Settings")
front_panel = RadioSettingGroup("front_panel", "Front Panel Settings")
top = RadioSettings(basic, front_panel)
# Transmit timeout
opt = ['Disabled', '1 minute'] + \
[s + ' minutes' for s in map(str, range(2, 31))]
rs = RadioSetting(
"tx_timeout", "Transmit timeout (min)",
RadioSettingValueList(opt, current_index=_settings.tx_timeout))
basic.append(rs)
# Auto Repeater (USA model only)
opt = ["Disabled", "Duplex Only", "Duplex and tone"]
rs = RadioSetting(
"auto_repeater", "Auto repeater",
RadioSettingValueList(
opt, current_index=_settings.auto_repeater))
basic.append(rs)
# Auto Power Off
opt = ["Disabled", "30 minutes", "60 minutes", "120 minutes"]
rs = RadioSetting(
"auto_power_off", "Auto power off",
RadioSettingValueList(
opt, current_index=_settings.auto_power_off))
basic.append(rs)
# Squelch Delay
opt = ["Short", "Long"]
rs = RadioSetting(
"squelch_delay", "Squelch delay",
RadioSettingValueList(
opt, current_index=_settings.squelch_delay))
basic.append(rs)
# Squelch Type
opt = ["Noise squelch", "S-meter squelch", "Squelch attenuator"]
rs = RadioSetting(
"squelch_type", "Squelch type",
RadioSettingValueList(
opt, current_index=_settings.squelch_type))
basic.append(rs)
# Repeater Lockout
opt = ["Disabled", "Repeater lockout", "Busy lockout"]
rs = RadioSetting(
"repeater_lockout", "Repeater lockout",
RadioSettingValueList(
opt, current_index=_settings.repeater_lockout))
basic.append(rs)
# DTMF Speed
opt = ["100ms interval, 5.0 cps",
"200ms interval, 2.5 cps",
"300ms interval, 1.6 cps",
"500ms interval, 1.0 cps"]
rs = RadioSetting("dtmf_speed", "DTMF speed",
RadioSettingValueList(
opt, current_index=_settings.dtmf_speed))
basic.append(rs)
# Scan pause timer
opt = [s + ' seconds' for s in map(str, range(2, 22, 2))] + ['Hold']
rs = RadioSetting("scan_pause_timer", "Scan pause timer",
RadioSettingValueList(
opt, current_index=_settings.scan_pause_timer))
basic.append(rs)
# Scan Resume Timer
opt = ['Immediate'] + \
[s + ' seconds' for s in map(str, range(1, 6))] + ['Hold']
rs = RadioSetting("scan_resume_timer", "Scan resume timer",
RadioSettingValueList(
opt, current_index=_settings.scan_resume_timer))
basic.append(rs)
# Weather Alert (USA model only)
rs = RadioSetting("weather_alert", "Weather alert",
RadioSettingValueBoolean(_settings.weather_alert))
basic.append(rs)
# Tone Burst
rs = RadioSetting("tone_burst", "Tone burst",
RadioSettingValueBoolean(_settings.tone_burst))
basic.append(rs)
# Memory Display Type
opt = ["Frequency", "Channel", "Name"]
rs = RadioSetting(
"display_type", "Memory display",
RadioSettingValueList(
opt, current_index=_settings.display_type))
front_panel.append(rs)
# Display backlight brightness;
opt = ["1 (dimmest)", "2", "3", "4 (brightest)"]
rs = RadioSetting("display_brightness", "Backlight brightness",
RadioSettingValueList(
opt,
current_index=_settings.display_brightness))
front_panel.append(rs)
# Display backlight color
opt = ["Amber", "Yellow", "Green"]
rs = RadioSetting(
"display_color", "Backlight color",
RadioSettingValueList(
opt, current_index=_settings.display_color))
front_panel.append(rs)
# Display contrast
opt = ["1 (lightest)", "2", "3", "4 (darkest)"]
rs = RadioSetting("display_contrast", "Display contrast",
RadioSettingValueList(
opt,
current_index=_settings.display_contrast))
front_panel.append(rs)
# Auto dimmer
opt = ["Disabled", "Backlight off", "1 (dimmest)", "2", "3"]
rs = RadioSetting("auto_dimmer", "Auto dimmer",
RadioSettingValueList(
opt, current_index=_settings.auto_dimmer))
front_panel.append(rs)
# Microphone gain
opt = ["Low", "High"]
rs = RadioSetting("mic_gain", "Microphone gain",
RadioSettingValueList(
opt, current_index=_settings.mic_gain))
front_panel.append(rs)
# Key press beep
rs = RadioSetting("key_beep", "Key press beep",
RadioSettingValueBoolean(_settings.key_beep))
front_panel.append(rs)
# Voltage Display;
rs = RadioSetting("voltage_display", "Voltage display",
RadioSettingValueBoolean(_settings.voltage_display))
front_panel.append(rs)
# TODO: Add Bank Links settings to GUI
return top
def set_settings(self, settings):
_settings = self._memobj.settings
for element in settings:
if not isinstance(element, RadioSetting):
self.set_settings(element)
continue
setting = element.get_name()
setattr(_settings, setting, element.value)
chirp-20261030/chirp/drivers/ic2720.py 0000644 0001750 0001750 00000012352 15257672455 016122 0 ustar hibby hibby # Copyright 2011 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.drivers import icf
from chirp import chirp_common, directory, bitwise
MEM_FORMAT = """
struct {
u32 freq;
u32 offset;
u8 unknown1:2,
rtone:6;
u8 unknown2:2,
ctone:6;
u8 unknown3:1,
dtcs:7;
u8 unknown4:2,
unknown5:2,
tuning_step:4;
u8 unknown6:2,
tmode:2,
duplex:2,
unknown7:2;
u8 power:2,
is_fm:1,
unknown8:1,
dtcs_polarity:2,
unknown9:2;
u8 unknown[2];
} memory[200];
#seekto 0x0E20;
u8 skips[25];
#seekto 0x0EB0;
u8 used[25];
#seekto 0x0E40;
struct {
u8 bank_even:4,
bank_odd:4;
} banks[100];
"""
TMODES = ["", "Tone", "TSQL", "DTCS"]
POWER = ["High", "Low", "Med"]
DTCS_POLARITY = ["NN", "NR", "RN", "RR"]
STEPS = [5.0, 10.0, 12.5, 15, 20, 25, 30, 50]
MODES = ["FM", "AM"]
DUPLEX = ["", "", "-", "+"]
POWER_LEVELS_VHF = [chirp_common.PowerLevel("High", watts=50),
chirp_common.PowerLevel("Low", watts=5),
chirp_common.PowerLevel("Mid", watts=15)]
@directory.register
class IC2720Radio(icf.IcomCloneModeRadio):
"""Icom IC-2720"""
VENDOR = "Icom"
MODEL = "IC-2720H"
_model = "\x24\x92\x00\x01"
_memsize = 5152
_endframe = "Icom Inc\x2eA0"
_ranges = [(0x0000, 0x1400, 32)]
@classmethod
def match_model(cls, filedata, filename):
return (super().match_model(filedata, filename) or
(cls._memsize and len(filedata) == cls._memsize))
def _get_bank(self, loc):
_bank = self._memobj.banks[loc / 2]
if loc % 2:
bank = _bank.bank_odd
else:
bank = _bank.bank_even
if bank == 0x0A:
return None
else:
return bank
def _set_bank(self, loc, index):
_bank = self._memobj.banks[loc / 2]
if index is None:
index = 0x0A
if loc % 2:
_bank.bank_odd = index
else:
_bank.bank_even = index
def get_features(self):
rf = chirp_common.RadioFeatures()
rf.has_name = False
rf.memory_bounds = (0, 199)
rf.valid_modes = list(MODES)
rf.valid_tmodes = list(TMODES)
rf.valid_duplexes = list(set(DUPLEX))
rf.valid_tuning_steps = list(STEPS)
rf.valid_bands = [(118000000, 999990000)]
rf.valid_skips = ["", "S"]
rf.valid_power_levels = POWER_LEVELS_VHF
return rf
def process_mmap(self):
self._memobj = bitwise.parse(MEM_FORMAT, self._mmap)
def get_raw_memory(self, number):
return repr(self._memobj.memory[number])
def get_memory(self, number):
bitpos = (1 << (number % 8))
bytepos = (number / 8)
_mem = self._memobj.memory[number]
_skp = self._memobj.skips[bytepos]
_usd = self._memobj.used[bytepos]
mem = chirp_common.Memory()
mem.number = number
if _usd & bitpos:
mem.empty = True
return mem
mem.freq = int(_mem.freq)
mem.offset = int(_mem.offset)
mem.rtone = chirp_common.TONES[_mem.rtone]
mem.ctone = chirp_common.TONES[_mem.ctone]
mem.dtcs = chirp_common.DTCS_CODES[_mem.dtcs]
mem.tmode = TMODES[_mem.tmode]
mem.dtcs_polarity = DTCS_POLARITY[_mem.dtcs_polarity]
mem.tuning_step = STEPS[_mem.tuning_step]
mem.mode = _mem.is_fm and "FM" or "AM"
mem.duplex = DUPLEX[_mem.duplex]
mem.skip = (_skp & bitpos) and "S" or ""
mem.power = POWER_LEVELS_VHF[_mem.power]
return mem
def set_memory(self, mem):
bitpos = (1 << (mem.number % 8))
bytepos = (mem.number / 8)
_mem = self._memobj.memory[mem.number]
_skp = self._memobj.skips[bytepos]
_usd = self._memobj.used[bytepos]
if mem.empty:
_usd |= bitpos
self._set_bank(mem.number, None)
return
_usd &= ~bitpos
_mem.freq = mem.freq
_mem.offset = mem.offset
_mem.rtone = chirp_common.TONES.index(mem.rtone)
_mem.ctone = chirp_common.TONES.index(mem.ctone)
_mem.dtcs = chirp_common.DTCS_CODES.index(mem.dtcs)
_mem.tmode = TMODES.index(mem.tmode)
_mem.dtcs_polarity = DTCS_POLARITY.index(mem.dtcs_polarity)
_mem.tuning_step = STEPS.index(mem.tuning_step)
_mem.is_fm = mem.mode == "FM"
_mem.duplex = DUPLEX.index(mem.duplex)
if mem.skip == "S":
_skp |= bitpos
else:
_skp &= ~bitpos
if mem.power:
_mem.power = POWER_LEVELS_VHF.index(mem.power)
else:
_mem.power = 0
chirp-20261030/chirp/drivers/ic2730.py 0000644 0001750 0001750 00000141015 15257672455 016122 0 ustar hibby hibby # Copyright 2018 Rhett Robinson Vers 1.0
# 6/2019: Vers 1.1 Added Settings support, Rick DeWitt
# 7/2020: Vers 1.2 Fixed reversed E:* / F:# DTMF codes Issue #8159
# 9/2020: Vers 1.3 Expanded tone mode support and added py3 HAS_FUTURE
# bytes() per issue #8233. (icf.py is NOT updated)
#
# 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 typing
from chirp.drivers import icf
from chirp import chirp_common, directory, bitwise
from chirp.settings import RadioSettingGroup, RadioSetting, \
RadioSettingValueBoolean, RadioSettingValueList, \
RadioSettingValueString, RadioSettingValueInteger, \
RadioSettingValueFloat, RadioSettings
LOG = logging.getLogger(__name__)
MEM_FORMAT = """
struct {
u24 freq_flags:6,
freq:18;
u16 offset;
u8 tune_step:4,
unknown5:2,
mode:2;
u8 unknown6:2,
rtone:6;
u8 unknown7:2,
ctone:6;
u8 unknown8;
u8 dtcs;
u8 tmode:4,
duplex:2,
dtcs_polarity:2;
char name[6];
} memory[1002];
#seekto 0x42c0;
u8 used_flags[125];
#seekto 0x433e;
u8 skip_flags[125];
u8 pskip_flags[125];
#seekto 0x4440;
struct {
u8 bank;
u8 index;
} bank_info[1000];
#seekto 0x4c40;
struct {
char com[16];
} comment;
#seekto 0x4c50;
struct {
char name[6];
} bank_names[10];
#seekto 0x4cc8;
struct{
u8 codes[24];
} dtmfcode[16];
#seekto 0x4c8c;
struct {
char nam[6];
} pslnam[10];
#seekto 0x4e80;
struct {
u24 loflags:6,
lofreq:18;
u24 hiflags:6,
hifreq:18;
u8 flag:4,
mode:4;
u8 tstp;
char name[6];
} pgmscanedge[25];
#seekto 0x5000;
struct {
u8 aprichn;
u8 bprichn;
u8 autopwr;
u8 unk5003;
u8 autorptr;
u8 rmtmic; // 0x05005
u8 pttlock;
u8 bcl;
u8 tot;
u8 actband;
u8 unk500a;
u8 dialspdup;
u8 toneburst;
u8 micgain;
u8 unk500e;
u8 civaddr;
u8 civbaud;
u8 civtcvr;
u8 sqlatt;
u8 sqldly;
u8 unk5014a:4,
fanspeed:4;
u8 unk5015;
u8 bthvox;
u8 bthvoxlvl;
u8 bthvoxdly;
u8 bthvoxtot; // 0x05019
u8 btoothon;
u8 btoothauto;
u8 bthdset;
u8 bthhdpsav;
u8 bth1ptt; // 0x0501e
u8 bthpttbeep;
u8 bthcustbeep; // 0x05020
u8 ascanpause;
u8 bscanpause;
u8 ascanresume;
u8 bscanresume;
u8 dtmfspd;
u8 unk5026;
u8 awxalert;
u8 bwxalert;
u8 aprgskpscn;
u8 bprgskpscn;
u8 memname;
u8 contrast;
u8 autodimtot;
u8 autodim;
u8 unk502f;
u8 backlight;
u8 unk5031;
u8 unk5032;
u8 openmsg;
u8 beeplvl; // 0x05034
u8 keybeep;
u8 scanstpbeep;
u8 bandedgbeep;
u8 subandmute;
u8 atmpskiptym;
u8 btmpskiptym;
u32 vfohome;
u8 vfohomeset;
u16 homech;
u8 mickyrxf1;
u8 mickyrxf2;
u8 mickyrxup;
u8 mickyrxdn; // 0x05045
u8 bthplaykey;
u8 bthfwdkey;
u8 bthrwdkey;
u8 mickytxf1;
u8 mickytxf2;
u8 mickytxup;
u8 mickytxdn;
u8 unk504d;
u8 unk504e;
u8 unk504f;
u8 homebeep;
u8 bthfctn;
u8 unk5052;
u8 unk5053;
u8 ifxchg;
u8 airbandch;
u8 vhfpower;
u8 uhfpower;
u8 unk5058;
u8 unk5059;
u8 unk505a;
u8 unk505b;
u8 unk505c;
u8 unk505d;
u8 unk505e:6,
rpthangup:1,
unk505e2:1;
u8 unk505f;
} settings;
#seekto 0x5220;
struct {
u16 left_memory;
u16 right_memory;
} initmem;
#seekto 0x523e;
struct {
u8 awxchan;
u8 bwxchan;
} abwx;
#seekto 0x5250;
struct {
u8 alnk[2];
u16 unk5252;
u8 blnk[2];
} banklink;
#seekto 0x5258;
struct {
u8 msk[4];
} pslgrps[25];
#seekto 0x5280;
u16 mem_writes_count;
"""
MDUPLEX = ["", "-", "+"]
MSKIPS = ["", "S", "P"]
MMODES = ["FM", "NFM", "AM", "NAM"]
MTONE_MODES = chirp_common.TONE_MODES
MTMODES = ["", "Tone", "TonePoc", "TSQL", "DTCSPoc", "DTCS",
"TSQL-R", "DTCS-R", "DTC.OFF", "TON.DTC",
"DTC.TSQ", "TON.TSQ"]
MCROSS_MODES = ["Tone->Tone", "DTCS->", "Tone->DTCS", "DTCS->Tone"]
MDTCSP = ["NN", "NR", "RN", "RR"]
MDTMF_CHARS = list("0123456789ABCD*#")
MBANKLINK_CHARS = list("ABCDEFGHIJ")
AUTOREPEATER = ["OFF", "DUP", "DUP.TON"]
MICKEYOPTS = ["Off", "Up", "Down", "Vol Up", "Vol Down", "SQL Up",
"SQL Down", "Monitor", "Call", "MR (Ch 0)", "MR (Ch 1)",
"VFO/MR", "Home Chan", "Band/Bank", "Scan", "Temp Skip",
"Main", "Mode", "Low", "Dup", "Priority", "Tone", "MW", "Mute",
"T-Call", "DTMF Direct"]
class IC2730Bank(icf.IcomNamedBank):
"""An IC2730 bank"""
def get_name(self):
_banks = self._model._radio._memobj.bank_names
return str(_banks[self.index].name).rstrip()
def set_name(self, name):
_banks = self._model._radio._memobj.bank_names
_banks[self.index].name = str(name).ljust(6)[:6]
def _get_special():
special = {"C0": -2, "C1": -1}
return special
def _resolve_memory_number(number):
if isinstance(number, str):
return _get_special()[number]
else:
return number
def _wipe_memory(mem, char):
mem.set_raw(char * (mem.size() // 8))
@directory.register
class IC2730Radio(icf.IcomCloneModeRadio):
"""Icom IC-2730A"""
VENDOR = "Icom"
MODEL = "IC-2730A"
_model = "\x35\x98\x00\x01"
_memsize = 21312 # 0x5340
_endframe = "Icom Inc\x2e4E"
_ranges = [(0x0000, 0x5300, 64),
(0x5300, 0x5310, 16),
(0x5310, 0x5340, 48)]
_num_banks = 10
_bank_class = IC2730Bank
_can_hispeed = True
_raw_frames = True
_highbit_flip = True
@classmethod
def match_model(cls, filedata, filename):
return (super().match_model(filedata, filename) or
(cls._memsize and len(filedata) == cls._memsize))
_icf_data: dict[str, typing.Any] = {
'MapRev': 1,
'EtcData': 0, # This might be wrong
'Comment': '',
'recordsize': 32,
}
@classmethod
def get_prompts(cls):
rp = chirp_common.RadioPrompts()
rp.info = ('Click the Special Channels tab on the main screen to '
'access the C0 and C1 frequencies.\n'
'The Pocket Beep tone/dtcs modes are not supported by '
'CHIRP. Set those manually.\n')
rp.pre_download = _(
"Follow these instructions to download your config:\n"
"1 - Turn off your radio\n"
"2 - Connect your interface cable to the Speaker-2 jack\n"
"3 - Turn on your radio\n"
"4 - Radio > Download from radio\n"
"5 - Disconnect the interface cable! Otherwise there will be\n"
" no right-side audio!\n")
rp.pre_upload = _(
"Follow these instructions to upload your config:\n"
"1 - Turn off your radio\n"
"2 - Connect your interface cable to the Speaker-2 jack\n"
"3 - Turn on your radio\n"
"4 - Radio > Upload to radio\n"
"5 - Disconnect the interface cable, otherwise there will be\n"
" no right-side audio!\n"
"6 - Cycle power on the radio to exit clone mode\n")
return rp
def _get_bank(self, loc):
_bank = self._memobj.bank_info[loc]
_bank.bank = _bank.bank & 0x1F # Bad index filter, fix issue #7031
if _bank.bank == 0x1F:
return None
else:
return _bank.bank
def _set_bank(self, loc, bank):
_bank = self._memobj.bank_info[loc]
if bank is None:
_bank.bank = 0x1F
else:
_bank.bank = bank
def _get_bank_index(self, loc):
_bank = self._memobj.bank_info[loc]
return _bank.index
def _set_bank_index(self, loc, index):
_bank = self._memobj.bank_info[loc]
_bank.index = index
def get_features(self):
rf = chirp_common.RadioFeatures()
rf.has_settings = True
rf.has_bank_index = True
rf.has_bank_names = True
rf.has_cross = True
rf.has_ctone = True
rf.has_dtcs = True
rf.has_rx_dtcs = False
rf.has_dtcs_polarity = True
rf.requires_call_lists = False
rf.memory_bounds = (0, 999)
rf.valid_modes = MMODES
rf.valid_tmodes = MTONE_MODES
rf.valid_duplexes = MDUPLEX
rf.valid_tuning_steps = list(chirp_common.TUNING_STEPS[0:9])
rf.valid_bands = [(118000000, 174000000),
(375000000, 550000000)]
rf.valid_skips = MSKIPS
rf.valid_characters = chirp_common.CHARSET_ASCII
rf.valid_cross_modes = MCROSS_MODES
rf.valid_name_length = 6
rf.valid_special_chans = sorted(_get_special().keys())
return rf
def process_mmap(self):
self._memobj = bitwise.parse(MEM_FORMAT, self._mmap)
def get_memory(self, number):
mem = chirp_common.Memory()
mem.number = _resolve_memory_number(number)
if mem.number >= 0:
_mem = self._memobj.memory[mem.number]
bitpos = (1 << (number % 8))
bytepos = number / 8
_used = self._memobj.used_flags[bytepos]
is_used = ((_used & bitpos) == 0)
_skip = self._memobj.skip_flags[bytepos]
_pskip = self._memobj.pskip_flags[bytepos]
if _skip & bitpos:
mem.skip = MSKIPS[1] # "S"
elif _pskip & bitpos:
mem.skip = MSKIPS[2] # "P"
if not is_used:
mem.empty = True
return mem
else: # C0, C1 specials
_mem = self._memobj.memory[1002 + mem.number]
# _mem.freq is stored as a multiple of a tuning step
frequency_flags = int(_mem.freq_flags)
frequency_multiplier = 5000
offset_multiplier = 5000
if frequency_flags & 0x08:
frequency_multiplier = 6250
if frequency_flags & 0x01:
offset_multiplier = 6250
if frequency_flags & 0x10:
frequency_multiplier = 8333
if frequency_flags & 0x02:
offset_multiplier = 8333
if frequency_flags & 0x10: # fix underflow
val = int(_mem.freq) * frequency_multiplier
mem.freq = round(val)
else:
mem.freq = int(_mem.freq) * frequency_multiplier
if frequency_flags & 0x02:
val = int(_mem.offset) * offset_multiplier
mem.offset = round(val)
else:
mem.offset = int(_mem.offset) * offset_multiplier
mem.rtone = chirp_common.TONES[_mem.rtone]
mem.ctone = chirp_common.TONES[_mem.ctone]
tmx = 0 # MTONE_MODES index default
cmx = 0 # MCROSS_MODES index
if _mem.tmode == 1: # Tone
tmx = 1
elif _mem.tmode == 3: # TSQL
tmx = 2
elif _mem.tmode == 5: # DTCS
tmx = 3
elif _mem.tmode == 6: # TSQL-R
tmx = 5
elif _mem.tmode == 7: # DTCS-R
tmx = 4
elif _mem.tmode == 8: # DTC.OFF
tmx = 6 # Real Cross modes now
cmx = 1
elif _mem.tmode == 9: # TON.DTC
tmx = 6
cmx = 2
elif _mem.tmode == 10: # DTC.TSQ
tmx = 6
cmx = 3
elif _mem.tmode == 11: # TON.TSQ
tmx = 6
cmx = 0
mem.tmode = MTONE_MODES[tmx]
mem.cross_mode = MCROSS_MODES[cmx]
mem.duplex = MDUPLEX[_mem.duplex]
mem.mode = MMODES[_mem.mode]
mem.dtcs = chirp_common.DTCS_CODES[_mem.dtcs]
mem.dtcs_polarity = MDTCSP[_mem.dtcs_polarity]
if _mem.tune_step > 8:
mem.tuning_step = 5.0 # Sometimes TS is garbage?
else:
mem.tuning_step = chirp_common.TUNING_STEPS[_mem.tune_step]
mem.name = str(_mem.name).rstrip()
if mem.number == -2:
mem.name = "C0"
mem.extd_number = "C0"
if mem.number == -1:
mem.name = "C1"
mem.extd_number = "C1"
return mem
def set_memory(self, mem):
if mem.number >= 0: # Normal
bitpos = (1 << (mem.number % 8))
bytepos = mem.number / 8
_mem = self._memobj.memory[mem.number]
_used = self._memobj.used_flags[bytepos]
was_empty = _used & bitpos
skip = self._memobj.skip_flags[bytepos]
pskip = self._memobj.pskip_flags[bytepos]
if mem.skip == "S":
skip |= bitpos
else:
skip &= ~bitpos
if mem.skip == "P":
pskip |= bitpos
else:
pskip &= ~bitpos
if mem.empty:
_used |= bitpos
_wipe_memory(_mem, "\xFF")
self._set_bank(mem.number, None)
return
_used &= ~bitpos
if was_empty:
_wipe_memory(_mem, "\x00")
_mem.name = mem.name.ljust(6)
else: # Specials: -2 and -1
_mem = self._memobj.memory[1002 + mem.number]
# Common to both types
frequency_flags = 0x00
frequency_multiplier = 5000
offset_multiplier = 5000
if mem.freq % 5000 != 0 and mem.freq % 6250 == 0:
frequency_flags |= 0x08
frequency_multiplier = 6250
elif mem.freq % 8333 == 0:
frequency_flags |= 0x10
frequency_multiplier = 8333
if mem.offset % 5000 != 0 and mem.offset % 6250 == 0:
frequency_flags |= 0x01
offset_multiplier = 6250
elif mem.offset % 8333 == 0:
frequency_flags |= 0x02
offset_multiplier = 8333
_mem.freq = mem.freq / frequency_multiplier
_mem.offset = mem.offset / offset_multiplier
_mem.freq_flags = frequency_flags
_mem.rtone = chirp_common.TONES.index(mem.rtone)
_mem.ctone = chirp_common.TONES.index(mem.ctone)
tmx = MTONE_MODES.index(mem.tmode)
cmx = MCROSS_MODES.index(mem.cross_mode)
if tmx != 6: # Not Cross
if tmx < 2: # "", Tone
_mem.tmode = tmx
if tmx == 2: # TSQL
_mem.tmode = 3
if tmx == 3: # DTCS
_mem.tmode = 5
if tmx == 4: # DTCS-R
_mem.tmode = 7
if tmx == 5: # TSQL-R
_mem.tmode = 6
else: # Cross modes
if cmx == 0: # Tone->Tone
_mem.tmode = 11
if cmx == 1: # DTCS->
_mem.tmode = 8
if cmx == 2: # Tone->DTCS
_mem.tmode = 9
if cmx == 3: # DTCS->Tone
_mem.tmode = 10
_mem.duplex = MDUPLEX.index(mem.duplex)
_mem.mode = MMODES.index(mem.mode)
_mem.dtcs = chirp_common.DTCS_CODES.index(mem.dtcs)
_mem.dtcs_polarity = MDTCSP.index(mem.dtcs_polarity)
_mem.tune_step = chirp_common.TUNING_STEPS.index(mem.tuning_step)
def get_raw_memory(self, number):
return repr(self._memobj.memory[number])
def get_settings(self):
"""Translate the MEM_FORMAT structs into settings in the UI"""
# Define mem struct write-back shortcuts
_sets = self._memobj.settings
_cmnt = self._memobj.comment
_wxch = self._memobj.abwx
_dtm = self._memobj.dtmfcode
_pses = self._memobj.pgmscanedge
_bklk = self._memobj.banklink
basic = RadioSettingGroup("basic", "Basic Settings")
mickey = RadioSettingGroup("mickey", "Microphone Keys")
bluet = RadioSettingGroup("bluet", "Bluetooth")
disp = RadioSettingGroup("disp", "Display")
sound = RadioSettingGroup("sound", "Sounds")
dtmf = RadioSettingGroup("dtmf", "DTMF Codes")
abset = RadioSettingGroup("abset", "A/B Band Settings")
edges = RadioSettingGroup("edges", "Program Scan Edges")
pslnk = RadioSettingGroup("pslnk", "Program Scan Links")
other = RadioSettingGroup("other", "Other Settings")
group = RadioSettings(basic, disp, sound, mickey, dtmf,
abset, bluet, edges, pslnk, other)
def mic_keys(setting, obj, atrb):
""" Callback to set subset of mic key options """
stx = str(setting.value)
value = MICKEYOPTS.index(stx)
setattr(obj, atrb, value)
return
def hex_val(setting, obj, atrb):
""" Callback to store string as hex values """
value = int(str(setting.value), 16)
setattr(obj, atrb, value)
return
def unpack_str(codestr):
"""Convert u8 DTMF array to a string: NOT a callback."""
stx = ""
for i in range(0, 24): # unpack up to ff
if codestr[i] != 0xff: # Issue 8159 fix
if codestr[i] == 0x0E:
stx += "*"
elif codestr[i] == 0x0F:
stx += "#"
else:
stx += format(int(codestr[i]), '0X')
return stx
def pack_chars(setting, obj, atrb, ndx):
"""Callback to build 0-9,A-D,*# nibble array from string"""
# String will be f padded to 24 bytes
# Chars are stored as hex values
ary = []
stx = str(setting.value).upper()
stx = stx.strip() # trim spaces
# Remove illegal characters first
sty = ""
for j in range(0, len(stx)):
if stx[j] in MDTMF_CHARS:
sty += stx[j]
for j in range(0, 24):
if j < len(sty): # Issue 8159 fix
if sty[j] == "*":
chrv = 0xE
elif sty[j] == "#":
chrv = 0xF
else:
chrv = int(sty[j], 16)
else: # pad to 24 bytes
chrv = 0xFF
ary.append(chrv) # append byte
setattr(obj[ndx], atrb, ary)
return
def myset_comment(setting, obj, atrb, knt):
""" Callback to create space-padded char array"""
stx = str(setting.value)
for i in range(0, knt):
if i > len(stx):
str.append(0x20)
setattr(obj, atrb, stx)
return
def myset_psnam(setting, obj, ndx, atrb, knt):
""" Callback to generate space-padded, uppercase char array """
# This sub also is specific to object arrays
stx = str(setting.value).upper()
for i in range(0, knt):
if i > len(stx):
str.append(0x20)
setattr(obj[ndx], atrb, stx)
return
def myset_frqflgs(setting, obj, ndx, flg, frq):
""" Callback to gen flag/freq pairs """
vfrq = float(str(setting.value))
vfrq = int(vfrq * 1000000)
vflg = 0x10
if vfrq % 6250 == 0:
vflg = 0x08
vfrq = int(vfrq / 6250)
elif vfrq % 5000 == 0:
vflg = 0
vfrq = int(vfrq / 5000)
else:
vfrq = int(vfrq / 8333)
setattr(obj[ndx], flg, vflg)
setattr(obj[ndx], frq, vfrq)
return
def banklink(ary):
""" Sub to generate A-J string from 2-byte bit pattern """
stx = ""
for kx in range(0, 10):
if kx < 8:
val = ary[0]
msk = 1 << kx
else:
val = ary[1]
msk = 1 << (kx - 8)
if val & msk:
stx += chr(kx + 65)
else:
stx += "_"
return stx
def myset_banklink(setting, obj, atrb):
"""Callback to create 10-bit, u8[2] array from 10 char string"""
stx = str(setting.value).upper()
ary = [0, 0]
for kx in range(0, 10):
if stx[kx] == chr(kx + 65):
if kx < 8:
ary[0] = ary[0] + (1 << kx)
else:
ary[1] = ary[1] + (1 << (kx - 8))
setattr(obj, atrb, ary)
return
def myset_tsopt(setting, obj, ndx, atrb, bx):
""" Callback to set scan Edge tstep """
stx = str(setting.value)
flg = 0
if stx == "-":
val = 0xff
else:
if bx == 1: # Air band
if stx == "Auto":
val = 0xe
elif stx == "25k":
val = 8
elif stx == "8.33k":
val = 2
else: # VHF or UHF
optx = ["-", "5k", "6.25k", "10k", "12.5k", "15k",
"20k", "25k", "30k", "50k"]
val = optx.index(stx) + 1
setattr(obj[ndx], atrb, val)
# and set flag
setattr(obj[ndx], "flag", flg)
return
def myset_mdopt(setting, obj, ndx, atrb, bx):
""" Callback to set Scan Edge mode """
stx = str(setting.value)
if stx == "-":
val = 0xf
elif stx == "FM":
val = 0
else:
val = 1
setattr(obj[ndx], atrb, val)
return
def myset_bitmask(setting, obj, ndx, atrb, knt):
""" Callback to generate byte-array bitmask from string"""
# knt is BIT count to process
lsx = str(setting.value).strip().split(",")
for kx in range(0, len(lsx)):
try:
lsx[kx] = int(lsx[kx])
except Exception:
lsx[kx] = -99 # will nop
ary = [0, 0, 0, 0xfe]
for kx in range(0, knt):
if kx < 8:
if kx in lsx:
ary[0] += 1 << kx
elif kx >= 8 and kx < 16:
if kx in lsx:
ary[1] += 1 << (kx - 8)
elif kx >= 16 and kx < 24:
if kx in lsx:
ary[2] += 1 << (kx - 16)
else:
if kx in lsx: # only bit 25
ary[3] += 1
setattr(obj[ndx], atrb, ary)
return
# --- Basic
options = ["Off", "S-Meter Squelch", "ATT"]
rx = RadioSettingValueList(options, current_index=_sets.sqlatt)
rset = RadioSetting("settings.sqlatt", "Squelch/ATT", rx)
basic.append(rset)
options = ["Short", "Long"]
rx = RadioSettingValueList(options, current_index=_sets.sqldly)
rset = RadioSetting("settings.sqldly", "Squelch Delay", rx)
basic.append(rset)
rx = RadioSettingValueBoolean(bool(_sets.pttlock))
rset = RadioSetting("settings.pttlock", "PTT Lockout", rx)
basic.append(rset)
rx = RadioSettingValueBoolean(bool(_sets.bcl))
rset = RadioSetting("settings.bcl", "Busy Channel Lockout", rx)
basic.append(rset)
options = ["Off", "1m", "3m", "5m", "10m", "15m", "30m"]
rx = RadioSettingValueList(options, current_index=_sets.tot)
rset = RadioSetting("settings.tot", "Tx Timeout", rx)
basic.append(rset)
val = int(_sets.vfohome)
if val == 0xffff:
val = 0
val = val / 1000000.0
rx = RadioSettingValueFloat(0.0, 550.0, val, 0.005, 4)
rx.set_mutable(False)
rset = RadioSetting("settings.vfohome", "Home VFO (Read-Only)", rx)
basic.append(rset)
val = _sets.homech
if val == 0xffff:
val = -1
rx = RadioSettingValueInteger(-1, 999, val)
rx.set_mutable(False)
rset = RadioSetting("settings.homech",
"Home Channel (Read-Only)", rx)
basic.append(rset)
options = ["1", "2", "3", "4"]
rx = RadioSettingValueList(options, current_index=_sets.micgain)
rset = RadioSetting("settings.micgain", "Microphone Gain", rx)
basic.append(rset)
_bmem = self._memobj.initmem
rx = RadioSettingValueInteger(0, 999, _bmem.left_memory)
rset = RadioSetting("initmem.left_memory",
"Left Bank Initial Mem Chan", rx)
basic.append(rset)
rx = RadioSettingValueInteger(0, 999, _bmem.right_memory)
rset = RadioSetting("initmem.right_memory",
"Right Bank Initial Mem Chan", rx)
basic.append(rset)
stx = ""
for i in range(0, 16):
stx += chr(int(_cmnt.com[i]))
stx = stx.rstrip()
rx = RadioSettingValueString(0, 16, stx)
rset = RadioSetting("comment.com", "Comment (16 chars)", rx)
rset.set_apply_callback(myset_comment, _cmnt, "com", 16)
basic.append(rset)
# --- Other
rset = RadioSetting("drv_clone_speed", "Use Hi-Speed Clone",
RadioSettingValueBoolean(self._can_hispeed))
other.append(rset)
options = ["Single", "All", "Ham"]
rx = RadioSettingValueList(options, current_index=_sets.actband)
rset = RadioSetting("settings.actband", "Active Band", rx)
other.append(rset)
options = ["Slow", "Mid", "Fast", "Auto"]
rx = RadioSettingValueList(options, current_index=_sets.fanspeed)
rset = RadioSetting("settings.fanspeed", "Fan Speed", rx)
other.append(rset)
rx = RadioSettingValueBoolean(bool(_sets.dialspdup))
rset = RadioSetting("settings.dialspdup", "Dial Speed-Up", rx)
other.append(rset)
options = ["Off", "On(Dup)", "On(Dup+Tone)"]
rx = RadioSettingValueList(options, current_index=_sets.autorptr)
rset = RadioSetting("settings.autorptr", "Auto Repeater", rx)
other.append(rset)
rx = RadioSettingValueBoolean(bool(_sets.rmtmic))
rset = RadioSetting("settings.rmtmic",
"One-Touch PTT (Remote Mic)", rx)
other.append(rset)
options = ["Low", "Mid", "High"]
rx = RadioSettingValueList(options, current_index=_sets.vhfpower)
rset = RadioSetting("settings.vhfpower", "VHF Power Default", rx)
other.append(rset)
rx = RadioSettingValueList(options, current_index=_sets.uhfpower)
rset = RadioSetting("settings.uhfpower", "UHF Power Default", rx)
other.append(rset)
rx = RadioSettingValueBoolean(bool(_sets.toneburst))
rset = RadioSetting("settings.toneburst", "1750 Hz Tone Burst", rx)
other.append(rset)
rx = RadioSettingValueBoolean(bool(_sets.ifxchg))
rset = RadioSetting("settings.ifxchg", "IF Exchange", rx)
other.append(rset)
rx = RadioSettingValueBoolean(bool(_sets.rpthangup))
rset = RadioSetting("settings.rpthangup",
"Repeater Hang up Timeout", rx)
other.append(rset)
stx = str(_sets.civaddr)[2:] # Hex value
rx = RadioSettingValueString(1, 2, stx)
rset = RadioSetting("settings.civaddr", "CI-V Address (90)", rx)
rset.set_apply_callback(hex_val, _sets, "civaddr")
other.append(rset)
options = ["1200", "2400", "4800", "9600", "19200", "Auto"]
rx = RadioSettingValueList(options, current_index=_sets.civbaud)
rset = RadioSetting("settings.civbaud", "CI-V Baud Rate (bps)", rx)
other.append(rset)
rx = RadioSettingValueBoolean(bool(_sets.civtcvr))
rset = RadioSetting("settings.civtcvr", "CI-V Transceive", rx)
other.append(rset)
# A/B Band Settings
options = ["Off", "On", "Bell"]
rx = RadioSettingValueList(options, current_index=_sets.aprichn)
rset = RadioSetting("settings.aprichn",
"A Band: VFO Priority Watch Mode", rx)
abset.append(rset)
options = ["2", "4", "6", "8", "10", "12", "14",
"16", "18", "20", "Hold"]
rx = RadioSettingValueList(options, current_index=_sets.ascanpause)
rset = RadioSetting("settings.ascanpause",
"-- A Band: Scan Pause Time (Secs)", rx)
abset.append(rset)
options = ["0", "1", "2", "3", "4", "5", "Hold"]
rx = RadioSettingValueList(options, current_index=_sets.ascanresume)
rset = RadioSetting("settings.ascanresume",
"-- A Band: Scan Resume Time (Secs)", rx)
abset.append(rset)
options = ["5", "10", "15"]
rx = RadioSettingValueList(options, current_index=_sets.atmpskiptym)
rset = RadioSetting("settings.atmpskiptym",
"-- A Band: Temp Skip Time (Secs)", rx)
abset.append(rset)
rx = RadioSettingValueBoolean(bool(_sets.aprgskpscn))
rset = RadioSetting("settings.aprgskpscn",
"-- A Band: Program Skip Scan", rx)
abset.append(rset)
rx = RadioSettingValueString(10, 10, banklink(_bklk.alnk))
rset = RadioSetting("banklink.alnk",
"-- A Band Banklink (use _ to skip)", rx)
rset.set_apply_callback(myset_banklink, _bklk, "alnk")
abset.append(rset)
rx = RadioSettingValueBoolean(bool(_sets.awxalert))
rset = RadioSetting("settings.awxalert",
"-- A Band: Weather Alert", rx)
abset.append(rset)
# Use list for Wx chans since chan 1 = index 0
options = ["1", "2", "3", "4", "5", "6", "7", "8", "9", "10"]
rx = RadioSettingValueList(options, current_index=_wxch.awxchan)
rset = RadioSetting("abwx.awxchan", "-- A Band: Weather Channel", rx)
abset.append(rset)
options = ["Off", "On", "Bell"]
rx = RadioSettingValueList(options, current_index=_sets.bprichn)
rset = RadioSetting("settings.bprichn",
"B Band: VFO Priority Watch Mode", rx)
abset.append(rset)
options = ["2", "4", "6", "8", "10", "12", "14",
"16", "18", "20", "Hold"]
rx = RadioSettingValueList(options, current_index=_sets.bscanpause)
rset = RadioSetting("settings.bscanpause",
"-- B Band: Scan Pause Time (Secs)", rx)
abset.append(rset)
options = ["0", "1", "2", "3", "4", "5", "Hold"]
rx = RadioSettingValueList(options, current_index=_sets.bscanresume)
rset = RadioSetting("settings.bscanresume",
"-- B Band: Scan Resume Time (Secs)", rx)
abset.append(rset)
options = ["5", "10", "15"]
rx = RadioSettingValueList(options, current_index=_sets.btmpskiptym)
rset = RadioSetting("settings.btmpskiptym",
"-- B Band: Temp Skip Time (Secs)", rx)
abset.append(rset)
rx = RadioSettingValueBoolean(bool(_sets.bprgskpscn))
rset = RadioSetting("settings.bprgskpscn",
"-- B Band: Program Skip Scan", rx)
abset.append(rset)
rx = RadioSettingValueString(10, 10, banklink(_bklk.blnk))
rset = RadioSetting("banklink.blnk",
"-- B Band Banklink (use _ to skip)", rx)
rset.set_apply_callback(myset_banklink, _bklk, "blnk")
abset.append(rset)
rx = RadioSettingValueBoolean(bool(_sets.bwxalert))
rset = RadioSetting("settings.bwxalert",
"-- B Band: Weather Alert", rx)
abset.append(rset)
options = ["1", "2", "3", "4", "5", "6", "7", "8", "9", "10"]
rx = RadioSettingValueList(options, current_index=_wxch.bwxchan)
rset = RadioSetting("abwx.bwxchan", "-- B Band: Weather Channel", rx)
abset.append(rset)
# --- Microphone Keys
# The Mic keys get weird: stored values are indecis to the full
# options list, but only a subset is valid...
shortopts = ["Off", "Monitor", "MR (Ch 0)", "MR (Ch 1)", "Band/Bank",
"Scan", "Temp Skip", "Mode", "Low", "Dup", "Priority",
"Tone", "MW", "Mute", "DTMF Direct", "T-Call"]
ptr = shortopts.index(MICKEYOPTS[_sets.mickyrxf1])
rx = RadioSettingValueList(shortopts, current_index=ptr)
rset = RadioSetting("settings.mickyrxf1",
"During Rx/Standby [F-1]", rx)
rset.set_apply_callback(mic_keys, _sets, "mickyrxf1")
mickey.append(rset)
ptr = shortopts.index(MICKEYOPTS[_sets.mickyrxf2])
rx = RadioSettingValueList(shortopts, current_index=ptr)
rset = RadioSetting("settings.mickyrxf2",
"During Rx/Standby [F-2]", rx)
rset.set_apply_callback(mic_keys, _sets, "mickyrxf2")
mickey.append(rset)
options = ["Off", "Low", "T-Call"] # NOT a subset of MICKEYOPTS
rx = RadioSettingValueList(options, current_index=_sets.mickytxf1)
rset = RadioSetting("settings.mickytxf1", "During Tx [F-1]", rx)
mickey.append(rset)
rx = RadioSettingValueList(options, current_index=_sets.mickytxf2)
rset = RadioSetting("settings.mickytxf2", "During Tx [F-2]", rx)
mickey.append(rset)
# These next two get the full options list
rx = RadioSettingValueList(MICKEYOPTS, current_index=_sets.mickyrxup)
rset = RadioSetting("settings.mickyrxup",
"During Rx/Standby [Up]", rx)
mickey.append(rset)
rx = RadioSettingValueList(MICKEYOPTS, current_index=_sets.mickyrxdn)
rset = RadioSetting("settings.mickyrxdn",
"During Rx/Standby [Down]", rx)
mickey.append(rset)
options = ["Off", "Low", "T-Call"]
rx = RadioSettingValueList(options, current_index=_sets.mickytxup)
rset = RadioSetting("settings.mickytxup", "During Tx [Up]", rx)
mickey.append(rset)
rx = RadioSettingValueList(options, current_index=_sets.mickytxdn)
rset = RadioSetting("settings.mickytxdn", "During Tx [Down]", rx)
mickey.append(rset)
# --- Bluetooth
rx = RadioSettingValueBoolean(bool(_sets.btoothon))
rset = RadioSetting("settings.btoothon", "Bluetooth", rx)
bluet.append(rset)
rx = RadioSettingValueBoolean(bool(_sets.btoothauto))
rset = RadioSetting("settings.btoothauto", "Auto Connect", rx)
bluet.append(rset)
options = ["Headset Only", "Headset & Speaker"]
rx = RadioSettingValueList(options, current_index=_sets.bthdset)
rset = RadioSetting("settings.bthdset", "Headset Audio", rx)
bluet.append(rset)
options = ["Normal", "Microphone", "PTT (Audio:Main)",
"PTT(Audio:Controller)"]
rx = RadioSettingValueList(options, current_index=_sets.bthfctn)
rset = RadioSetting("settings.bthfctn", "Headset Function", rx)
bluet.append(rset)
rx = RadioSettingValueBoolean(bool(_sets.bthvox))
rset = RadioSetting("settings.bthvox", "Vox", rx)
bluet.append(rset)
options = ["Off", "1.0", "2", "3", "4", "5", "6", "7", "8", "9", "10"]
rx = RadioSettingValueList(options, current_index=_sets.bthvoxlvl)
rset = RadioSetting("settings.bthvoxlvl", "Vox Level", rx)
bluet.append(rset)
options = ["0.5", "1.0", "1.5", "2.0", "2.5", "3.0"]
rx = RadioSettingValueList(options, current_index=_sets.bthvoxdly)
rset = RadioSetting("settings.bthvoxdly", "Vox Delay (Secs)", rx)
bluet.append(rset)
options = ["Off", "1", "2", "3", "4", "5", "10", "15"]
rx = RadioSettingValueList(options, current_index=_sets.bthvoxtot)
rset = RadioSetting("settings.bthvoxtot", "Vox Time-Out (Mins)", rx)
bluet.append(rset)
rx = RadioSettingValueBoolean(bool(_sets.bthhdpsav))
rset = RadioSetting("settings.bthhdpsav",
"ICOM Headset Power-Save", rx)
bluet.append(rset)
rx = RadioSettingValueBoolean(bool(_sets.bth1ptt))
rset = RadioSetting("settings.bth1ptt",
"ICOM Headset One-Touch PTT", rx)
bluet.append(rset)
rx = RadioSettingValueBoolean(bool(_sets.bthpttbeep))
rset = RadioSetting("settings.bthpttbeep",
"ICOM Headset PTT Beep", rx)
bluet.append(rset)
rx = RadioSettingValueBoolean(bool(_sets.bthcustbeep))
rset = RadioSetting("settings.bthcustbeep",
"ICOM Headset Custom Key Beep", rx)
bluet.append(rset)
rx = RadioSettingValueList(MICKEYOPTS, current_index=_sets.bthplaykey)
rset = RadioSetting("settings.bthplaykey",
"ICOM Headset Custom Key [Play]", rx)
bluet.append(rset)
rx = RadioSettingValueList(MICKEYOPTS, current_index=_sets.bthfwdkey)
rset = RadioSetting("settings.bthfwdkey",
"ICOM Headset Custom Key [Fwd]", rx)
bluet.append(rset)
rx = RadioSettingValueList(MICKEYOPTS, current_index=_sets.bthrwdkey)
rset = RadioSetting("settings.bthrwdkey",
"ICOM Headset Custom Key [Rwd]", rx)
bluet.append(rset)
# ---- Display
options = ["1: Dark", "2", "3", "4: Bright"]
rx = RadioSettingValueList(options, current_index=_sets.backlight)
rset = RadioSetting("settings.backlight", "Backlight Level", rx)
disp.append(rset)
options = ["Off", "Auto-Off", "Auto-1", "Auto-2", "Auto-3"]
rx = RadioSettingValueList(options, current_index=_sets.autodim)
rset = RadioSetting("settings.autodim", "Auto Dimmer", rx)
disp.append(rset)
options = ["5", "10"]
rx = RadioSettingValueList(options, current_index=_sets.autodimtot)
rset = RadioSetting("settings.autodimtot",
"Auto-Dimmer Timeout (Secs)", rx)
disp.append(rset)
options = ["1", "2", "3", "4", "5", "6", "7", "8", "9", "10"]
rx = RadioSettingValueList(options, current_index=_sets.contrast)
rset = RadioSetting("settings.contrast", "LCD Contrast", rx)
disp.append(rset)
rx = RadioSettingValueBoolean(bool(_sets.openmsg))
rset = RadioSetting("settings.openmsg", "Opening Message", rx)
disp.append(rset)
rx = RadioSettingValueBoolean(bool(_sets.memname))
rset = RadioSetting("settings.memname", "Memory Names", rx)
disp.append(rset)
options = ["CH ID", "Frequency"]
rx = RadioSettingValueList(options, current_index=_sets.airbandch)
rset = RadioSetting("settings.airbandch", "Air Band Display", rx)
disp.append(rset)
# -- Sounds
rx = RadioSettingValueInteger(0, 9, _sets.beeplvl)
rset = RadioSetting("settings.beeplvl", "Beep Level", rx)
sound.append(rset)
rx = RadioSettingValueBoolean(bool(_sets.homebeep))
rset = RadioSetting("settings.homebeep", "Home Chan Beep", rx)
sound.append(rset)
rx = RadioSettingValueBoolean(bool(_sets.keybeep))
rset = RadioSetting("settings.keybeep", "Key Touch Beep", rx)
sound.append(rset)
rx = RadioSettingValueBoolean(bool(_sets.bandedgbeep))
rset = RadioSetting("settings.bandedgbeep", "Band Edge Beep", rx)
sound.append(rset)
rx = RadioSettingValueBoolean(bool(_sets.scanstpbeep))
rset = RadioSetting("settings.scanstpbeep", "Scan Stop Beep", rx)
sound.append(rset)
options = ["Off", "Mute", "Beep", "Mute & Beep"]
rx = RadioSettingValueList(options, current_index=_sets.subandmute)
rset = RadioSetting("settings.subandmute", "Sub Band Mute", rx)
sound.append(rset)
# --- DTMF Codes
options = ["100", "200", "300", "500"]
rx = RadioSettingValueList(options, current_index=_sets.dtmfspd)
rset = RadioSetting("settings.dtmfspd", "DTMF Speed (mSecs)", rx)
dtmf.append(rset)
for kx in range(0, 16):
stx = unpack_str(_dtm[kx].codes)
rx = RadioSettingValueString(0, 24, stx)
# NOTE the / to indicate indexed array
rset = RadioSetting("dtmfcode/%d.codes" % kx,
"DTMF Code %X" % kx, rx)
rset.set_apply_callback(pack_chars, _dtm, "codes", kx)
dtmf.append(rset)
# --- Program Scan Edges
for kx in range(0, 25):
stx = ""
for i in range(0, 6):
stx += chr(int(_pses[kx].name[i]))
stx = stx.rstrip()
rx = RadioSettingValueString(0, 6, stx)
rset = RadioSetting("pgmscanedge/%d.name" % kx,
"Program Scan %d Name" % kx, rx)
rset.set_apply_callback(myset_psnam, _pses, kx, "name", 6)
edges.append(rset)
# Freq's use the multiplier flags
fmult = 5000.0
if _pses[kx].loflags == 0x10:
fmult = 8333
if _pses[kx].loflags == 0x08:
fmult = 6250.0
flow = (int(_pses[kx].lofreq) * fmult) / 1000000.0
flow = round(flow, 4)
fmult = 5000.0
if _pses[kx].hiflags == 0x10:
fmult = 8333
if _pses[kx].hiflags == 0x08:
fmult = 6250.0
fhigh = (int(_pses[kx].hifreq) * fmult) / 1000000.0
fhigh = round(fhigh, 4)
if (flow > 0) and (flow >= fhigh): # reverse em
val = flow
flow = fhigh
fhigh = val
rx = RadioSettingValueFloat(0, 550.0, flow, 0.010, 3)
rset = RadioSetting("pgmscanedge/%d.lofreq" % kx,
"-- Scan %d Low Limit" % kx, rx)
rset.set_apply_callback(myset_frqflgs, _pses, kx, "loflags",
"lofreq")
edges.append(rset)
rx = RadioSettingValueFloat(0, 550.0, fhigh, 0.010, 3)
rset = RadioSetting("pgmscanedge/%d.hifreq" % kx,
"-- Scan %d High Limit" % kx, rx)
rset.set_apply_callback(myset_frqflgs, _pses, kx, "hiflags",
"hifreq")
edges.append(rset)
# Tstep and Mode depend on the bands
ndxt = 0
ndxm = 0
bxnd = 0
tsopt = ["-", "5k", "6.25k", "10k", "12.5k", "15k",
"20k", "25k", "30k", "50k"]
mdopt = ["-", "FM", "FM-N"]
if fhigh > 0:
if fhigh < 135.0: # Air band
bxnd = 1
tsopt = ["-", "8.33k", "25k", "Auto"]
ndxt = _pses[kx].tstp
if ndxt == 0xe: # Auto
ndxt = 3
elif ndxt == 8: # 25k
ndxt = 2
elif ndxt == 2: # 8.33k
ndxt = 1
else:
ndxt = 0
mdopt = ["-"]
elif (flow >= 137.0) and (fhigh <= 174.0): # VHF
ndxt = _pses[kx].tstp - 1
ndxm = _pses[kx].mode + 1
bxnd = 2
elif (flow >= 375.0) and (fhigh <= 550.0): # UHF
ndxt = _pses[kx].tstp - 1
ndxm = _pses[kx].mode + 1
bxnd = 3
else: # Mixed, ndx's = 0 default
tsopt = ["-"]
mdopt = ["-"]
bxnd = 4
if (ndxt > 9) or (ndxt < 0):
ndxt = 0 # trap ff
if ndxm > 2:
ndxm = 0
# end if fhigh > 0
rx = RadioSettingValueList(tsopt, current_index=ndxt)
rset = RadioSetting("pgmscanedge/%d.tstp" % kx,
"-- Scan %d Freq Step" % kx, rx)
rset.set_apply_callback(myset_tsopt, _pses, kx, "tstp", bxnd)
edges.append(rset)
rx = RadioSettingValueList(mdopt, current_index=ndxm)
rset = RadioSetting("pgmscanedge/%d.mode" % kx,
"-- Scan %d Mode" % kx, rx)
rset.set_apply_callback(myset_mdopt, _pses, kx, "mode", bxnd)
edges.append(rset)
# End for kx
# --- Program Scan Links
_psln = self._memobj.pslnam
_pslg = self._memobj.pslgrps
for kx in range(0, 10):
stx = ""
for i in range(0, 6):
stx += chr(int(_psln[kx].nam[i]))
stx = stx.rstrip()
rx = RadioSettingValueString(0, 6, stx)
rset = RadioSetting("pslnam/%d.nam" % kx,
"Program Scan Link %d Name" % kx, rx)
rset.set_apply_callback(myset_psnam, _psln, kx, "nam", 6)
pslnk.append(rset)
for px in range(0, 25):
# Generate string numeric representation of 4-byte bitmask
stx = ""
for nx in range(0, 25):
if nx < 8:
if (_pslg[kx].msk[0] & (1 << nx)):
stx += "%0d, " % nx
elif (nx >= 8) and (nx < 16):
if (_pslg[kx].msk[1] & (1 << (nx - 8))):
stx += "%0d, " % nx
elif (nx >= 16) and (nx < 24):
if (_pslg[kx].msk[2] & (1 << (nx - 16))):
stx += "%0d, " % nx
elif (nx >= 24):
if (_pslg[kx].msk[3] & (1 << (nx - 24))):
stx += "%0d, " % nx
rx = RadioSettingValueString(0, 80, stx)
rset = RadioSetting("pslgrps/%d.msk" % kx,
"--- Scan Link %d Scans" % kx, rx)
rset.set_apply_callback(myset_bitmask, _pslg,
kx, "msk", 25)
pslnk.append(rset)
# end for px
# End for kx
return group # END get_settings()
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()
elif element.get_name() == "drv_clone_speed":
val = element.value.get_value()
self.__class__._can_hispeed = val
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
@directory.register
class IC2730ERadio(IC2730Radio):
"""Icom IC-2730E (European variant)"""
MODEL = "IC-2730E"
# The IC-2730E speaks the initial clone handshake at 19200 baud
# (not 9600 like the IC-2730A) and requires the radio's model code
# in the CLONE_ID query payload instead of all zeros. After the
# handshake the standard hispeed transition to 38400 still applies.
BAUD_RATE = 19200
_id_query_with_model = True
# The IC-2730E ends a clone-out with "Icom Inc.4D" rather than the
# "Icom Inc.4E" sent by the A variant.
_endframe = "Icom Inc\x2e4D"
@classmethod
def match_model(cls, filedata, filename):
# The IC-2730A and IC-2730E are byte-indistinguishable in
# legacy raw-image dumps (same _model and _memsize). Files
# saved by modern CHIRP carry an explicit driver-class tag in
# the .img metadata trailer and route there directly, so this
# variant only needs to abstain from blind auto-detection.
return False
chirp-20261030/chirp/drivers/ic2820.py 0000644 0001750 0001750 00000023037 15257672455 016125 0 ustar hibby hibby # 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 .
from chirp.drivers import icf
from chirp import chirp_common, util, directory, bitwise
MEM_FORMAT = """
struct {
u32 freq;
u32 offset;
char urcall[8];
char r1call[8];
char r2call[8];
u8 unknown1;
u8 unknown2:1,
duplex:2,
tmode:3,
unknown3:2;
u16 ctone:6,
rtone:6,
tune_step:4;
u16 dtcs:7,
mode:3,
unknown4:6;
u8 unknown5:1,
digital_code:7;
u8 unknown6:2,
dtcs_polarity:2,
unknown7:4;
char name[8];
} memory[522];
#seekto 0x61E0;
u8 used_flags[66];
#seekto 0x6222;
u8 skip_flags[65];
u8 pskip_flags[65];
#seekto 0x62A4;
struct {
u8 bank;
u8 index;
} bank_info[500];
#seekto 0x66C0;
struct {
char name[8];
} bank_names[26];
#seekto 0x6970;
struct {
char call[8];
u8 unknown[4];
} mycall[6];
#seekto 0x69B8;
struct {
char call[8];
} urcall[60];
struct {
char call[8];
} rptcall[60];
"""
TMODES = ["", "Tone", "??0", "TSQL", "??1", "??2", "DTCS"]
DUPLEX = ["", "-", "+", "+"] # Not sure about index 3
MODES = ["FM", "NFM", "AM", "??", "DV"]
DTCSP = ["NN", "NR", "RN", "RR"]
MEM_LOC_SIZE = 48
class IC2820Bank(icf.IcomNamedBank):
"""An IC2820 bank"""
def get_name(self):
_banks = self._model._radio._memobj.bank_names
return str(_banks[self.index].name).rstrip()
def set_name(self, name):
_banks = self._model._radio._memobj.bank_names
_banks[self.index].name = str(name).ljust(8)[:8]
def _get_special():
special = {"C0": 500 + 20,
"C1": 500 + 21}
for i in range(0, 10):
ida = "%iA" % i
idb = "%iB" % i
special[ida] = 500 + i * 2
special[idb] = 500 + i * 2 + 1
return special
def _resolve_memory_number(number):
if isinstance(number, str):
return _get_special()[number]
else:
return number
def _wipe_memory(mem, char):
mem.set_raw(char * (mem.size() // 8))
@directory.register
class IC2820Radio(icf.IcomCloneModeRadio, chirp_common.IcomDstarSupport):
"""Icom IC-2820"""
VENDOR = "Icom"
MODEL = "IC-2820H"
_model = "\x29\x70\x00\x01"
_memsize = 44224
_endframe = "Icom Inc\x2e68"
_ranges = [(0x0000, 0x6960, 32),
(0x6960, 0x6980, 16),
(0x6980, 0x7160, 32),
(0x7160, 0x7180, 16),
(0x7180, 0xACC0, 32),
]
_num_banks = 26
_bank_class = IC2820Bank
_can_hispeed = True
@classmethod
def match_model(cls, filedata, filename):
return (super().match_model(filedata, filename) or
(cls._memsize and len(filedata) == cls._memsize))
MYCALL_LIMIT = (1, 7)
URCALL_LIMIT = (1, 61)
RPTCALL_LIMIT = (1, 61)
_memories = {}
def _get_bank(self, loc):
_bank = self._memobj.bank_info[loc]
if _bank.bank == 0xFF:
return None
else:
return _bank.bank
def _set_bank(self, loc, bank):
_bank = self._memobj.bank_info[loc]
if bank is None:
_bank.bank = 0xFF
else:
_bank.bank = bank
def _get_bank_index(self, loc):
_bank = self._memobj.bank_info[loc]
return _bank.index
def _set_bank_index(self, loc, index):
_bank = self._memobj.bank_info[loc]
_bank.index = index
def get_features(self):
rf = chirp_common.RadioFeatures()
rf.has_settings = True
rf.has_bank_index = True
rf.has_bank_names = True
rf.requires_call_lists = False
rf.memory_bounds = (0, 499)
rf.valid_modes = [x for x in MODES if '?' not in x]
rf.valid_tmodes = list(TMODES)
rf.valid_duplexes = list(set(DUPLEX))
rf.valid_tuning_steps = list(chirp_common.TUNING_STEPS)
rf.valid_bands = [(118000000, 999990000)]
rf.valid_skips = ["", "S", "P"]
rf.valid_characters = chirp_common.CHARSET_ASCII
rf.valid_name_length = 8
rf.valid_special_chans = sorted(_get_special().keys())
return rf
def process_mmap(self):
self._memobj = bitwise.parse(MEM_FORMAT, self._mmap)
def get_memory(self, number):
number = _resolve_memory_number(number)
bitpos = (1 << (number % 8))
bytepos = number / 8
_mem = self._memobj.memory[number]
_used = self._memobj.used_flags[bytepos]
is_used = ((_used & bitpos) == 0)
if is_used and MODES[_mem.mode] == "DV":
mem = chirp_common.DVMemory()
mem.dv_urcall = str(_mem.urcall).rstrip()
mem.dv_rpt1call = str(_mem.r1call).rstrip()
mem.dv_rpt2call = str(_mem.r2call).rstrip()
else:
mem = chirp_common.Memory()
mem.number = number
if number < 500:
_skip = self._memobj.skip_flags[bytepos]
_pskip = self._memobj.pskip_flags[bytepos]
if _skip & bitpos:
mem.skip = "S"
elif _pskip & bitpos:
mem.skip = "P"
else:
mem.extd_number = util.get_dict_rev(_get_special(), number)
mem.immutable = ["number", "skip", "extd_number"]
if not is_used:
mem.empty = True
return mem
mem.freq = int(_mem.freq)
mem.offset = int(_mem.offset)
mem.rtone = chirp_common.TONES[_mem.rtone]
mem.ctone = chirp_common.TONES[_mem.ctone]
mem.tmode = TMODES[_mem.tmode]
mem.duplex = DUPLEX[_mem.duplex]
mem.mode = MODES[_mem.mode]
mem.dtcs = chirp_common.DTCS_CODES[_mem.dtcs]
mem.dtcs_polarity = DTCSP[_mem.dtcs_polarity]
if _mem.tune_step > 8:
mem.tuning_step = 5.0 # Sometimes TS is garbage?
else:
mem.tuning_step = chirp_common.TUNING_STEPS[_mem.tune_step]
mem.name = str(_mem.name).rstrip()
return mem
def set_memory(self, mem):
bitpos = (1 << (mem.number % 8))
bytepos = mem.number / 8
_mem = self._memobj.memory[mem.number]
_used = self._memobj.used_flags[bytepos]
was_empty = _used & bitpos
if mem.number < 500:
skip = self._memobj.skip_flags[bytepos]
pskip = self._memobj.pskip_flags[bytepos]
if mem.skip == "S":
skip |= bitpos
else:
skip &= ~bitpos
if mem.skip == "P":
pskip |= bitpos
else:
pskip &= ~bitpos
if mem.empty:
_used |= bitpos
_wipe_memory(_mem, "\xFF")
if mem.number < 500:
self._set_bank(mem.number, None)
return
_used &= ~bitpos
if was_empty:
_wipe_memory(_mem, "\x00")
_mem.freq = mem.freq
_mem.offset = mem.offset
_mem.rtone = chirp_common.TONES.index(mem.rtone)
_mem.ctone = chirp_common.TONES.index(mem.ctone)
_mem.tmode = TMODES.index(mem.tmode)
_mem.duplex = DUPLEX.index(mem.duplex)
_mem.mode = MODES.index(mem.mode)
_mem.dtcs = chirp_common.DTCS_CODES.index(mem.dtcs)
_mem.dtcs_polarity = DTCSP.index(mem.dtcs_polarity)
_mem.tune_step = chirp_common.TUNING_STEPS.index(mem.tuning_step)
_mem.name = mem.name.ljust(8)
if isinstance(mem, chirp_common.DVMemory):
_mem.urcall = mem.dv_urcall.ljust(8)
_mem.r1call = mem.dv_rpt1call.ljust(8)
_mem.r2call = mem.dv_rpt2call.ljust(8)
def get_raw_memory(self, number):
return repr(self._memobj.memory[number])
def get_urcall_list(self):
_calls = self._memobj.urcall
calls = []
for i in range(*self.URCALL_LIMIT):
calls.append(str(_calls[i-1].call))
return calls
def get_repeater_call_list(self):
_calls = self._memobj.rptcall
calls = []
for i in range(*self.RPTCALL_LIMIT):
calls.append(str(_calls[i-1].call))
return calls
def get_mycall_list(self):
_calls = self._memobj.mycall
calls = []
for i in range(*self.MYCALL_LIMIT):
calls.append(str(_calls[i-1].call))
return calls
def set_urcall_list(self, calls):
_calls = self._memobj.urcall
for i in range(*self.URCALL_LIMIT):
try:
call = calls[i-1]
except IndexError:
call = " " * 8
_calls[i-1].call = call.ljust(8)[:8]
def set_repeater_call_list(self, calls):
_calls = self._memobj.rptcall
for i in range(*self.RPTCALL_LIMIT):
try:
call = calls[i-1]
except IndexError:
call = " " * 8
_calls[i-1].call = call.ljust(8)[:8]
def set_mycall_list(self, calls):
_calls = self._memobj.mycall
for i in range(*self.MYCALL_LIMIT):
try:
call = calls[i-1]
except IndexError:
call = " " * 8
_calls[i-1].call = call.ljust(8)[:8]
chirp-20261030/chirp/drivers/ic9x.py 0000644 0001750 0001750 00000033564 15172631605 016064 0 ustar hibby hibby # 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 time
import threading
import logging
import uuid
from chirp.drivers import ic9x_ll, icf
from chirp import chirp_common, errors, util, directory
from chirp import bitwise
LOG = logging.getLogger(__name__)
IC9XA_SPECIAL = {}
IC9XB_SPECIAL = {}
for i in range(0, 25):
idA = "%iA" % i
idB = "%iB" % i
Anum = 800 + i * 2
Bnum = 400 + i * 2
IC9XA_SPECIAL[idA] = Anum
IC9XA_SPECIAL[idB] = Bnum
IC9XB_SPECIAL[idA] = Bnum
IC9XB_SPECIAL[idB] = Bnum + 1
IC9XA_SPECIAL["C0"] = IC9XB_SPECIAL["C0"] = -1
IC9XA_SPECIAL["C1"] = IC9XB_SPECIAL["C1"] = -2
IC9X_SPECIAL = {
0: {},
1: IC9XA_SPECIAL,
2: IC9XB_SPECIAL,
}
CHARSET = chirp_common.CHARSET_ALPHANUMERIC + \
"!\"#$%&'()*+,-./:;<=>?@[\\]^_`{|}~"
class Lock:
"""Maintains the state of an ic9x
This makes sure that only one RadioThread accesses the radio at one
time. It also keeps track of the last successful unlock of the radio
so that we know when to re-send the magic wakeup sequence.
"""
def __init__(self):
self.lock = threading.Lock()
self.id = str(uuid.uuid4())
self._last = 0
def __enter__(self):
LOG.debug('%s locking', self.id)
self.lock.acquire()
LOG.debug('%s locked', self.id)
def __exit__(self, exc_type, exc_value, exc_tb):
self.lock.release()
if exc_type is None:
self._last = time.time()
LOG.debug('%s unlocked success=%s', self.id, exc_type is None)
def __repr__(self):
return '' % self.id
@property
def stale(self):
return time.time() - self._last > 4
def locked(fn):
"""Decorator for operations that talk to the radio.
Runs them with the lock and sends the magic wakeup if the timer is
expired.
"""
def _lock(self, *a, **k):
with self._lock:
LOG.debug('Locked for %s', fn.__name__)
if self._lock.stale:
LOG.debug('Doing wakeup')
self._init()
r = fn(self, *a, **k)
return r
return _lock
class IC9xBank(icf.IcomNamedBank):
"""Icom 9x Bank"""
def get_name(self):
banks = self._model._radio._ic9x_get_banks()
return banks[self.index]
def set_name(self, name):
banks = self._model._radio._ic9x_get_banks()
banks[self.index] = name
self._model._radio._ic9x_set_banks(banks)
@directory.register
class IC9xRadio(icf.IcomLiveRadio):
"""Base class for Icom IC-9x radios"""
MODEL = "IC-91/92AD"
_model = "ic9x" # Fake model info for detect.py
vfo = 0
_upper = 300
_num_banks = 26
_bank_class = IC9xBank
def _get_bank(self, loc):
mem = self.get_memory(loc)
return mem._bank
def _set_bank(self, loc, bank):
mem = self.get_memory(loc)
mem._bank = bank
self.set_memory(mem)
def _get_bank_index(self, loc):
mem = self.get_memory(loc)
return mem._bank_index
def _set_bank_index(self, loc, index):
mem = self.get_memory(loc)
mem._bank_index = index
self.set_memory(mem)
def _init(self):
"""Does the magic wakeup dance"""
self.pipe.baudrate = 4800
self.pipe.timeout = 1
for i in range(5):
buf = (b'\xfe' * 8) * 20 + (
b'\xfe\xfe\x01\x80\x19\xfd')
self.pipe.write(buf)
time.sleep(0.1)
resp = self.pipe.read(5)
if b'\x80\x01\x19' in resp:
LOG.debug('Radio responded to wakeup attempt %i' % (i + 1))
self.pipe.baudrate = 38400
while True:
r = ic9x_ll.ic9x_recv(self.pipe)
if not r:
break
LOG.debug('Got post-wakeup response: %r' % r)
return
time.sleep(1)
LOG.warning('Made %i attempts to wake radio', i + 1)
raise errors.RadioNoResponse()
def __init__(self, *args, **kwargs):
if 'lock' in kwargs:
self._lock = kwargs.pop('lock')
else:
self._lock = Lock()
super().__init__(*args, **kwargs)
self.__memcache = {}
self.__bankcache = {}
@locked
def get_memory(self, number):
if isinstance(number, str):
try:
number = IC9X_SPECIAL[self.vfo][number]
except KeyError:
raise errors.InvalidMemoryLocation(
"Unknown channel %s" % number)
if number < -2 or number > 999:
raise errors.InvalidValueError("Number must be between 0 and 999")
if number in self.__memcache:
return self.__memcache[number]
try:
mem = ic9x_ll.get_memory(self.pipe, self.vfo, number)
except errors.InvalidMemoryLocation:
mem = chirp_common.Memory()
mem.number = number
if number < self._upper:
mem.empty = True
if number > self._upper or number < 0:
mem.extd_number = util.get_dict_rev(IC9X_SPECIAL,
[self.vfo][number])
mem.immutable = ["number", "skip", "bank", "bank_index",
"extd_number"]
self.__memcache[mem.number] = mem
return mem
@locked
def get_raw_memory(self, number):
mframe = ic9x_ll.get_memory_frame(self.pipe, self.vfo, number)
return repr(bitwise.parse(ic9x_ll.MEMORY_FRAME_FORMAT, mframe))
def get_memories(self, lo=0, hi=None):
if hi is None:
hi = self._upper
memories = []
for i in range(lo, hi + 1):
try:
LOG.debug("Getting %i" % i)
mem = self.get_memory(i)
if mem:
memories.append(mem)
LOG.debug("Done: %s" % mem)
except errors.InvalidMemoryLocation:
pass
except errors.InvalidDataError as e:
LOG.error("Error talking to radio: %s" % e)
break
return memories
@locked
def set_memory(self, _memory):
# Make sure we mirror the DV-ness of the new memory we're
# setting, and that we capture the Bank value of any currently
# stored memory (unless the special type is provided) and
# communicate that to the low-level routines with the special
# subclass
if isinstance(_memory, ic9x_ll.IC9xMemory) or \
isinstance(_memory, ic9x_ll.IC9xDVMemory):
memory = _memory
else:
if isinstance(_memory, chirp_common.DVMemory):
memory = ic9x_ll.IC9xDVMemory()
else:
memory = ic9x_ll.IC9xMemory()
try:
memory.clone(ic9x_ll.get_memory(self.pipe, self.vfo,
_memory.number))
except errors.InvalidMemoryLocation:
pass
memory.clone(_memory)
if memory.empty:
ic9x_ll.erase_memory(self.pipe, self.vfo, memory.number)
else:
ic9x_ll.set_memory(self.pipe, self.vfo, memory)
memory = ic9x_ll.get_memory(self.pipe, self.vfo, memory.number)
self.__memcache[memory.number] = memory
@locked
def _ic9x_get_banks(self):
if len(list(self.__bankcache.keys())) == 26:
return [self.__bankcache[k] for k in
sorted(self.__bankcache.keys())]
banks = ic9x_ll.get_banks(self.pipe, self.vfo)
i = 0
for bank in banks:
self.__bankcache[i] = bank
i += 1
return banks
@locked
def _ic9x_set_banks(self, banks):
if len(banks) != len(list(self.__bankcache.keys())):
raise errors.InvalidDataError("Invalid bank list length (%i:%i)" %
(len(banks),
len(list(self.__bankcache.keys()))))
cached_names = [str(self.__bankcache[x])
for x in sorted(self.__bankcache.keys())]
need_update = False
for i in range(0, 26):
if banks[i] != cached_names[i]:
need_update = True
self.__bankcache[i] = banks[i]
LOG.dbeug("Updating %s: %s -> %s" %
(chr(i + ord("A")), cached_names[i], banks[i]))
if need_update:
ic9x_ll.set_banks(self.pipe, self.vfo, banks)
def get_sub_devices(self):
return [IC9xRadioA(self.pipe, lock=self._lock),
IC9xRadioB(self.pipe, lock=self._lock)]
def get_features(self):
rf = chirp_common.RadioFeatures()
rf.has_sub_devices = True
rf.valid_special_chans = list(IC9X_SPECIAL[self.vfo].keys())
return rf
class IC9xRadioA(IC9xRadio):
"""IC9x Band A subdevice"""
VARIANT = "Band A"
vfo = 1
_upper = 849
def get_features(self):
rf = chirp_common.RadioFeatures()
rf.has_bank = True
rf.has_bank_index = True
rf.has_bank_names = True
rf.memory_bounds = (0, self._upper)
rf.valid_modes = ["FM", "WFM", "AM"]
rf.valid_tmodes = ["", "Tone", "TSQL", "DTCS"]
rf.valid_duplexes = ["", "-", "+"]
rf.valid_tuning_steps = list(chirp_common.TUNING_STEPS)
rf.valid_bands = [(500000, 9990000000)]
rf.valid_skips = ["", "S", "P"]
rf.valid_characters = CHARSET
rf.valid_name_length = 8
return rf
class IC9xRadioB(IC9xRadio, chirp_common.IcomDstarSupport):
"""IC9x Band B subdevice"""
VARIANT = "Band B"
vfo = 2
_upper = 399
MYCALL_LIMIT = (1, 7)
URCALL_LIMIT = (1, 61)
RPTCALL_LIMIT = (1, 61)
def get_features(self):
rf = chirp_common.RadioFeatures()
rf.has_bank = True
rf.has_bank_index = True
rf.has_bank_names = True
rf.requires_call_lists = False
rf.memory_bounds = (0, self._upper)
rf.valid_modes = ["FM", "NFM", "AM", "DV"]
rf.valid_tmodes = ["", "Tone", "TSQL", "DTCS"]
rf.valid_duplexes = ["", "-", "+"]
rf.valid_tuning_steps = list(chirp_common.TUNING_STEPS)
rf.valid_bands = [(118000000, 174000000), (350000000, 470000000)]
rf.valid_skips = ["", "S", "P"]
rf.valid_characters = CHARSET
rf.valid_name_length = 8
return rf
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.__rcalls = []
self.__mcalls = []
self.__ucalls = []
@locked
def __get_call_list(self, cache, cstype, ulimit):
if cache:
return cache
calls = []
for i in range(ulimit - 1):
call = ic9x_ll.get_call(self.pipe, cstype, i+1)
calls.append(call)
return calls
@locked
def __set_call_list(self, cache, cstype, ulimit, calls):
for i in range(ulimit - 1):
blank = " " * 8
try:
acall = cache[i]
except IndexError:
acall = blank
try:
bcall = calls[i]
except IndexError:
bcall = blank
if acall == bcall:
continue # No change to this one
ic9x_ll.set_call(self.pipe, cstype, i + 1, calls[i])
return calls
def get_mycall_list(self):
self.__mcalls = self.__get_call_list(self.__mcalls,
ic9x_ll.IC92MyCallsignFrame,
self.MYCALL_LIMIT[1])
return self.__mcalls
def get_urcall_list(self):
self.__ucalls = self.__get_call_list(self.__ucalls,
ic9x_ll.IC92YourCallsignFrame,
self.URCALL_LIMIT[1])
return self.__ucalls
def get_repeater_call_list(self):
self.__rcalls = self.__get_call_list(self.__rcalls,
ic9x_ll.IC92RepeaterCallsignFrame,
self.RPTCALL_LIMIT[1])
return self.__rcalls
def set_mycall_list(self, calls):
self.__mcalls = self.__set_call_list(self.__mcalls,
ic9x_ll.IC92MyCallsignFrame,
self.MYCALL_LIMIT[1],
calls)
def set_urcall_list(self, calls):
self.__ucalls = self.__set_call_list(self.__ucalls,
ic9x_ll.IC92YourCallsignFrame,
self.URCALL_LIMIT[1],
calls)
def set_repeater_call_list(self, calls):
self.__rcalls = self.__set_call_list(self.__rcalls,
ic9x_ll.IC92RepeaterCallsignFrame,
self.RPTCALL_LIMIT[1],
calls)
def _test():
import serial
ser = IC9xRadioB(serial.Serial(port="/dev/ttyUSB1",
baudrate=38400, timeout=0.1))
print(ser.get_urcall_list())
print("-- FOO --")
ser.set_urcall_list(["K7TAY", "FOOBAR", "BAZ"])
if __name__ == "__main__":
_test()
chirp-20261030/chirp/drivers/ic9x_icf.py 0000644 0001750 0001750 00000004715 14665027664 016713 0 ustar hibby hibby # 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 .
from chirp.drivers import icf, ic9x_icf_ll
from chirp import chirp_common, util, errors
# Don't register as this module is used to load icf file from File-Open menu
# see do_open in mainapp.py
class IC9xICFRadio(chirp_common.CloneModeRadio):
VENDOR = "Icom"
MODEL = "IC-91/92AD"
VARIANT = "ICF File"
_model = None
_upper = 1200
def get_features(self):
rf = chirp_common.RadioFeatures()
rf.has_bank = False
rf.memory_bounds = (0, self._upper)
rf.has_sub_devices = True
rf.valid_modes = ["FM", "AM"]
if "A" in self.VARIANT:
rf.valid_modes.append("WFM")
else:
rf.valid_modes.append("DV")
rf.valid_modes.append("NFM")
return rf
def get_raw_memory(self, number):
raw = ic9x_icf_ll.get_raw_memory(self._mmap, number).get_packed()
return util.hexprint(raw)
def get_memory(self, number):
return ic9x_icf_ll.get_memory(self._mmap, number)
def load_mmap(self, filename):
_icf_data, self._mmap = icf.read_file(filename)
def get_sub_devices(self):
return [IC9xICFRadioA(self._mmap),
IC9xICFRadioB(self._mmap)]
class IC9xICFRadioA(IC9xICFRadio):
VARIANT = "ICF File Band A"
_upper = 800
def get_memory(self, number):
if number > self._upper:
raise errors.InvalidMemoryLocation("Number must be <800")
return ic9x_icf_ll.get_memory(self._mmap, number)
class IC9xICFRadioB(IC9xICFRadio):
VARIANT = "ICF File Band B"
_upper = 400
def get_memory(self, number):
if number > self._upper:
raise errors.InvalidMemoryLocation("Number must be <400")
mem = ic9x_icf_ll.get_memory(self._mmap, 850 + number)
mem.number = number
return mem
chirp-20261030/chirp/drivers/ic9x_icf_ll.py 0000644 0001750 0001750 00000007335 14411666130 017365 0 ustar hibby hibby # 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 struct
from chirp import chirp_common
from chirp.memmap import MemoryMap
MEM_LOC_SIZE_A = 20
MEM_LOC_SIZE_B = MEM_LOC_SIZE_A + 1 + (3 * 8)
POS_FREQ = 0
POS_OFFSET = 3
POS_TONE = 5
POS_MODE = 6
POS_DTCS = 7
POS_TS = 8
POS_DTCSPOL = 11
POS_DUPLEX = 11
POS_NAME = 12
def get_mem_offset(number):
"""Get the offset into the memory map for memory @number"""
if number < 850:
return MEM_LOC_SIZE_A * number
else:
return (MEM_LOC_SIZE_A * 850) + (MEM_LOC_SIZE_B * (number - 850))
def get_raw_memory(mmap, number):
"""Return a raw representation of memory @number"""
offset = get_mem_offset(number)
if number >= 850:
size = MEM_LOC_SIZE_B
else:
size = MEM_LOC_SIZE_A
return MemoryMap(mmap[offset:offset+size])
def get_freq(mmap):
"""Return the memory frequency"""
if ord(mmap[10]) & 0x10:
mult = 6250
else:
mult = 5000
val, = struct.unpack(">I", "\x00" + mmap[POS_FREQ:POS_FREQ+3])
return val * mult
def get_offset(mmap):
"""Return the memory offset"""
val, = struct.unpack(">H", mmap[POS_OFFSET:POS_OFFSET+2])
return val * 5000
def get_rtone(mmap):
"""Return the memory rtone"""
val = (ord(mmap[POS_TONE]) & 0xFC) >> 2
return chirp_common.TONES[val]
def get_ctone(mmap):
"""Return the memory ctone"""
val = (ord(mmap[POS_TONE]) & 0x03) | ((ord(mmap[POS_TONE+1]) & 0xF0) >> 4)
return chirp_common.TONES[val]
def get_dtcs(mmap):
"""Return the memory dtcs value"""
val = ord(mmap[POS_DTCS]) >> 1
return chirp_common.DTCS_CODES[val]
def get_mode(mmap):
"""Return the memory mode"""
val = ord(mmap[POS_MODE]) & 0x07
modemap = ["FM", "NFM", "WFM", "AM", "DV", "FM"]
return modemap[val]
def get_ts(mmap):
"""Return the memory tuning step"""
val = (ord(mmap[POS_TS]) & 0xF0) >> 4
if val == 14:
return 5.0 # Coerce "Auto" to 5.0
icf_ts = list(chirp_common.TUNING_STEPS)
icf_ts.insert(2, 8.33)
icf_ts.insert(3, 9.00)
icf_ts.append(100.0)
icf_ts.append(125.0)
icf_ts.append(200.0)
return icf_ts[val]
def get_dtcs_polarity(mmap):
"""Return the memory dtcs polarity"""
val = (ord(mmap[POS_DTCSPOL]) & 0x03)
pols = ["NN", "NR", "RN", "RR"]
return pols[val]
def get_duplex(mmap):
"""Return the memory duplex"""
val = (ord(mmap[POS_DUPLEX]) & 0x0C) >> 2
dup = ["", "-", "+", ""]
return dup[val]
def get_name(mmap):
"""Return the memory name"""
return mmap[POS_NAME:POS_NAME+8]
def get_memory(_mmap, number):
"""Get memory @number from global memory map @_mmap"""
mmap = get_raw_memory(_mmap, number)
mem = chirp_common.Memory()
mem.number = number
mem.freq = get_freq(mmap)
mem.offset = get_offset(mmap)
mem.rtone = get_rtone(mmap)
mem.ctone = get_ctone(mmap)
mem.dtcs = get_dtcs(mmap)
mem.mode = get_mode(mmap)
mem.tuning_step = get_ts(mmap)
mem.dtcs_polarity = get_dtcs_polarity(mmap)
mem.duplex = get_duplex(mmap)
mem.name = get_name(mmap)
mem.empty = mem.freq == 0
return mem
chirp-20261030/chirp/drivers/ic9x_ll.py 0000644 0001750 0001750 00000035310 15172631605 016542 0 ustar hibby hibby # 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 struct
import logging
from chirp import chirp_common, util, errors, bitwise
from chirp.memmap import MemoryMapBytes
LOG = logging.getLogger(__name__)
TUNING_STEPS = [
5.0, 6.25, 8.33, 9.0, 10.0, 12.5, 15, 20, 25, 30, 50, 100, 125, 200
]
MODES = ["FM", "NFM", "WFM", "AM", "DV"]
DUPLEX = ["", "-", "+"]
TMODES = ["", "Tone", "TSQL", "TSQL", "DTCS", "DTCS"]
DTCS_POL = ["NN", "NR", "RN", "RR"]
MEM_LEN = 34
DV_MEM_LEN = 60
# Dirty hack until I clean up this IC9x mess
class IC9xMemory(chirp_common.Memory):
"""A dirty hack to stash bank information in a memory"""
_bank = None
_bank_index = 0
def __init__(self):
chirp_common.Memory.__init__(self)
class IC9xDVMemory(chirp_common.DVMemory):
"""See above dirty hack"""
_bank = None
_bank_index = 0
def __init__(self):
chirp_common.DVMemory.__init__(self)
def _ic9x_parse_frames(buf):
frames = []
while b"\xfe\xfe" in buf:
try:
start = buf.index(b"\xfe\xfe")
end = buf[start:].index(b"\xfd") + start + 1
except Exception:
LOG.error("No trailing bit")
break
framedata = buf[start:end]
buf = buf[end:]
try:
frame = IC92Frame()
frame.from_raw(framedata[2:-1])
frames.append(frame)
except errors.InvalidDataError as e:
LOG.error("Broken frame: %s" % e)
# LOG.debug("Parsed %i frames" % len(frames))
return frames
def ic9x_recv(pipe):
data = b""
while b'\xfd' not in data:
buf = pipe.read(1)
if not buf:
break
data += buf
return _ic9x_parse_frames(data)
def ic9x_send(pipe, buf):
"""Send @buf to @pipe, wrapped in a header and trailer. Attempt to read
any response frames, which are returned as a list"""
# Add header and trailer
realbuf = b"\xfe\xfe" + buf + b"\xfd"
# LOG.debug("Sending:\n%s" % util.hexprint(realbuf))
pipe.write(realbuf)
pipe.flush()
return ic9x_recv(pipe)
class IC92Frame:
"""IC9x frame base class"""
def get_vfo(self):
"""Return the vfo number"""
return ord(self._map[0])
def set_vfo(self, vfo):
"""Set the vfo number"""
self._map[0] = vfo
def from_raw(self, data):
"""Construct the frame from raw data"""
self._map = MemoryMapBytes(data)
def from_frame(self, frame):
"""Construct the frame by copying another frame"""
self._map = MemoryMapBytes(frame.get_raw())
def __init__(self, subcmd=0, flen=0, cmd=0x1A):
self._map = MemoryMapBytes(b"\x00" * (4 + flen))
self._map[0] = b"\x01\x80" + bytes([cmd, subcmd])
def get_payload(self):
"""Return the entire payload (sans header)"""
return MemoryMapBytes(self._map[4:])
def get_raw(self):
"""Return the raw version of the frame"""
return self._map.get_byte_compatible().get_packed()
def __str__(self):
string = "Frame VFO=%i (len = %i)\n" % (self.get_vfo(),
len(self.get_payload()))
string += util.hexprint(self.get_payload())
string += "\n"
return string
def send(self, pipe, verbose=False):
"""Send the frame to the radio via @pipe"""
if verbose:
LOG.debug("Sending:\n%s" % util.hexprint(self.get_raw()))
response = ic9x_send(pipe, self.get_raw())
while (response and len(response[0].get_raw()) > 4 and
response[0].get_raw()[4] != self.get_raw()[4]):
LOG.warning('Skipping unexpected frame:\n%s',
util.hexprint(response[0].get_raw()))
response = ic9x_recv(pipe)
if not response:
raise errors.RadioNoResponse()
return response[0]
def __setitem__(self, start, value):
self._map[start+4] = value
def __getitem__(self, index):
return self.get_payload()[index]
def __getslice__(self, start, end):
return self._map[start+4:end+4]
class IC92GetBankFrame(IC92Frame):
"""A frame for requesting bank information"""
def __init__(self):
IC92Frame.__init__(self, 0x09)
def send(self, pipe, verbose=False):
rframes = ic9x_send(pipe, self.get_raw())
if len(rframes) == 0:
raise errors.RadioNoResponse()
return rframes
class IC92BankFrame(IC92Frame):
"""A frame for bank information"""
def __init__(self):
# 1 byte for identifier
# 8 bytes for name
IC92Frame.__init__(self, 0x0B, 9)
def get_name(self):
"""Return the bank name"""
return self[1:]
def get_identifier(self):
"""Return the letter for the bank (A-Z)"""
return self[0]
def set_name(self, name):
"""Set the bank name"""
self[1] = name[:8].ljust(8)
def set_identifier(self, ident):
"""Set the letter for the bank (A-Z)"""
self[0] = ident[0]
class IC92MemClearFrame(IC92Frame):
"""A frame for clearing (erasing) a memory"""
def __init__(self, loc):
# 2 bytes for location
# 1 byte for 0xFF
IC92Frame.__init__(self, 0x00, 4)
self[0] = struct.pack(">BHB", 1, int("%i" % loc, 16), 0xFF)
class IC92MemGetFrame(IC92Frame):
"""A frame for requesting a memory"""
def __init__(self, loc, iscall=False):
# 2 bytes for location
IC92Frame.__init__(self, 0x00, 3)
if iscall:
call = 2
else:
call = 1
self[0] = struct.pack(">BH", call, int("%i" % loc, 16))
class IC92GetCallsignFrame(IC92Frame):
"""A frame for getting callsign information"""
def __init__(self, calltype, number):
IC92Frame.__init__(self, calltype, 1, 0x1D)
self[0] = chr(number)
class IC92CallsignFrame(IC92Frame):
"""A frame to communicate callsign information"""
command = 0 # Invalid
width = 8
def __init__(self, number=0, callsign=""):
# 1 byte for index
# $width bytes for callsign
IC92Frame.__init__(self, self.command, self.width+1, 0x1D)
self[0] = chr(number) + callsign[:self.width].ljust(self.width)
def get_callsign(self):
"""Return the actual callsign"""
return self[1:self.width+1].rstrip()
class IC92YourCallsignFrame(IC92CallsignFrame):
"""URCALL frame"""
command = 6 # Your
class IC92RepeaterCallsignFrame(IC92CallsignFrame):
"""RPTCALL frame"""
command = 7 # Repeater
class IC92MyCallsignFrame(IC92CallsignFrame):
"""MYCALL frame"""
command = 8 # My
width = 12 # 4 bytes for /STID
MEMORY_FRAME_FORMAT = """
struct {
u8 vfo;
bbcd number[2];
lbcd freq[5];
lbcd offset[4];
u8 unknown8;
bbcd rtone[2];
bbcd ctone[2];
bbcd dtcs[2];
u8 unknown9[2];
u8 unknown2:1,
mode:3,
tuning_step:4;
u8 unknown1:3,
tmode: 3,
duplex: 2;
u8 unknown5:4,
dtcs_polarity:2,
pskip:1,
skip:1;
char bank;
bbcd bank_index[1];
char name[8];
u8 unknown10;
u8 digital_code;
char rpt2call[8];
char rpt1call[8];
char urcall[8];
} mem[1];
"""
class IC92MemoryFrame(IC92Frame):
"""A frame for communicating memory information"""
def __init__(self):
IC92Frame.__init__(self, 0, DV_MEM_LEN)
# For good measure, here is a whole, valid memory block
# at 146.010 FM. Since the 9x will complain if any bits
# are invalid, it's easiest to start with a known-good one
# since we don't set everything.
self[0] = \
b"\x01\x00\x03\x00\x00\x01\x46\x01" + \
b"\x00\x00\x60\x00\x00\x08\x85\x08" + \
b"\x85\x00\x23\x22\x80\x06\x00\x00" + \
b"\x00\x00\x20\x20\x20\x20\x20\x20" + \
b"\x20\x20\x00\x00\x20\x20\x20\x20" + \
b"\x20\x20\x20\x20\x4b\x44\x37\x52" + \
b"\x45\x58\x20\x43\x43\x51\x43\x51" + \
b"\x43\x51\x20\x20"
def set_vfo(self, vfo):
IC92Frame.set_vfo(self, vfo)
if vfo == 1:
self._map.truncate(MEM_LEN + 4)
def set_iscall(self, iscall):
"""This frame refers to a call channel if @iscall is True"""
if iscall:
self[0] = 2
else:
self[0] = 1
def get_iscall(self):
"""Return True if this frame refers to a call channel"""
return ord(self[0]) == 2
def set_memory(self, mem):
"""Take Memory object @mem and configure the frame accordingly"""
if mem.number < 0:
self.set_iscall(True)
number = abs(mem.number) - 1
LOG.debug("Memory is %i (call %s)" %
(mem.number, self.get_iscall()))
else:
number = mem.number
_mem = bitwise.parse(MEMORY_FRAME_FORMAT, self).mem
_mem.number = number
_mem.freq = mem.freq
_mem.offset = mem.offset
_mem.rtone = int(mem.rtone * 10)
_mem.ctone = int(mem.ctone * 10)
_mem.dtcs = int(mem.dtcs)
_mem.mode = MODES.index(mem.mode)
_mem.tuning_step = TUNING_STEPS.index(mem.tuning_step)
_mem.duplex = DUPLEX.index(mem.duplex)
_mem.tmode = TMODES.index(mem.tmode)
_mem.dtcs_polarity = DTCS_POL.index(mem.dtcs_polarity)
if mem._bank is not None:
_mem.bank = chr(ord("A") + mem._bank)
_mem.bank_index = mem._bank_index
_mem.skip = mem.skip == "S"
_mem.pskip = mem.skip == "P"
_mem.name = mem.name.ljust(8)[:8]
if mem.mode == "DV":
_mem.urcall = mem.dv_urcall.upper().ljust(8)[:8]
_mem.rpt1call = mem.dv_rpt1call.upper().ljust(8)[:8]
_mem.rpt2call = mem.dv_rpt2call.upper().ljust(8)[:8]
_mem.digital_code = mem.dv_code
def get_memory(self):
"""Return a Memory object based on the contents of the frame"""
_mem = bitwise.parse(MEMORY_FRAME_FORMAT, self.get_payload()).mem
if MODES[_mem.mode] == "DV":
mem = IC9xDVMemory()
else:
mem = IC9xMemory()
mem.number = int(_mem.number)
if self.get_iscall():
mem.number = -1 - mem.number
mem.freq = int(_mem.freq)
mem.offset = int(_mem.offset)
mem.rtone = int(_mem.rtone) / 10.0
mem.ctone = int(_mem.ctone) / 10.0
mem.dtcs = int(_mem.dtcs)
mem.mode = MODES[int(_mem.mode)]
mem.tuning_step = TUNING_STEPS[int(_mem.tuning_step)]
mem.duplex = DUPLEX[int(_mem.duplex)]
mem.tmode = TMODES[int(_mem.tmode)]
mem.dtcs_polarity = DTCS_POL[int(_mem.dtcs_polarity)]
if int(_mem.bank) != 0:
mem._bank = ord(str(_mem.bank)) - ord("A")
mem._bank_index = int(_mem.bank_index)
if _mem.skip:
mem.skip = "S"
elif _mem.pskip:
mem.skip = "P"
else:
mem.skip = ""
mem.name = str(_mem.name).rstrip()
if mem.mode == "DV":
mem.dv_urcall = str(_mem.urcall).rstrip()
mem.dv_rpt1call = str(_mem.rpt1call).rstrip()
mem.dv_rpt2call = str(_mem.rpt2call).rstrip()
mem.dv_code = int(_mem.digital_code)
return mem
def get_memory_frame(pipe, vfo, number):
"""Get the memory frame for @vfo and @number via @pipe"""
if number < 0:
number = abs(number + 1)
call = True
else:
call = False
frame = IC92MemGetFrame(number, call)
frame.set_vfo(vfo)
return frame.send(pipe)
def get_memory(pipe, vfo, number):
"""Get a memory object for @vfo and @number via @pipe"""
rframe = get_memory_frame(pipe, vfo, number)
if len(rframe.get_payload()) < 1:
raise errors.RadioError("Unexpected empty response from radio")
if rframe.get_payload()[3] == b'\xff':
raise errors.InvalidMemoryLocation("Radio says location is empty")
mf = IC92MemoryFrame()
mf.from_frame(rframe)
if mf.get_memory().number != number:
raise errors.RadioError('get_memory() wanted %i got %i' % (
number, mf.get_memory().number))
return mf.get_memory()
def set_memory(pipe, vfo, memory):
"""Set memory @memory on @vfo via @pipe"""
frame = IC92MemoryFrame()
frame.set_memory(memory)
frame.set_vfo(vfo)
# LOG.debug("Sending (%i):" % (len(frame.get_raw())))
# LOG.debug(util.hexprint(frame.get_raw()))
rframe = frame.send(pipe)
if rframe.get_raw()[2] != 0xfb:
raise errors.InvalidDataError("Radio reported error:\n%s" %
util.hexprint(rframe.get_payload()))
def erase_memory(pipe, vfo, number):
"""Erase memory @number on @vfo via @pipe"""
frame = IC92MemClearFrame(number)
frame.set_vfo(vfo)
rframe = frame.send(pipe)
if rframe.get_raw()[2] != 0xfb:
raise errors.InvalidDataError("Radio reported error")
def get_banks(pipe, vfo):
"""Get banks for @vfo via @pipe"""
frame = IC92GetBankFrame()
frame.set_vfo(vfo)
rframes = frame.send(pipe)
if vfo == 1:
base = 180
else:
base = 237
banks = []
for i in range(base, base+26):
bframe = IC92BankFrame()
bframe.from_frame(rframes[i])
banks.append(bframe.get_name().rstrip())
return banks
def set_banks(pipe, vfo, banks):
"""Set banks for @vfo via @pipe"""
for i in range(0, 26):
bframe = IC92BankFrame()
bframe.set_vfo(vfo)
bframe.set_identifier(chr(i + ord("A")))
bframe.set_name(banks[i])
rframe = bframe.send(pipe)
if rframe.get_payload() != "\xfb":
raise errors.InvalidDataError("Radio reported error")
def get_call(pipe, cstype, number):
"""Get @cstype callsign @number via @pipe"""
cframe = IC92GetCallsignFrame(cstype.command, number)
cframe.set_vfo(2)
rframe = cframe.send(pipe)
cframe = IC92CallsignFrame()
cframe.from_frame(rframe)
return cframe.get_callsign()
def set_call(pipe, cstype, number, call):
"""Set @cstype @call at position @number via @pipe"""
cframe = cstype(number, call)
cframe.set_vfo(2)
rframe = cframe.send(pipe)
if rframe.get_payload() != "\xfb":
raise errors.RadioError("Radio reported error")
chirp-20261030/chirp/drivers/icf.py 0000644 0001750 0001750 00000110757 15177323550 015753 0 ustar hibby hibby # 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 binascii
import hashlib
import os
import struct
import re
import time
import logging
from chirp import chirp_common, errors, util, memmap
from chirp import directory
from chirp.settings import RadioSetting, RadioSettingGroup, \
RadioSettingValueString, RadioSettings
LOG = logging.getLogger(__name__)
CMD_CLONE_ID = 0xE0
CMD_CLONE_MODEL = 0xE1
CMD_CLONE_OUT = 0xE2
CMD_CLONE_IN = 0xE3
CMD_CLONE_DAT = 0xE4
CMD_CLONE_END = 0xE5
CMD_CLONE_OK = 0xE6
CMD_CLONE_HISPEED = 0xE8
ADDR_PC = 0xEE
ADDR_RADIO = 0xEF
SAVE_PIPE = None
TRACE_ICF = 'CHIRP_DEBUG_ICF' in os.environ
class IcfFrame:
"""A single ICF communication frame"""
def __str__(self):
addrs = {ADDR_PC: "PC",
ADDR_RADIO: "Radio"}
cmds = {CMD_CLONE_ID: "ID",
CMD_CLONE_MODEL: "Model",
CMD_CLONE_OUT: "Clone out",
CMD_CLONE_IN: "Clone in",
CMD_CLONE_DAT: "Clone data",
CMD_CLONE_END: "Clone end",
CMD_CLONE_OK: "Clone OK",
CMD_CLONE_HISPEED: "Clone hispeed"}
return "%s -> %s [%s]:\n%s" % (addrs.get(self.src, '??'),
addrs.get(self.dst, '??'),
cmds.get(self.cmd, '??'),
util.hexprint(self.payload))
def __init__(self, src=0, dst=0, cmd=0):
self.src = src
self.dst = dst
self.cmd = cmd
self.payload = b''
@classmethod
def parse(cls, data):
"""Parse an ICF frame of unknown type from the beginning of @data"""
frame = cls(util.byte_to_int(data[2]),
util.byte_to_int(data[3]),
util.byte_to_int(data[4]))
try:
end = data.index(b"\xFD")
except ValueError:
LOG.warning('Frame parsed with no end')
return None
assert isinstance(data, bytes), (
'parse_frame_generic() expected bytes, '
'but got %s' % data.__class__)
if data[end + 1:]:
LOG.warning('Frame parsed with trailing data')
frame.payload = data[5:end]
return frame
def pack(self):
return (b'\xfe\xfe' +
struct.pack('BBB', self.src, self.dst, self.cmd) +
self.payload + b'\xfd')
class RadioStream:
"""A class to make reading a stream of IcfFrames easier"""
def __init__(self, pipe):
self.pipe = pipe
self.data = bytes()
self.iecho = None
def _process_frames(self):
if not self.data.startswith(b"\xFE\xFE"):
LOG.error("Out of sync with radio:\n%s" % util.hexprint(self.data))
raise errors.InvalidDataError("Out of sync with radio")
elif len(self.data) < 5:
return [] # Not enough data for a full frame
frames = []
while self.data:
# Hispeed clone frames start with a pad of \xFE, so strip those
# away until we have the two we expect
while self.data.startswith(b'\xfe\xfe\xfe'):
self.data = self.data[1:]
try:
cmd = self.data[4]
except IndexError:
break # Out of data
try:
end = self.data.index(b'\xFD')
frame = IcfFrame.parse(self.data[:end + 1])
self.data = self.data[end + 1:]
if frame.src == ADDR_PC and frame.dst == ADDR_RADIO:
# PC echo, ignore
if self.iecho is None:
LOG.info('Detected an echoing cable')
self.iecho = True
else:
if TRACE_ICF:
LOG.debug('Received frame:\n%s' % frame)
frames.append(frame)
except ValueError:
# no data
break
except errors.InvalidDataError as e:
LOG.error("Failed to parse frame (cmd=%i): %s" % (cmd, e))
return []
if frames and self.iecho is None:
LOG.info('Non-echoing cable detected')
self.iecho = False
return frames
def get_frames(self, nolimit=False, limit=None):
"""Read any pending frames from the stream"""
while True:
_data = self.pipe.read(1)
if not _data:
if limit:
LOG.warning('Hit timeout before one frame')
break
else:
self.data += _data
if limit and 0xFD in self.data:
break
if not nolimit and len(self.data) > 128 and 0xFD in self.data:
break # Give us a chance to do some status
if len(self.data) > 1024:
break # Avoid an endless loop of chewing garbage
if not self.data:
return []
return self._process_frames()
def munch_echo(self):
if self.iecho is not False:
f = self.get_frames(limit=1)
if len(f) != 0:
LOG.warning('Expected to read one echo frame, found %i',
len(f))
if f and f[0].src == ADDR_RADIO:
LOG.warning('Expected PC echo but found radio frame!')
def decode_model(data):
if len(data) == 49:
rev = util.byte_to_int(data[5])
LOG.info('Radio revision is %i' % rev)
comment = data[6:6 + 16]
LOG.info('Radio comment is %r' % comment)
serial = binascii.unhexlify(data[35:35 + 14])
model, b1, b2, u1, s3 = struct.unpack('>HBBBH', serial)
serial_num = '%04i%02i%02i%04i' % (model, b1, b2, s3)
LOG.info('Radio serial is %r' % serial_num)
if len(data) == 56:
rev = None
comment = data[6:6 + 32]
LOG.info('Radio comment is %r' % comment)
else:
rev = None
LOG.info('Unable to decode %i-byte model data' % len(data))
return rev
def get_model_data(radio, mdata=b"\x00\x00\x00\x00", stream=None):
"""Query the @radio for its model data"""
send_clone_frame(radio, CMD_CLONE_ID, mdata, raw=True)
if stream is None:
stream = RadioStream(radio.pipe)
frames = stream.get_frames()
if len(frames) == 0:
raise errors.RadioNoResponse()
if len(frames) != 1:
raise errors.RadioError("Unexpected response from radio")
LOG.debug('Model query result:\n%s' % frames[0])
return frames[0].payload
def get_clone_resp(pipe, length=None, max_count=None):
"""Read the response to a clone frame"""
def exit_criteria(buf, length, cnt, max_count):
"""Stop reading a clone response if we have enough data or encounter
the end of a frame"""
if max_count is not None:
if cnt >= max_count:
return True
if length is None:
return buf.endswith("\xfd")
else:
return len(buf) == length
resp = ""
cnt = 0
while not exit_criteria(resp, length, cnt, max_count):
resp += pipe.read(1)
cnt += 1
return resp
def send_clone_frame(radio, cmd, data, raw=False, checksum=False):
"""Send a clone frame with @cmd and @data to the @radio"""
frame = IcfFrame(ADDR_PC, ADDR_RADIO, cmd)
frame.payload = radio.get_payload(data, raw, checksum)
if SAVE_PIPE:
LOG.debug("Saving data...")
SAVE_PIPE.write(frame.pack())
if TRACE_ICF:
LOG.debug('Sending:\n%s' % frame)
if cmd == CMD_CLONE_DAT:
# Uncomment to avoid cloning to the radio
# return frame
pass
radio.pipe.write(frame.pack())
return frame
def process_data_frame(radio, frame, _mmap):
"""Process a data frame, adding the payload to @_mmap"""
_data = radio.process_frame_payload(frame.payload)
# NOTE: On the _data[N:N+1] below. Because:
# - on py2 bytes[N] is a bytes
# - on py3 bytes[N] is an int
# - on both bytes[N:M] is a bytes
# So we do a slice so we get consistent behavior
# Checksum logic added by Rick DeWitt, 9/2019, issue # 7075
if len(_mmap) >= 0x10000: # This map size not tested for checksum
saddr, = struct.unpack(">I", _data[0:4])
length, = struct.unpack("B", _data[4:5])
data = _data[5:5+length]
sumc, = struct.unpack("B", _data[5+length:])
addr1, = struct.unpack("B", _data[0:1])
addr2, = struct.unpack("B", _data[1:2])
addr3, = struct.unpack("B", _data[2:3])
addr4, = struct.unpack("B", _data[3:4])
else: # But this one has been tested for raw mode radio (IC-2730)
saddr, = struct.unpack(">H", _data[0:2])
length, = struct.unpack("B", _data[2:3])
data = _data[3:3+length]
sumc, = struct.unpack("B", _data[3+length:])
addr1, = struct.unpack("B", _data[0:1])
addr2, = struct.unpack("B", _data[1:2])
addr3 = 0
addr4 = 0
cs = addr1 + addr2 + addr3 + addr4 + length
for byte in data:
cs += byte
vx = ((cs ^ 0xFFFF) + 1) & 0xFF
if sumc != vx:
LOG.error("Bad checksum in address %04X frame: %02x "
"calculated, %02x sent!" % (saddr, vx, sumc))
raise errors.InvalidDataError(
"Checksum error in download!")
try:
_mmap[saddr] = data
except IndexError:
LOG.error("Error trying to set %i bytes at %05x (max %05x)" %
(length, saddr, len(_mmap)))
return saddr, saddr + length
def start_hispeed_clone(radio, cmd):
"""Send the magic incantation to the radio to go fast"""
frame = IcfFrame(ADDR_PC, ADDR_RADIO, CMD_CLONE_HISPEED)
frame.payload = radio.get_model() + b'\x00\x00\x02\x01\xFD'
LOG.debug("Starting HiSpeed:\n%s" % frame)
radio.pipe.write(b'\xFE' * 20 + frame.pack())
radio.pipe.flush()
resp = radio.pipe.read(128)
LOG.debug("Response:\n%s" % util.hexprint(resp))
LOG.info("Switching to 38400 baud")
radio.pipe.baudrate = 38400
frame = IcfFrame(ADDR_PC, ADDR_RADIO, cmd)
frame.payload = radio.get_model()[:3] + b'\x00'
LOG.debug("Starting HiSpeed Clone:\n%s" % frame)
radio.pipe.write(b'\xFE' * 14 + frame.pack())
radio.pipe.flush()
def _id_query_payload(radio):
"""Build the payload for the initial CLONE_ID query.
Most Icom clone-mode radios reply to a query with all-zero payload.
A few (e.g. IC-2730E) only respond when the query carries the radio's
own model code with the last byte zeroed; those drivers set the
`_id_query_with_model` flag (defined on IcomCloneModeRadio).
"""
if radio._id_query_with_model:
model = radio.get_model().ljust(4, b"\x00")[:4]
return model[:3] + b"\x00"
return b"\x00\x00\x00\x00"
def _clone_from_radio(radio):
md = get_model_data(radio, mdata=_id_query_payload(radio))
try:
radio_rev = decode_model(md)
except Exception:
LOG.error('Failed to decode model data')
radio_rev = None
if md[0:4] != radio.get_model():
LOG.info("This model: %s" % util.hexprint(md[0:4]))
LOG.info("Supp model: %s" % util.hexprint(radio.get_model()))
raise errors.RadioError("I can't talk to this model")
if radio.is_hispeed():
start_hispeed_clone(radio, CMD_CLONE_OUT)
else:
send_clone_frame(radio, CMD_CLONE_OUT,
radio.get_model(),
raw=True)
LOG.debug("Sent clone frame")
stream = RadioStream(radio.pipe)
addr = 0
_mmap = memmap.MemoryMapBytes(bytes(b'\x00') * radio.get_memsize())
last_size = 0
got_end = False
last_frame = time.time()
timeout = 10
while not got_end:
frames = stream.get_frames()
if not frames and (time.time() - last_frame) > timeout:
break
for frame in frames:
last_frame = time.time()
if frame.cmd == CMD_CLONE_DAT:
src, dst = process_data_frame(radio, frame, _mmap)
if last_size != (dst - src):
LOG.debug("ICF Size change from %i to %i at %04x" %
(last_size, dst - src, src))
last_size = dst - src
if addr != src:
LOG.debug("ICF GAP %04x - %04x" % (addr, src))
addr = dst
elif frame.cmd == CMD_CLONE_END:
LOG.debug("End frame (%i):\n%s" %
(len(frame.payload), util.hexprint(frame.payload)))
LOG.debug("Last addr: %04x" % addr)
# For variable-length radios, make sure we don't
# return a longer map than we got from the radio.
_mmap.truncate(addr)
got_end = True
if radio.status_fn:
status = chirp_common.Status()
status.msg = "Cloning from radio"
status.max = radio.get_memsize()
status.cur = addr
radio.status_fn(status)
if not got_end:
LOG.error('clone_from_radio ending at address %06X before '
'CLONE_END; stream buffer is:\n%s',
addr, util.hexprint(stream.data))
raise errors.RadioError('Data stream stopped before end-of-clone '
'received')
return _mmap
def clone_from_radio(radio):
"""Do a full clone out of the radio's memory"""
try:
return _clone_from_radio(radio)
except errors.RadioError:
raise
except Exception as e:
raise errors.RadioError("Failed to communicate with the radio: %s" % e)
def send_mem_chunk(radio, stream, start, stop, bs=32):
"""Send a single chunk of the radio's memory from @start-@stop"""
_mmap = radio.get_mmap().get_byte_compatible()
status = chirp_common.Status()
status.msg = "Cloning to radio"
status.max = radio.get_memsize()
LOG.debug('Sending memory range %06x - %06x @ %i bytes' % (
start, stop, bs))
for i in range(start, stop, bs):
if i + bs < stop:
size = bs
else:
size = stop - i
if radio.get_memsize() >= 0x10000:
chunk = struct.pack(">IB", i, size)
else:
chunk = struct.pack(">HB", i, size)
chunk += _mmap[i:i+size]
send_clone_frame(radio,
CMD_CLONE_DAT,
chunk,
raw=False,
checksum=True)
stream.munch_echo()
if radio.status_fn:
status.cur = i+bs
radio.status_fn(status)
return True
def _clone_to_radio(radio):
global SAVE_PIPE
# Uncomment to save out a capture of what we actually write to the radio
# SAVE_PIPE = file("pipe_capture.log", "w", 0)
mdata = _id_query_payload(radio)
stream = RadioStream(radio.pipe)
md = get_model_data(radio, mdata=mdata, stream=stream)
if radio._double_ident:
md = get_model_data(radio, mdata=mdata, stream=stream)
if md[0:4] != radio.get_model():
raise errors.RadioError("I can't talk to this model")
try:
radio_rev = decode_model(md)
except Exception:
LOG.error('Failed to decode model data')
radio_rev = None
image_rev = radio._icf_data.get('MapRev', 1)
if radio_rev is not None and radio_rev != image_rev:
raise errors.RadioError('Radio revision %i does not match image %i' % (
radio_rev, image_rev))
# This mimics what the Icom software does, but isn't required and just
# takes longer
# md = get_model_data(radio, mdata=md[0:2]+"\x00\x00")
# md = get_model_data(radio, mdata=md[0:2]+"\x00\x00")
if radio.is_hispeed():
start_hispeed_clone(radio, CMD_CLONE_IN)
else:
send_clone_frame(radio, CMD_CLONE_IN,
radio.get_model(),
raw=True)
frames = []
for start, stop, bs in radio.get_ranges():
if not send_mem_chunk(radio, stream, start, stop, bs):
break
send_clone_frame(radio, CMD_CLONE_END,
radio.get_endframe(),
raw=True)
if SAVE_PIPE:
SAVE_PIPE.close()
SAVE_PIPE = None
for i in range(0, 10):
try:
frames += stream.get_frames(True)
result = frames[-1]
except IndexError:
LOG.debug("Waiting for clone result...")
time.sleep(0.5)
if radio.ignore_clone_result():
return True
if len(frames) == 0:
raise errors.RadioNoResponse()
elif result.cmd != CMD_CLONE_OK:
LOG.error('Clone failed result frame:\n%s' % result)
raise errors.RadioError('Radio rejected clone')
else:
LOG.debug('Clone result frame:\n%s' % result)
return result.payload[0] == bytes(b'\x00')
def clone_to_radio(radio):
"""Initiate a full memory clone out to @radio"""
try:
return _clone_to_radio(radio)
except errors.RadioError:
raise
except Exception as e:
logging.exception("Failed to communicate with the radio")
raise errors.RadioError("Failed to communicate with the radio: %s" % e)
def convert_model(mod_str):
"""Convert an ICF-style model string into what we get from the radio"""
data = b""
for i in range(0, len(mod_str), 2):
hexval = mod_str[i:i+2]
intval = int(hexval, 16)
data += bytes([intval])
return data
def convert_data_line(line):
"""Convert an ICF data line to raw memory format"""
if line.startswith("#"):
return ""
line = line.strip()
# Detection of the prefix length. The code assumes that the data line
# length (without the prefix) is multiply of 8 characters (i.e. multiply
# of 4 bytes), so the rest (remainder of division by 8) has to be the
# prefix (prefix = address + length) - for small memory the address is
# 2 bytes long and the length indicator is 1 byte long which means 3 bytes
# in total which is 6 characters in total for the prefix on the ICF line.
if len(line) % 8 == 6:
# Small memory (< 0x10000)
size = int(line[4:6], 16)
data = line[6:]
else:
# Large memory (>= 0x10000)
size = int(line[8:10], 16)
data = line[10:]
_mmap = b""
i = 0
while i < (size * 2):
try:
val = int("%s%s" % (data[i], data[i+1]), 16)
i += 2
_mmap += struct.pack("B", val)
except ValueError as e:
LOG.debug("Failed to parse byte: %s" % e)
break
return _mmap
def read_file(filename):
"""Read an ICF file and return the model string and memory data"""
f = open(filename)
mod_str = f.readline()
dat = f.readlines()
icfdata = {
'model': convert_model(mod_str.strip())
}
_mmap = b""
for line in dat:
if line.startswith("#"):
try:
key, value = line.strip().split('=', 1)
if key == '#EtcData':
value = int(value, 16)
elif value.isdigit():
value = int(value)
icfdata[key[1:]] = value
except ValueError:
# Some old files have lines with just #
pass
else:
line_data = convert_data_line(line)
_mmap += line_data
if 'recordsize' not in icfdata:
icfdata['recordsize'] = len(line_data)
return icfdata, memmap.MemoryMapBytes(_mmap)
def _encode_model_for_icf(model):
""" Encode the model magically for the ICF file hash.
If model is: AB CD 00 00
Then the magic is AA BB CC DD AB CD
"""
a = (util.byte_to_int(model[0]) & 0xF0) >> 4
b = util.byte_to_int(model[0]) & 0x0F
c = (util.byte_to_int(model[1]) & 0xF0) >> 4
d = util.byte_to_int(model[1]) & 0x0F
sequence = [(a << 4) | a,
(b << 4) | b,
(c << 4) | c,
(d << 4) | d,
(a << 4) | b,
(c << 4) | d]
return b''.join(util.int_to_byte(x) for x in sequence)
def write_file(radio, filename):
"""Write an ICF file"""
f = open(filename, 'w', newline='\r\n')
model = radio._model
mdata = '%02x%02x%02x%02x' % (ord(model[0]),
ord(model[1]),
ord(model[2]),
ord(model[3]))
data = radio._mmap.get_packed()
f.write('%s\n' % mdata)
f.write('#Comment=%s\n' % radio._icf_data.get('Comment', ''))
f.write('#MapRev=%i\n' % radio._icf_data.get('MapRev', 1))
f.write('#EtcData=%06x\n' % radio._icf_data.get('EtcData', 0))
binicf = _encode_model_for_icf(model)
# ICF files for newer models (probably everything with an SD card
# slot) store a hash on the last line of the ICF file, as
# #CD=$hexdigest. This is an MD5 sum of a sequence that starts
# with the specially-encoded model from _encode_model_for_icf(),
# followed by the unencoded lines of the ICF file (including
# address and length values). So for an ID31 the value we hash
# looks like this (in hex, line breaks for the humans and pep8):
#
# 333322223322
# 0000000020
# dddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddd
# 0000002020
# dddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddd
# ... and so on
LOG.debug('ICF hash header: %r' % binascii.hexlify(binicf))
blksize = radio._icf_data.get('recordsize', 32)
for addr in range(0, len(data), blksize):
block = binascii.hexlify(data[addr:addr + blksize]).decode().upper()
if blksize == 32:
line = '%08X%02X%s' % (addr, blksize, block)
binicf += binascii.unhexlify(line)
else:
line = '%04X%02X%s' % (addr, blksize, block)
f.write(line + '\n')
if blksize == 32:
hash = hashlib.md5(binicf)
digest = hash.hexdigest().upper()
LOG.debug('ICF hash digest: %s' % digest)
f.write('#CD=%s\n' % digest)
f.close()
def is_9x_icf(filename):
"""Returns True if @filename is an IC9x ICF file"""
try:
with open(filename) as f:
mdata = f.read(8)
except UnicodeDecodeError:
# ICF files are ASCII, so any unicode failure means no.
return False
return mdata in ["30660000", "28880000"]
def is_icf_file(filename):
"""Returns True if @filename is an ICF file"""
try:
with open(filename) as f:
data = f.readline()
data += f.readline()
except UnicodeDecodeError:
# ICF files are ASCII, so any unicode failure means no.
return False
data = data.replace("\n", "").replace("\r", "")
return bool(re.match("^[0-9]{8}#", data))
class IcomBank(chirp_common.Bank):
"""A bank that works for all Icom radios"""
# Integral index of the bank (not to be confused with per-memory
# bank indexes
index = 0
class IcomNamedBank(IcomBank):
"""A bank with an adjustable name"""
def set_name(self, name):
"""Set the name of the bank"""
pass
class IcomBankModel(chirp_common.BankModel):
"""Icom radios all have pretty much the same simple bank model. This
central implementation can, with a few icom-specific radio interfaces
serve most/all of them"""
def get_num_mappings(self):
return self._radio._num_banks
def get_mappings(self):
banks = []
for i in range(0, self._radio._num_banks):
index = chr(ord("A") + i)
bank = self._radio._bank_class(self, index, "BANK-%s" % index)
bank.index = i
banks.append(bank)
return banks
def add_memory_to_mapping(self, memory, bank):
self._radio._set_bank(memory.number, bank.index)
def remove_memory_from_mapping(self, memory, bank):
if self._radio._get_bank(memory.number) != bank.index:
raise Exception("Memory %i not in bank %s. Cannot remove." %
(memory.number, bank))
self._radio._set_bank(memory.number, None)
def get_mapping_memories(self, bank):
memories = []
for i in range(*self._radio.get_features().memory_bounds):
if self._radio._get_bank(i) == bank.index:
memories.append(self._radio.get_memory(i))
return memories
def get_memory_mappings(self, memory):
index = self._radio._get_bank(memory.number)
if index is None:
return []
else:
try:
return [self.get_mappings()[index]]
except IndexError:
LOG.error('Invalid bank index %i for memory %i' % (
index, memory.number))
return []
class IcomIndexedBankModel(IcomBankModel,
chirp_common.MappingModelIndexInterface):
"""Generic bank model for Icom radios with indexed banks"""
def get_index_bounds(self):
return self._radio._bank_index_bounds
def get_memory_index(self, memory, bank):
return self._radio._get_bank_index(memory.number)
def set_memory_index(self, memory, bank, index):
if bank not in self.get_memory_mappings(memory):
raise Exception("Memory %i is not in bank %s" % (memory.number,
bank))
if index not in list(range(*self._radio._bank_index_bounds)):
raise Exception("Invalid index")
self._radio._set_bank_index(memory.number, index)
def get_next_mapping_index(self, bank):
indexes = []
for i in range(*self._radio.get_features().memory_bounds):
if self._radio._get_bank(i) == bank.index:
indexes.append(self._radio._get_bank_index(i))
for i in range(0, 256):
if i not in indexes:
return i
raise errors.RadioError("Out of slots in this bank")
def compute_checksum(data):
cs = 0
for byte in data:
cs += byte
return ((cs ^ 0xFFFF) + 1) & 0xFF
class IcomCloneModeRadio(chirp_common.CloneModeRadio):
"""Base class for Icom clone-mode radios"""
VENDOR = "Icom"
BAUDRATE = 9600
FORMATS = [directory.register_format('Icom ICF', '*.icf')]
_model = "\x00\x00\x00\x00" # 4-byte model string
_endframe = "" # Model-unique ending frame
_ignore_clone_ok = False
_ranges = [] # Ranges of the mmap to send to the radio
_num_banks = 10 # Most simple Icoms have 10 banks, A-J
_bank_index_bounds = (0, 99)
_bank_class = IcomBank
_can_hispeed = False
_double_ident = False # A couple radios require double ident before upload
# If True, the initial CLONE_ID query payload carries the radio's
# own model code (with the last byte zeroed) instead of all zeros.
# Required by IC-2730E; most clone-mode Icoms answer to all-zeros.
_id_query_with_model = False
# Newer radios (ID51Plus2, ID5100, IC2730) use a slightly
# different CLONE_DAT format
_raw_frames = False
_highbit_flip = False
# Attributes that get put into ICF files when we export. The meaning
# of these are unknown at this time, but differ per model (at least).
# Saving ICF files will not be offered unless _icf_data has attributes.
_icf_data = {
'recordsize': 16,
}
@classmethod
def is_hispeed(cls):
"""Returns True if the radio supports hispeed cloning"""
return cls._can_hispeed
@classmethod
def get_model(cls):
"""Returns the Icom model data for this radio"""
return bytes([util.byte_to_int(x) for x in cls._model])
def get_endframe(self):
"""Returns the magic clone end frame for this radio"""
return bytes([util.byte_to_int(x) for x in self._endframe])
def ignore_clone_result(self):
"""ignore the result of a clone"""
return self._ignore_clone_ok
def get_ranges(self):
"""Returns the ranges this radio likes to have in a clone"""
return self._ranges
def process_frame_payload(self, payload):
if self._raw_frames:
return unescape_raw_bytes(payload)
# Legacy frame format: convert BCD-encoded data to raw"""
bcddata = payload
data = b""
i = 0
while i+1 < len(bcddata):
try:
val = int(b"%s%s" % (util.int_to_byte(bcddata[i]),
util.int_to_byte(bcddata[i+1])), 16)
i += 2
data += struct.pack("B", val)
except (ValueError, TypeError) as e:
LOG.error("Failed to parse byte %i (%r): %s" % (i,
bcddata[i:i+2],
e))
break
return data
def get_payload(self, data, raw, checksum):
"""Returns the data with optional checksum BCD-encoded for the radio"""
if checksum:
data += util.int_to_byte(compute_checksum(data))
if self._raw_frames:
# Always raw format, no need to check raw
return b''.join(escape_raw_byte(b) for b in data)
if raw:
return data
payload = b''
for byte in data:
payload += b"%02X" % util.byte_to_int(byte)
return payload
def sync_in(self):
_mmap = clone_from_radio(self)
if self._highbit_flip:
LOG.debug('Flipping high bits of received image')
map_cls = _mmap.__class__
_mmap = flip_high_order_bit(_mmap.get_packed())
self._mmap = map_cls(_mmap)
else:
self._mmap = _mmap
self.process_mmap()
def get_mmap(self):
if self._highbit_flip:
map_cls = self._mmap.__class__
LOG.debug('Flipping high bits of image')
return map_cls(flip_high_order_bit(self._mmap.get_packed()))
else:
return self._mmap
def sync_out(self):
# We always have to start in low-speed mode. The UI may have
# handed us a different rate left over from a prior hispeed run,
# so reset to the driver's configured rate here. For most Icom
# clone-mode radios that's 9600 (the default inherited from
# chirp_common.Radio); a few (e.g. IC-2730E) override it.
self.pipe.baudrate = self.BAUD_RATE
clone_to_radio(self)
def get_bank_model(self):
rf = self.get_features()
if rf.has_bank:
if rf.has_bank_index:
return IcomIndexedBankModel(self)
else:
return IcomBankModel(self)
else:
return None
# Icom-specific bank routines
def _get_bank(self, loc):
"""Get the integral bank index of memory @loc, or None"""
raise Exception("Not implemented")
def _set_bank(self, loc, index):
"""Set the integral bank index of memory @loc to @index, or
no bank if None"""
raise Exception("Not implemented")
def _make_call_list_setting_group(self, listname):
current = getattr(self, 'get_%s_list' % listname)()
nice_name = listname.split('_', 1)[0].upper()
group = RadioSettingGroup('%s_list' % listname,
'%s List' % nice_name)
charset = chirp_common.CHARSET_UPPER_NUMERIC + '-'
for i, cs in enumerate(current):
cs = ''.join(filter(lambda c: c in charset, cs))
group.append(RadioSetting('%03i' % i, '%i' % i,
RadioSettingValueString(0, 8, cs)))
return group
def get_settings(self):
if isinstance(self, chirp_common.IcomDstarSupport):
dstar = RadioSettingGroup('dstar', 'D-STAR')
dstar.append(self._make_call_list_setting_group('urcall'))
dstar.append(self._make_call_list_setting_group('repeater_call'))
dstar.append(self._make_call_list_setting_group('mycall'))
return RadioSettings(dstar)
return []
def _apply_call_list_setting(self, dstar, listname):
listgroup = dstar['%s_list' % listname]
calls = [str(listgroup[i].value)
for i in sorted(listgroup.keys())]
getattr(self, 'set_%s_list' % listname)(calls)
def set_settings(self, settings):
for group in settings:
if group.get_name() == 'dstar':
self._apply_call_list_setting(group, 'mycall')
self._apply_call_list_setting(group, 'urcall')
self._apply_call_list_setting(group, 'repeater_call')
def load_mmap(self, filename):
if filename.lower().endswith('.icf'):
self._icf_data, self._mmap = read_file(filename)
LOG.debug('Loaded ICF file %s with data: %s' % (filename,
self._icf_data))
self.process_mmap()
else:
chirp_common.CloneModeRadio.load_mmap(self, filename)
def save_mmap(self, filename):
if filename.lower().endswith('.icf'):
write_file(self, filename)
else:
chirp_common.CloneModeRadio.save_mmap(self, filename)
@classmethod
def match_model(cls, filedata, filename):
if (filedata[:4] == binascii.hexlify(cls.get_model())[:4] and
filename.lower().endswith('.icf')):
return True
else:
return super(IcomCloneModeRadio, cls).match_model(filedata,
filename)
def flip_high_order_bit(data):
return bytes([d ^ 0x80 for d in list(data)])
def escape_raw_byte(byte):
"""Escapes a raw byte for sending to the radio"""
# Certain bytes are used as control characters to the radio, so if one of
# these bytes is present in the stream to the radio, it gets escaped as
# 0xff followed by (byte & 0x0f)
if byte > 0xf9:
return bytes([0xff, byte & 0xf])
return bytes([byte])
def unescape_raw_bytes(escaped_data):
"""Unescapes raw bytes from the radio."""
data = b""
i = 0
while i < len(escaped_data):
byte = escaped_data[i]
if byte == 0xff:
if i + 1 >= len(escaped_data):
raise errors.InvalidDataError(
"Unexpected escape character at end of data")
i += 1
byte = 0xf0 | escaped_data[i]
data += bytes([byte])
i += 1
return data
class IcomLiveRadio(chirp_common.LiveRadio):
"""Base class for an Icom Live-mode radio"""
VENDOR = "Icom"
BAUD_RATE = 38400
_num_banks = 26 # Most live Icoms have 26 banks, A-Z
_bank_index_bounds = (0, 99)
_bank_class = IcomBank
def get_bank_model(self):
rf = self.get_features()
if rf.has_bank:
if rf.has_bank_index:
return IcomIndexedBankModel(self)
else:
return IcomBankModel(self)
else:
return None
def warp_byte_size(inbytes, obw=8, ibw=8, iskip=0, opad=0):
"""Convert between "byte sizes".
This will pack N-bit characters into a sequence of 8-bit bytes,
and perform the opposite.
ibw (input bit width) is the width of the storage
obw (output bit width) is the width of the characters to extract
iskip is the number of input padding bits to skip
opad is the number of output zero padding bits to add
ibw=8,obw=7 will pull seven-bit characters from a sequence of bytes
ibw=7,obw=8 will pack seven-bit characters into a sequence of bytes
"""
if isinstance(inbytes, str):
inbytes = [ord(x) for x in inbytes]
outbit = opad
tmp = 0
stripmask = 1 << (ibw - 1)
for byte in inbytes:
inbit = 0
while iskip:
byte = (byte << 1) & 0xFF
inbit += 1
iskip -= 1
for i in range(0, max(obw, ibw - inbit)):
if inbit == ibw:
# Move to next char
inbit = 0
break
tmp = (tmp << 1) | ((byte & stripmask) and 1 or 0)
byte = (byte << 1) & 0xFF
inbit += 1
outbit += 1
if outbit == obw:
yield tmp
tmp = 0
outbit = 0
chirp-20261030/chirp/drivers/icf520.py 0000644 0001750 0001750 00000151037 15047574664 016210 0 ustar hibby hibby """
Icom IC-F621 Land Mobile Radio
"""
# Copyright 2023 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 logging
from chirp import chirp_common
from chirp import directory
from chirp import bitwise
from chirp.drivers import icf
from chirp.settings import RadioSetting, RadioSettingGroup, \
RadioSettingValueInteger, RadioSettingValueList, \
RadioSettingValueString, RadioSettingValueFloat
LOG = logging.getLogger(__name__)
MEM_FORMAT = """
u8 model_code[4];
u8 unknown100[269];
u8 beep_on:1,
beep_level_linked:1,
beep_level:6;
u8 squelch_level;
u8 volume_min_level;
u8 unknown101:5,
mic_gain:3;
u8 unknown102[9];
u8 unknown103:3,
ignition_switch_on:1,
audio_filter:2,
backlight:2;
u8 horn_off:1,
unknown104:7;
u8 unknown105[16];
struct {
u16 user_1:4,
user_2:4,
user_3:4,
user_4:4;
u24 dealer_1:4,
dealer_2:4,
dealer_3:4,
dealer_4:4,
dealer_5:4,
dealer_6:4;
} passwd;
u8 scrambler_syn_capture:2,
scrambler_tone_start_timing:2,
scrambler_non_rolling:1,
unknown106:1,
scrambler_group_code:2;
u8 unknown107[4];
u16 beep_frequency_low;
u16 beep_frequency_high;
u16 unknown107a;
u8 backlight_set_enable:1,
beep_set_enable:1,
beep_level_set_enable:1,
squelch_level_set_enable:1,
volume_min_level_set_enable:1,
audio_filter_set_enable:1,
mic_gain_set_enable:1,
horn_set_enable:1;
u8 unknown108[5];
bit trunk_on[16];
struct {
u8 emergency;
u8 priority_a;
u8 priority_b;
} channel;
struct {
u8 unk1;
u8 unk2;
u8 unk3;
u8 unk4;
u8 unk5;
u8 unk6;
} unknown109;
u8 tx_dtcs_inverse:1,
rx_dtcs_inverse:1,
unknown110:6;
u8 unknown111:2,
emergency_channel_on:1,
unknown112:1,
tone_burst_phase_on:1,
rx_af_filter_low_cut_on:1,
unknown113:2;
u8 poweron_scan_on:1,
unknown114:1,
dtmf_id_out_on:1,
time_out_timer_beep_on:1,
unknown115:1,
data_out_on:1,
poweron_priority_a:1,
unknown116:1;
u8 unknown117:2,
opening_text_on:1,
poweron_selection_off:1,
unknown118:4;
u8 unknown119:2,
password_on:1,
unknown120:2,
rf_power_selection:1,
unknown121:2;
u8 unknown122;
u8 hookoff_monitor_on:1,
hookoff_priority_a:1,
hookon_scan_on:1,
unknown123:1,
exo_on:1,
tone_mute_eptt_on:1,
unknown124:2;
struct {
u8 scan_fast;
u8 scan_slow;
u8 ctcss_reverse_burst;
u8 unk1;
u8 unk2;
u8 unk3;
u8 unk4;
u8 two_tone_1_period;
u8 unk5;
u8 dtmf;
u8 dtmf_first;
u8 dtmf_starpound;
u8 two_tone_group;
u8 scan_stop;
u8 scan_resume;
u8 unk6;
u8 unk7;
u8 emergency_switch_on;
u8 emergency_switch_off;
u8 emergency_start_repeat;
u8 time_out;
u8 penalty;
u8 lockout_penalty;
u8 two_tone_beep_repeat;
u8 auto_reset_a;
u8 auto_reset_b;
u8 unk8;
u8 two_tone_auto_tx;
u8 horn;
u8 exo_delay;
u8 eptt_delay;
u8 ignition_switch_delay:4,
unk9:4;
} timer;
struct {
u8 lup;
u8 ldown;
u8 rup;
u8 rdown;
u8 p0;
u8 p1;
u8 p2;
u8 p3;
u8 p4;
u8 opf0;
u8 opf1;
u8 opf2;
} conv_key;
struct {
u8 lup;
u8 ldown;
u8 rup;
u8 rdown;
u8 p0;
u8 p1;
u8 p2;
u8 p3;
u8 p4;
u8 opf0;
u8 opf1;
u8 opf2;
} trunk_key;
struct {
u16 capacity;
} bank[16];
struct {
u16 unk;
} unknown125[16];
char clone_comment[32];
struct {
u16 map;
} custom_characters[16];
u16 two_tone_1_tone;
u16 two_tone_2_tone;
struct {
u16 unk;
} unknown126[40];
u8 two_tone_2_period;
struct {
u8 unk;
} unknown127[14];
u8 two_tone_timer_notone;
struct {
u8 unk;
} unknown128[22];
u16 user_ctcss_freq;
u32 unknown129;
struct {
u16 rx;
u16 tx;
} continuous_tone[9];
struct {
char name[10];
bit active[256];
u8 text_on:1,
unk1:5,
scan_mode:2;
u8 unk2;
} scan_list[10];
struct {
char name[10];
u8 code[12];
u8 count;
} dtmf_autodial[10];
struct {
char name[10];
u32 disp_inhibit:1,
freq_rx:31;
u32 tx_inhibit:1,
freq_tx:31;
u16 rx_tone_off:1,
rx_tone_digital:1,
unk01:5,
rx_tone:9;
u16 tx_tone_off:1,
tx_tone_digital:1,
unk02:5,
tx_tone:9;
u8 unk03:3,
tot_on:1,
lockout_repeater:1,
lockout_busy:1,
power_rf:2;
u8 log_in:2,
log_out:2,
unk04:1,
text_on:1,
unk05:1,
two_tone_unk1:1;
u8 unk06:4,
two_tone_unk2:2,
auto_reset:1,
unk07:1;
u8 narrow:1,
scrambler_on:1,
scrambler_inhibit:1,
compander_on:1,
unk08:4;
u8 unk09;
u8 scrambler_code;
u16 unk10;
u16 unk11;
u8 unk12:6,
two_tone_index:2;
} memory[256];
struct {
char name[10];
u16 unk1;
u16 unk2;
u16 unk3;
} unknown130[32];
u32 unknown131;
struct {
char name[10];
u8 unk1;
u8 ans_on;
u8 unk2:2,
scan_start:1,
scan_cancel:1,
unk3:1,
beep:3;
u8 exo_on:1,
unk4:1,
auto_tx_on:1,
stun_on:1,
kill_on:1,
unk5:1,
bell:2;
u8 unk6;
u16 unk7;
u16 unk8;
} two_tone[9];
u8 unknown132[4];
char opening_text[10];
"""
APPEAR_CHOICE = ["Appear", "Inhibit"]
ENABLE_CHOICE = ["Disable", "Enable"]
INVERSE_CHOICE = ["Normal", "Inverse"]
NO_YES_CHOICE = ["No", "Yes"]
ON_CHOICE = ["", "On"] # ["OFF", "ON"]
ON_OFF_CHOICE = ["Off", "On"]
YES_CHOICE = ["", "Yes"]
MODES = ["FM", "NFM"] # Supported emission (or receive) modes
TMODES = ["", "Tone", "TSQL", "DTCS", "Cross", ]
CROSS_MODES = [
"Tone->Tone",
"Tone->DTCS",
"DTCS->Tone",
"DTCS->",
"->Tone",
"->DTCS",
"DTCS->DTCS",
]
TONES = [
67.0, 69.3, 71.0, 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,
]
DUPLEX = ["", "+", "-", "split", "off"]
# Valid values for programmable keys.
# Hex numbers are used for unknown functions.
KEY_FUNC = [
"Null",
"CH Up",
"CH Down",
"Bank",
"Scan A Start/Stop",
"Scan B Start/Stop",
"Scan Add/Del(Tag)",
"Prio A",
"Prio A(Rewrite)",
"Prio B",
"MR-CH 1",
"MR-CH 2",
"MR-CH 3",
"MR-CH 4",
"Moni",
"Lock",
"High/Low",
"Wide/Narrow",
"Compander",
"C.Tone CH Ent",
"Talk Around",
"Scrambler",
"Re-dial",
"DTMF Autodial",
"Call",
"0x19",
"0x1A",
"Emergency Single",
"Emergency Repeat",
"0x1D",
"0x1E",
"0x1F",
"0x20",
"User Set Mode",
"Public Address",
"RX Speaker",
"0x24",
"0x25",
"0x26",
"0x27",
"Turbo SpeeDial A",
"Turbo SpeeDial B",
"Turbo SpeeDial C",
"Turbo SpeeDial D",
"Trunking Group Switch",
"Sp. Func 1",
"Sp. Func 2",
"Hook Scan",
"OPT1 Out",
"OPT2 Out",
"OPT3 Out",
"OPT1 Momentary",
"OPT2 Momentary",
"OPT3 Momentary",
]
# List of valid conventional keys
# in the order they should be presented to the user.
KEY_CONV = [
"Null",
"CH Up",
"CH Down",
"Bank",
"Scan A Start/Stop",
"Scan B Start/Stop",
"Scan Add/Del(Tag)",
"Prio A",
"Prio A(Rewrite)",
"Prio B",
"MR-CH 1",
"MR-CH 2",
"MR-CH 3",
"MR-CH 4",
"Moni",
"Public Address",
"RX Speaker",
"Lock",
"High/Low",
"C.Tone CH Ent",
"Talk Around",
"Wide/Narrow",
"DTMF Autodial",
"Re-dial",
"Call",
"Emergency Single",
"Emergency Repeat",
"Scrambler",
"Compander",
"Hook Scan",
"User Set Mode",
"OPT1 Out",
"OPT2 Out",
"OPT3 Out",
"OPT1 Momentary",
"OPT2 Momentary",
"OPT3 Momentary",
"Sp. Func 1",
"Sp. Func 2",
]
# List of valid trunk keys
# in the order they should be presented to the user.
KEY_TRUNK = [
"Null",
"Bank",
"Lock",
"High/Low",
"Trunking Group Switch",
"Turbo SpeeDial A",
"Turbo SpeeDial B",
"Turbo SpeeDial C",
"Turbo SpeeDial D",
"CH Up",
"CH Down",
"Compander",
"Sp. Func 1",
"Sp. Func 2",
]
AUDIO_FILTER_RX_OPTIONS = [
"Normal",
"Low Cut",
]
RF_POWER_SELECTION_OPTIONS = [
"MR CH Individual",
"Override",
]
BACKLIGHT_VALUES = [
"Off",
"Dim",
"Auto",
"On",
]
BEEP_LEVEL_VALUES = [
"",
"1",
"2",
"3",
"4",
"5",
]
AUDIO_FILTER_VALUES = [
"300-3000 Hz",
"0-3000 Hz",
"300-3400 Hz",
"0-3400 Hz",
]
# Valid values for Mic Gain.
# Hex numbers are used for unknown functions.
MIC_GAIN_FUNC = [
"0x00",
"1 Min",
"2",
"3",
"4",
"5 Max",
]
# List of valid Mic Gain choices
# in the order they should be presented to the user.
MIC_GAIN_CHOICE = [
"1 Min",
"2",
"3",
"4",
"5 Max",
]
LOCKOUT_VALUES = [ # CS-F500 equivalent
"", # OFF
"Repeater", # Repeater Lockout
"Busy", # Busy Lockout
]
LOG_IN_OUT_VALUES = [ # CS-F500 equivalent
"", # OFF
"L-IN", # Log IN
"L-OFF", # Log OFF
"Both", # Log IN/OFF
]
RESET_TIMERS_VALUES = [
"Timer B",
"Timer A",
]
SCRAMBLER_TYPE_VALUES = [
"Rolling",
"Non-rolling",
]
SCRAMBLER_GCODE_VALUES = [
"1",
"2",
"3",
"4",
]
# Valid values for Scrambler Synchronous Capture.
# Hex numbers are used for unknown functions.
SYN_CAPTURE_FUNC = [
"Standard",
"0x01",
"0x02",
"Continuous",
]
# List of valid Scrambler Synchronous Capture choices
# in the order they should be presented to the user.
SYN_CAPTURE_CHOICE = [
"Standard",
"Continuous",
]
TONE_BURST_VALUES = [
"Notone",
"Phase",
]
TONE_STIMING_VALUES = [
"Off",
"300 ms",
"600 ms",
"1100 ms",
]
def kf_kc(i):
"""
Given the index of the conventional key choice
return the index of the corresponding function
"""
return int(KEY_FUNC.index(KEY_CONV[int(i)]))
def kc_kf(i):
"""
Given the index of the key function
return the index of the corresponding conventional key choice
"""
return int(KEY_CONV.index(KEY_FUNC[int(i)]))
def kf_kt(i):
"""
Given the index of the trunk key choice
return the index of the corresponding function
"""
return int(KEY_FUNC.index(KEY_TRUNK[int(i)]))
def kt_kf(i):
"""
Given the index of the key function
return the index of the corresponding trunk key choice
"""
return int(KEY_TRUNK.index(KEY_FUNC[int(i)]))
def mf_mc(i):
"""
Given the index of the mic gain choice
return the index of the corresponding function
"""
return int(MIC_GAIN_FUNC.index(MIC_GAIN_CHOICE[int(i)]))
def mc_mf(i):
"""
Given the index of the mic gain function
return the index of the corresponding choice
"""
return int(MIC_GAIN_CHOICE.index(MIC_GAIN_FUNC[int(i)]))
def sf_sc(i):
"""
Given the index of the sync capture choice
return the index of the corresponding function
"""
return int(SYN_CAPTURE_FUNC.index(SYN_CAPTURE_CHOICE[int(i)]))
def sc_sf(i):
"""
Given the index of the sync capture function
return the index of the corresponding choice
"""
return int(SYN_CAPTURE_CHOICE.index(SYN_CAPTURE_FUNC[int(i)]))
@directory.register
class ICF621_2Radio(icf.IcomCloneModeRadio):
"""
Icom IC-F621-2 UHF Land Mobile Radio
"""
VENDOR = "Icom"
MODEL = "IC-F621-2"
# _power_high watts is taken from the brochure.
# FIXME: The others are guesses
_power_low1 = chirp_common.PowerLevel("Low1", watts=5)
_power_low2 = chirp_common.PowerLevel("Low2", watts=20)
_power_high = chirp_common.PowerLevel("High", watts=45)
_model = "\x25\x26\x02\x00" # 4 byte model (don't use Byte String)
_memsize = 0x3000
_endframe = "Icom Inc." # \x49\x63\x6F\x6D\x20\x49\x6E\x63\x2E
# Ranges of the mmap to send to the radio
_ranges = [(0x0100, 0x3000, 16)]
_num_banks = 16
_can_hispeed = False
# Different models and versions support subsets of the valid bands.
# Frequency values out of range for a radio will not cause a
# transfer error. The radio display will flash when that channel
# is selected.
# IC-F521 _valid_bands = [(136000000, 174000000)]
_valid_bands = [(440000000, 490000000)]
_memories = {}
def get_features(self):
rf = chirp_common.RadioFeatures()
rf.has_settings = True
# Channels
rf.has_bank = False
rf.has_bank_names = False
rf.memory_bounds = (1, 256)
# Channel Name
rf.has_name = True
rf.valid_characters = chirp_common.CHARSET_ASCII
rf.valid_name_length = 10
# Frequency
rf.valid_modes = list(MODES)
rf.has_offset = True
rf.has_tuning_step = False
rf.valid_tuning_steps = chirp_common.TUNING_STEPS
rf.valid_duplexes = list(DUPLEX)
rf.valid_bands = list(self._valid_bands)
rf.can_odd_split = True
rf.has_mode = True
# Tone
rf.has_ctone = True
rf.has_dtcs = True
rf.has_rx_dtcs = True
rf.has_dtcs_polarity = False
rf.has_cross = True
rf.valid_cross_modes = list(CROSS_MODES)
rf.valid_tmodes = list(TMODES)
rf.valid_tones = list(TONES)
# RF Power
rf.has_variable_power = False
rf.valid_power_levels = [
self._power_high, self._power_low2, self._power_low1]
# Scanning
rf.valid_skips = []
return rf
def process_mmap(self):
"""Convert the raw byte array into a memory object structure"""
self._memobj = bitwise.parse(MEM_FORMAT, self._mmap)
# FIXME: BEGIN Ugly Hack
# Force driver to use one bank that contains all memories.
# This allows us to work on other parts of the driver before
# the bank model is complete.
self._memobj.bank[0].capacity = 256
for i in range(1, self._num_banks):
self._memobj.bank[i].capacity = 0
# When deleting this ugly hack, remember to change:
# rf.has_bank = True
# below.
# FIXME: END Ugly Hack
def get_memory(self, number):
"""Create a high-level memory object to return to the UI"""
_mem = self._memobj.memory[number - 1]
memory = chirp_common.Memory()
memory.number = number
memory.immutable = []
# Frequency: Low level format is frequency in Hz
memory.freq = int(_mem.freq_rx)
if _mem.tx_inhibit:
memory.duplex = "off"
memory.offset = 0
elif int(_mem.freq_rx) == int(_mem.freq_tx):
memory.duplex = ""
memory.offset = 0
else:
chirp_common.split_to_offset(
memory, int(_mem.freq_rx), int(_mem.freq_tx))
# Modes
if _mem.narrow:
memory.mode = "NFM"
else:
memory.mode = "FM"
# Name
memory.name = str(_mem.name).rstrip()
# Tone
txpol = None
if _mem.tx_tone_off:
txmode = None
txval = None
elif _mem.tx_tone_digital:
txmode = "DTCS"
txval = chirp_common.ALL_DTCS_CODES[int(_mem.tx_tone)]
else:
txmode = "Tone"
txval = TONES[_mem.tx_tone]
rxpol = None
if _mem.rx_tone_off:
rxmode = None
rxval = None
elif _mem.rx_tone_digital:
rxmode = "DTCS"
rxval = chirp_common.ALL_DTCS_CODES[int(_mem.rx_tone)]
else:
rxmode = "Tone"
rxval = TONES[_mem.rx_tone]
txspec = (txmode, txval, txpol)
rxspec = (rxmode, rxval, rxpol)
chirp_common.split_tone_decode(memory, txspec, rxspec)
# RF Power
if _mem.power_rf == 3:
memory.power = self._power_high
elif _mem.power_rf == 2:
memory.power = self._power_low2
else:
memory.power = self._power_low1
memory.extra = RadioSettingGroup('extra', 'Extra')
# Mask (Don't display) channel on radio.
# This is referred as Inhibit in the CS-F500 software.
_disp_inhibit = RadioSetting(
"disp_inhibit", "Mask",
RadioSettingValueList(
YES_CHOICE, current_index=int(_mem.disp_inhibit)))
_disp_inhibit.set_doc(
"Prevent the radio from displaying the channel. This "
"prevents use of the channel but CHIRP will still download "
"the channel information.")
memory.extra.append(_disp_inhibit)
_compander_on = RadioSetting(
"compander_on", "Compander",
RadioSettingValueList(
ON_CHOICE, current_index=int(
_mem.compander_on)))
_compander_on.set_doc("Compander On")
memory.extra.append(_compander_on)
_tot_on = RadioSetting(
"tot_on", "TOT",
RadioSettingValueList(
ON_CHOICE, current_index=int(_mem.tot_on)))
_tot_on.set_doc("Time Out Timer On")
memory.extra.append(_tot_on)
_auto_reset_timer = RadioSetting(
"auto_reset_timer", "Auto Reset Timer",
RadioSettingValueList(
RESET_TIMERS_VALUES, current_index=int(
_mem.auto_reset)))
_auto_reset_timer.set_doc("Auto Reset Timer Selection")
memory.extra.append(_auto_reset_timer)
_lockout_bits = (_mem.lockout_busy << 1) | _mem.lockout_repeater
_lockout = RadioSetting(
"lockout", "Lockout",
RadioSettingValueList(
LOCKOUT_VALUES, current_index=int(_lockout_bits)))
_lockout.set_doc("Transmit Lockout")
memory.extra.append(_lockout)
_log_in_out_bits = (_mem.log_in << 2) + _mem.log_out
_log_in_out_index = 0
if _log_in_out_bits == 0x03:
_log_in_out_index = 2
elif _log_in_out_bits == 0x0C:
_log_in_out_index = 1
elif _log_in_out_bits == 0x0F:
_log_in_out_index = 3
_log_in_out = RadioSetting(
"log_in_out", "Log",
RadioSettingValueList(
LOG_IN_OUT_VALUES, current_index=int(_log_in_out_index)))
_log_in_out.set_doc("Log in and Log out")
memory.extra.append(_log_in_out)
# Any memory with a frequency of 0 MHz is treated as empty.
#
# When CS-F500 inserts a blank memory, it sets Tx and Rx frequency
# to 0 MHz and leaves the disp_inhibit bit unset. When the
# disp_inhibit bit is set, the memory is displayed in CS-F500
# with an "i" in the inhibit column.
#
# The radio does not display inhibited (masked) memory
# channels.
if memory.freq == 0:
memory.empty = True
return memory
def set_memory(self, memory):
"""Set the memory object @memory"""
# Store details about a high-level memory to the memory map
# This is called when a user edits a memory in the UI
# Get a low-level memory object mapped to the image
_mem = self._memobj.memory[memory.number - 1]
rf = self.get_features()
# Frequency: Low level format is frequency in Hz
_mem.freq_rx = memory.freq
_mem.tx_inhibit = 0
if memory.duplex == "off":
_mem.freq_tx = memory.freq
_mem.tx_inhibit = 1
elif memory.duplex == "split":
_mem.freq_tx = memory.offset
elif memory.duplex == "-":
_mem.freq_tx = _mem.freq_rx - memory.offset
elif memory.duplex == "+":
_mem.freq_tx = _mem.freq_rx + memory.offset
else:
_mem.freq_tx = memory.freq
# Modes
if memory.mode == "NFM":
_mem.narrow = 1
else:
_mem.narrow = 0
# Name (alpha tag)
_mem.name = str(memory.name).ljust(rf.valid_name_length)[
:rf.valid_name_length]
_mem.text_on = 1
# Tone
txspec, rxspec = chirp_common.split_tone_encode(memory)
txmode, txval, txpol = txspec
rxmode, rxval, rxpol = rxspec
_mem.tx_tone_off = 1
_mem.tx_tone_digital = 0
_mem.tx_tone = 0
if txmode == "Tone":
_mem.tx_tone_off = 0
_mem.tx_tone = TONES.index(txval)
elif txmode == "DTCS":
_mem.tx_tone_off = 0
_mem.tx_tone_digital = 1
_mem.tx_tone = chirp_common.ALL_DTCS_CODES.index(txval)
_mem.rx_tone_off = 1
_mem.rx_tone_digital = 0
_mem.rx_tone = 0
if rxmode == "Tone":
_mem.rx_tone_off = 0
_mem.rx_tone = TONES.index(rxval)
elif rxmode == "DTCS":
_mem.rx_tone_off = 0
_mem.rx_tone_digital = 1
_mem.rx_tone = chirp_common.ALL_DTCS_CODES.index(rxval)
# RF Power
if memory.power == self._power_high:
_mem.power_rf = 3
elif memory.power == self._power_low2:
_mem.power_rf = 2
else:
_mem.power_rf = 1
_mem.disp_inhibit = 0
_mem.compander_on = 0
_mem.tot_on = 0
_mem.auto_reset = 0
_mem.lockout_busy = 0
_mem.lockout_repeater = 0
_mem.log_in = 0
_mem.log_out = 0
if memory.extra:
_mem.disp_inhibit = memory.extra['disp_inhibit'].value
_mem.compander_on = memory.extra['compander_on'].value
_mem.tot_on = memory.extra['tot_on'].value
_mem.auto_reset = memory.extra['auto_reset_timer'].value
_lockout = int(memory.extra['lockout'].value)
_mem.lockout_repeater = _lockout & 1
_mem.lockout_busy = (_lockout >> 1) & 1
# There are two bits each for log in and log out/off.
# 0 is off and 3 is on. 1 and 2 remain a mystery
_log_in_out = int(memory.extra['log_in_out'].value)
_mem.log_in = 0
_mem.log_out = 0
if _log_in_out == 1:
_mem.log_in = 3
elif _log_in_out == 2:
_mem.log_out = 3
elif _log_in_out == 3:
_mem.log_in = 3
_mem.log_out = 3
if memory.empty:
_mem.freq_rx = 0
_mem.freq_tx = 0
_mem.narrow = 0
_mem.name = " "
_mem.text_on = 0
_mem.tx_tone_off = 1
_mem.tx_tone_digital = 0
_mem.tx_tone = 0
_mem.rx_tone_off = 1
_mem.rx_tone_digital = 0
_mem.rx_tone = 0
# Radio Settings
# Build the "DTMF" menu.
# TODO: This menu is not even started.
# Build the "Continuous Tone" menu.
# TODO: This menu is not complete.
def _get_continuous_tone_group(self):
_group = RadioSettingGroup(
"group_continuous_tone", "Continuous Tone")
# 9 custom CTCSS pairs
_group.append(RadioSetting(
"tone_burst_phase_on", "Tone Burst",
RadioSettingValueList(
TONE_BURST_VALUES, current_index=int(
self._memobj.tone_burst_phase_on))))
# "CTCSS Reverse Burst Timer"
# "User CTCSS Freq (Hz)"
_group.append(RadioSetting(
"tx_dtcs_inverse", "TX DTCS Inverse",
RadioSettingValueList(
INVERSE_CHOICE, current_index=int(
self._memobj.tx_dtcs_inverse))))
_group.append(RadioSetting(
"rx_dtcs_inverse", "RX DTCS Inverse",
RadioSettingValueList(
INVERSE_CHOICE, current_index=int(
self._memobj.rx_dtcs_inverse))))
_group.append(RadioSetting(
"rx_af_filter_low_cut_on", "RX AF Filter",
RadioSettingValueList(
AUDIO_FILTER_RX_OPTIONS, current_index=int(
self._memobj.rx_af_filter_low_cut_on))))
return _group
# Build the "Scan" menu.
# TODO: This menu is barely started
def _get_scan_group(self):
_group = RadioSettingGroup("group_scan", "Scan")
# 10 scan lists
# timer.scan_stop "Stop"
# timer.scan_resume "Resume"
# timer.scan_fast "Fast Scan
# timer.scan_slow "Slow Scan"
_group.append(RadioSetting(
"poweron_scan_on", "Power on Scan",
RadioSettingValueList(
ON_OFF_CHOICE, current_index=int(
self._memobj.poweron_scan_on))))
return _group
# Build the "2TONE" menu
# TODO: This menu is not even started.
# Build the "Conventional Key" menu.
def _get_conv_key_group(self):
_group = RadioSettingGroup("conv_keys", "Conventional Keys")
_group.append(RadioSetting(
"conv_key_lup", "L-Up",
RadioSettingValueList(
KEY_CONV, current_index=kc_kf(
self._memobj.conv_key.lup))))
_group.append(RadioSetting(
"conv_key_ldown", "L-Down",
RadioSettingValueList(
KEY_CONV, current_index=kc_kf(
self._memobj.conv_key.ldown))))
_group.append(RadioSetting(
"conv_key_rup", "R-Up",
RadioSettingValueList(
KEY_CONV, current_index=kc_kf(
self._memobj.conv_key.rup))))
_group.append(RadioSetting(
"conv_key_rdown", "R-Down",
RadioSettingValueList(
KEY_CONV, current_index=kc_kf(
self._memobj.conv_key.rdown))))
_group.append(RadioSetting(
"conv_key_p0", "P0",
RadioSettingValueList(
KEY_CONV, current_index=kc_kf(
self._memobj.conv_key.p0))))
_group.append(RadioSetting(
"conv_key_p1", "P1",
RadioSettingValueList(
KEY_CONV, current_index=kc_kf(
self._memobj.conv_key.p1))))
_group.append(RadioSetting(
"conv_key_p2", "P2",
RadioSettingValueList(
KEY_CONV, current_index=kc_kf(
self._memobj.conv_key.p2))))
_group.append(RadioSetting(
"conv_key_p3", "P3",
RadioSettingValueList(
KEY_CONV, current_index=kc_kf(
self._memobj.conv_key.p3))))
_group.append(RadioSetting(
"conv_key_p4", "P4",
RadioSettingValueList(
KEY_CONV, current_index=kc_kf(
self._memobj.conv_key.p4))))
_group.append(RadioSetting(
"conv_key_opf0", "OPF0",
RadioSettingValueList(
KEY_CONV, current_index=kc_kf(
self._memobj.conv_key.opf0))))
_group.append(RadioSetting(
"conv_key_opf1", "OPF1",
RadioSettingValueList(
KEY_CONV, current_index=kc_kf(
self._memobj.conv_key.opf1))))
_group.append(RadioSetting(
"conv_key_opf2", "OPF2",
RadioSettingValueList(
KEY_CONV, current_index=kc_kf(
self._memobj.conv_key.opf2))))
return _group
# Build the "SmarTrunk Key" menu.
def _get_trunk_key_group(self):
_group = RadioSettingGroup("trunk_keys", "SmarTrunk Keys")
_group.append(RadioSetting(
"trunk_key_lup", "L-Up",
RadioSettingValueList(
KEY_TRUNK, current_index=kt_kf(
self._memobj.trunk_key.lup))))
_group.append(RadioSetting(
"trunk_key_ldown", "L-Down",
RadioSettingValueList(
KEY_TRUNK, current_index=kt_kf(
self._memobj.trunk_key.ldown))))
_group.append(RadioSetting(
"trunk_key_rup", "R-Up",
RadioSettingValueList(
KEY_TRUNK, current_index=kt_kf(
self._memobj.trunk_key.rup))))
_group.append(RadioSetting(
"trunk_key_rdown", "R-Down",
RadioSettingValueList(
KEY_TRUNK, current_index=kt_kf(
self._memobj.trunk_key.rdown))))
_group.append(RadioSetting(
"trunk_key_p0", "P0",
RadioSettingValueList(
KEY_TRUNK, current_index=kt_kf(
self._memobj.trunk_key.p0))))
_group.append(RadioSetting(
"trunk_key_p1", "P1",
RadioSettingValueList(
KEY_TRUNK, current_index=kt_kf(
self._memobj.trunk_key.p1))))
_group.append(RadioSetting(
"trunk_key_p2", "P2",
RadioSettingValueList(
KEY_TRUNK, current_index=kt_kf(
self._memobj.trunk_key.p2))))
_group.append(RadioSetting(
"trunk_key_p3", "P3",
RadioSettingValueList(
KEY_TRUNK, current_index=kt_kf(
self._memobj.trunk_key.p3))))
_group.append(RadioSetting(
"trunk_key_p4", "P4",
RadioSettingValueList(
KEY_TRUNK, current_index=kt_kf(
self._memobj.trunk_key.p4))))
_group.append(RadioSetting(
"trunk_key_opf0", "OPF0",
RadioSettingValueList(
KEY_TRUNK, current_index=kt_kf(
self._memobj.trunk_key.opf0))))
_group.append(RadioSetting(
"trunk_key_opf1", "OPF1",
RadioSettingValueList(
KEY_TRUNK, current_index=kt_kf(
self._memobj.trunk_key.opf1))))
_group.append(RadioSetting(
"trunk_key_opf2", "OPF2",
RadioSettingValueList(
KEY_TRUNK, current_index=kt_kf(
self._memobj.trunk_key.opf2))))
return _group
# Build the "Display" menu.
def _get_display_group(self):
_freq_lo = self._memobj.beep_frequency_low / 10.0
_freq_hi = self._memobj.beep_frequency_high / 10.0
_psw = int(not self._memobj.poweron_selection_off)
_isw = int(self._memobj.ignition_switch_on)
_pria = int(self._memobj.poweron_priority_a)
_group = RadioSettingGroup("group_display", "Display")
_group.append(RadioSetting(
"beep_frequency_low", "Beep Low Freq",
RadioSettingValueFloat(
400.0, 3000.0, _freq_lo, precision=0)))
_group.append(RadioSetting(
"beep_frequency_high", "Beep High Freq",
RadioSettingValueFloat(
400.0, 3000.0, _freq_hi, precision=0)))
_group.append(RadioSetting(
"opening_text", "Opening Text",
RadioSettingValueString(
0, 10, str(self._memobj.opening_text).rstrip(), False)))
_group.append(RadioSetting(
"rf_power_selection", "RF Power",
RadioSettingValueList(
RF_POWER_SELECTION_OPTIONS, current_index=int(
self._memobj.rf_power_selection))))
_group.append(RadioSetting(
"poweron_selection_off", "Power Switch Selection",
RadioSettingValueList(APPEAR_CHOICE, current_index=_psw)))
_group.append(RadioSetting(
"ignition_switch_on", "Ignition Switch",
RadioSettingValueList(APPEAR_CHOICE, current_index=_isw)))
_group.append(RadioSetting(
"ignition_switch_delay", "Ignition Switch Delay Timer",
RadioSettingValueInteger(
0, 240, self._memobj.timer.ignition_switch_delay)))
_group.append(RadioSetting(
"poweron_priority_a", "Power On Priority A Channel",
RadioSettingValueList(ENABLE_CHOICE, current_index=_pria)))
_group.append(RadioSetting(
"hookoff_priority_a", "Hook Off Priority A Channel",
RadioSettingValueList(
ENABLE_CHOICE, current_index=int(
self._memobj.hookoff_priority_a))))
_group.append(RadioSetting(
"hookoff_monitor_on", "Hook Off Monitor",
RadioSettingValueList(
ON_OFF_CHOICE, current_index=int(
self._memobj.hookoff_monitor_on))))
_group.append(RadioSetting(
"hookon_scan_on", "Hook on Scan",
RadioSettingValueList(
ON_OFF_CHOICE, current_index=int(
self._memobj.hookon_scan_on))))
return _group
# Build the "Set Mode" menu.
def _get_set_mode_group(self):
_hrn = int(not self._memobj.horn_off)
_group = RadioSettingGroup("set_mode_group", "Set Mode")
_group.append(RadioSetting(
"backlight", "Backlight",
RadioSettingValueList(
BACKLIGHT_VALUES, current_index=int(
self._memobj.backlight))))
_group.append(RadioSetting(
"backlight_set_enable", "Enable Backlight",
RadioSettingValueList(
NO_YES_CHOICE, current_index=int(
self._memobj.backlight_set_enable))))
_group.append(RadioSetting(
"beep_on", "Beep",
RadioSettingValueList(
ON_OFF_CHOICE, current_index=int(
self._memobj.beep_on))))
_group.append(RadioSetting(
"beep_set_enable", "Enable Beep",
RadioSettingValueList(
NO_YES_CHOICE, current_index=int(
self._memobj.beep_set_enable))))
_group.append(RadioSetting(
"beep_level", "Beep Level",
RadioSettingValueList(
BEEP_LEVEL_VALUES, current_index=int(
self._memobj.beep_level))))
_group.append(RadioSetting(
"beep_level_set_enable", "Enable Beep Level",
RadioSettingValueList(
NO_YES_CHOICE, current_index=int(
self._memobj.beep_level_set_enable))))
_group.append(RadioSetting(
"squelch_level", "Squelch Level",
RadioSettingValueInteger(
0, 255, self._memobj.squelch_level)))
_group.append(RadioSetting(
"squelch_level_set_enable", "Enable Squelch Level",
RadioSettingValueList(
NO_YES_CHOICE, current_index=int(
self._memobj.squelch_level_set_enable))))
_group.append(RadioSetting(
"volume_min_level", "Volume Minimum Level",
RadioSettingValueInteger(
0, 255, self._memobj.volume_min_level)))
_group.append(RadioSetting(
"volume_min_level_set_enable", "Enable Volume Minimum Level",
RadioSettingValueList(
NO_YES_CHOICE, current_index=int(
self._memobj.volume_min_level_set_enable))))
_group.append(RadioSetting(
"audio_filter", "Audio Filter",
RadioSettingValueList(
AUDIO_FILTER_VALUES, current_index=int(
self._memobj.audio_filter))))
_group.append(RadioSetting(
"audio_filter_set_enable", "Enable Audio Filter",
RadioSettingValueList(
NO_YES_CHOICE, current_index=int(
self._memobj.audio_filter_set_enable))))
_group.append(RadioSetting(
"mic_gain", "Mic Gain",
RadioSettingValueList(
MIC_GAIN_CHOICE, current_index=mc_mf(
self._memobj.mic_gain))))
_group.append(RadioSetting(
"mic_gain_set_enable", "Enable Mic Gain",
RadioSettingValueList(
NO_YES_CHOICE, current_index=int(
self._memobj.mic_gain_set_enable))))
_group.append(RadioSetting(
"horn_off", "Horn",
RadioSettingValueList(
ON_CHOICE, current_index=_hrn)))
_group.append(RadioSetting(
"horn_set_enable", "Enable Horn",
RadioSettingValueList(
NO_YES_CHOICE, current_index=int(
self._memobj.horn_set_enable))))
return _group
# Build the "Common" menu.
def _get_common_group(self):
_timer_eptt = self._memobj.timer.eptt_delay / 10.0
_timer_exo = self._memobj.timer.exo_delay / 10.0
_timer_es_on = self._memobj.timer.emergency_switch_on / 10.0
_timer_es_off = self._memobj.timer.emergency_switch_off / 10.0
_group = RadioSettingGroup("common_group", "Common")
_group.append(RadioSetting(
"clone_comment", "Clone Comment",
RadioSettingValueString(
0, 32, str(self._memobj.clone_comment).rstrip(), False)))
_group.append(RadioSetting(
"password_on", "Power-on Password",
RadioSettingValueList(
ON_OFF_CHOICE, current_index=int(
self._memobj.password_on))))
_group.append(RadioSetting(
"data_out_on", "Transceiver Data Out",
RadioSettingValueList(
ON_OFF_CHOICE, current_index=int(
self._memobj.data_out_on))))
_group.append(RadioSetting(
"timer_auto_reset_a", "Auto Reset Timer A",
RadioSettingValueInteger(
0, 255, self._memobj.timer.auto_reset_a)))
_group.append(RadioSetting(
"timer_auto_reset_b", "Auto Reset Timer B",
RadioSettingValueInteger(
0, 255, self._memobj.timer.auto_reset_b)))
_group.append(RadioSetting(
"timer_time_out", "Time Out Timer",
RadioSettingValueInteger(
0, 255, self._memobj.timer.time_out)))
_group.append(RadioSetting(
"timer_penalty", "Penalty Timer",
RadioSettingValueInteger(
0, 255, self._memobj.timer.penalty)))
_group.append(RadioSetting(
"dtmf_id_out_on", "DTMF ID Out",
RadioSettingValueList(
ON_OFF_CHOICE, current_index=int(
self._memobj.dtmf_id_out_on))))
_group.append(RadioSetting(
"time_out_timer_beep_on", "Time Out Timer Beep",
RadioSettingValueList(
ON_OFF_CHOICE, current_index=int(
self._memobj.time_out_timer_beep_on))))
_group.append(RadioSetting(
"timer_lockout_penalty", "Lockout Penalty Timer",
RadioSettingValueInteger(
0, 255, self._memobj.timer.lockout_penalty)))
_group.append(RadioSetting(
"timer_eptt_delay", "EPTT Delay Timer",
RadioSettingValueFloat(0, 25.5, _timer_eptt, precision=1)))
_group.append(RadioSetting(
"tone_mute_eptt_on", "Tone Mute EPTT",
RadioSettingValueList(
ON_OFF_CHOICE, current_index=int(
self._memobj.tone_mute_eptt_on))))
_group.append(RadioSetting(
"exo_on", "EXO", RadioSettingValueList(
ON_OFF_CHOICE, current_index=int(self._memobj.exo_on))))
_group.append(RadioSetting(
"timer_exo_delay", "EXO Delay Timer",
RadioSettingValueFloat(0, 25.5, _timer_exo, precision=1)))
_group.append(RadioSetting(
"timer_horn", "EXO/Horn Timer",
RadioSettingValueInteger(0, 255, self._memobj.timer.horn)))
_group.append(RadioSetting(
"scrambler_non_rolling", "Scrambler Type",
RadioSettingValueList(
SCRAMBLER_TYPE_VALUES, current_index=int(
self._memobj.scrambler_non_rolling))))
_group.append(RadioSetting(
"scrambler_group_code", "Scrambler Group Code",
RadioSettingValueList(
SCRAMBLER_GCODE_VALUES, current_index=int(
self._memobj.scrambler_group_code))))
_group.append(RadioSetting(
"scrambler_syn_capture", "Scrambler Synchronous Capture",
RadioSettingValueList(
SYN_CAPTURE_CHOICE, current_index=sc_sf(
self._memobj.scrambler_syn_capture))))
_group.append(RadioSetting(
"scrambler_tone_start_timing", "Scrambler Tone Start Timing",
RadioSettingValueList(
TONE_STIMING_VALUES, current_index=int(
self._memobj.scrambler_tone_start_timing))))
_group.append(RadioSetting(
"timer_emergency_switch_on", "Emergency Switch On Timer",
RadioSettingValueFloat(0, 25.5, _timer_es_on, precision=1)))
_group.append(RadioSetting(
"timer_emergency_switch_off", "Emergency Switch Off Timer",
RadioSettingValueFloat(0, 25.5, _timer_es_off, precision=1)))
_group.append(RadioSetting(
"emergency_start_repeat", "Emergency Start/Repeat",
RadioSettingValueInteger(
0, 255, self._memobj.timer.emergency_start_repeat)))
return _group
# Build the top menu.
def get_settings(self):
"""Returns a RadioSettingGroup containing one or more
RadioSettingGroup or RadioSetting objects. These represent general
setting knobs and dials that can be adjusted on the radio."""
return list(
RadioSettingGroup(
"top", "LMR",
self._get_continuous_tone_group(),
self._get_scan_group(),
self._get_conv_key_group(),
self._get_trunk_key_group(),
self._get_display_group(),
self._get_set_mode_group(),
self._get_common_group()))
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()."""
for element in settings:
if not isinstance(element, RadioSetting):
self.set_settings(element)
# "Continuous Tone" items
elif element.get_name() == "tone_burst_phase_on":
self._memobj.tone_burst_phase_on = element.value
elif element.get_name() == "tx_dtcs_inverse":
self._memobj.tx_dtcs_inverse = element.value
elif element.get_name() == "rx_dtcs_inverse":
self._memobj.rx_dtcs_inverse = element.value
elif element.get_name() == "rx_af_filter_low_cut_on":
self._memobj.rx_af_filter_low_cut_on = element.value
# "Scan" items
elif element.get_name() == "poweron_scan_on":
self._memobj.poweron_scan_on = element.value
# "Conventional Key" items
elif element.get_name() == "conv_key_lup":
self._memobj.conv_key.lup = kf_kc(element.value)
elif element.get_name() == "conv_key_ldown":
self._memobj.conv_key.ldown = kf_kc(element.value)
elif element.get_name() == "conv_key_rup":
self._memobj.conv_key.rup = kf_kc(element.value)
elif element.get_name() == "conv_key_rdown":
self._memobj.conv_key.rdown = kf_kc(element.value)
elif element.get_name() == "conv_key_p0":
self._memobj.conv_key.p0 = kf_kc(element.value)
elif element.get_name() == "conv_key_p1":
self._memobj.conv_key.p1 = kf_kc(element.value)
elif element.get_name() == "conv_key_p2":
self._memobj.conv_key.p2 = kf_kc(element.value)
elif element.get_name() == "conv_key_p3":
self._memobj.conv_key.p3 = kf_kc(element.value)
elif element.get_name() == "conv_key_p4":
self._memobj.conv_key.p4 = kf_kc(element.value)
elif element.get_name() == "conv_key_opf0":
self._memobj.conv_key.opf0 = kf_kc(element.value)
elif element.get_name() == "conv_key_opf1":
self._memobj.conv_key.opf1 = kf_kc(element.value)
elif element.get_name() == "conv_key_opf2":
self._memobj.conv_key.opf2 = kf_kc(element.value)
elif element.get_name() == "trunk_key_lup":
self._memobj.trunk_key.lup = kf_kt(element.value)
# "SmarTrunk Key" items
elif element.get_name() == "trunk_key_ldown":
self._memobj.trunk_key.ldown = kf_kt(element.value)
elif element.get_name() == "trunk_key_rup":
self._memobj.trunk_key.rup = kf_kt(element.value)
elif element.get_name() == "trunk_key_rdown":
self._memobj.trunk_key.rdown = kf_kt(element.value)
elif element.get_name() == "trunk_key_p0":
self._memobj.trunk_key.p0 = kf_kt(element.value)
elif element.get_name() == "trunk_key_p1":
self._memobj.trunk_key.p1 = kf_kt(element.value)
elif element.get_name() == "trunk_key_p2":
self._memobj.trunk_key.p2 = kf_kt(element.value)
elif element.get_name() == "trunk_key_p3":
self._memobj.trunk_key.p3 = kf_kt(element.value)
elif element.get_name() == "trunk_key_p4":
self._memobj.trunk_key.p4 = kf_kt(element.value)
elif element.get_name() == "trunk_key_opf0":
self._memobj.trunk_key.opf0 = kf_kt(element.value)
elif element.get_name() == "trunk_key_opf1":
self._memobj.trunk_key.opf1 = kf_kt(element.value)
elif element.get_name() == "trunk_key_opf2":
self._memobj.trunk_key.opf2 = kf_kt(element.value)
# "Display Mode" items
elif element.get_name() == "beep_frequency_low":
self._memobj.beep_frequency_low = element.value * 10.0
elif element.get_name() == "beep_frequency_high":
self._memobj.beep_frequency_high = element.value * 10.0
elif element.get_name() == "opening_text":
self._memobj.opening_text = str(
element.value).ljust(10)[:10]
elif element.get_name() == "rf_power_selection":
self._memobj.rf_power_selection = element.value
elif element.get_name() == "poweron_selection_off":
self._memobj.poweron_selection_off = not element.value
elif element.get_name() == "ignition_switch_on":
self._memobj.ignition_switch_on = element.value
elif element.get_name() == "ignition_switch_delay":
self._memobj.timer.ignition_switch_delay = element.value
elif element.get_name() == "poweron_priority_a":
self._memobj.poweron_priority_a = element.value
elif element.get_name() == "hookoff_priority_a":
self._memobj.hookoff_priority_a = element.value
elif element.get_name() == "hookoff_monitor_on":
self._memobj.hookoff_monitor_on = element.value
elif element.get_name() == "hookon_scan_on":
self._memobj.hookon_scan_on = element.value
# "Set Mode" items
elif element.get_name() == "backlight":
self._memobj.backlight = element.value
elif element.get_name() == "backlight_set_enable":
self._memobj.backlight_set_enable = element.value
elif element.get_name() == "beep_on":
self._memobj.beep_on = element.value
elif element.get_name() == "beep_set_enable":
self._memobj.beep_set_enable = element.value
elif element.get_name() == "beep_level":
self._memobj.beep_level = element.value
elif element.get_name() == "beep_level_set_enable":
self._memobj.beep_level_set_enable = element.value
elif element.get_name() == "squelch_level":
self._memobj.squelch_level = element.value
elif element.get_name() == "squelch_level_set_enable":
self._memobj.squelch_level_set_enable = element.value
elif element.get_name() == "volume_min_level":
self._memobj.volume_min_level = element.value
elif element.get_name() == "volume_min_level_set_enable":
self._memobj.volume_min_level_set_enable = element.value
elif element.get_name() == "audio_filter":
self._memobj.audio_filter = element.value
elif element.get_name() == "audio_filter_set_enable":
self._memobj.audio_filter_set_enable = element.value
elif element.get_name() == "mic_gain":
self._memobj.mic_gain = mf_mc(element.value)
elif element.get_name() == "mic_gain_set_enable":
self._memobj.mic_gain_set_enable = element.value
elif element.get_name() == "horn_off":
self._memobj.horn_off = not element.value
elif element.get_name() == "horn_set_enable":
self._memobj.horn_set_enable = element.value
# "Common" items
elif element.get_name() == "clone_comment":
self._memobj.clone_comment = str(
element.value).ljust(32)[:32]
elif element.get_name() == "password_on":
self._memobj.password_on = element.value
elif element.get_name() == "data_out_on":
self._memobj.data_out_on = element.value
elif element.get_name() == "timer_auto_reset_a":
self._memobj.timer.auto_reset_a = element.value
elif element.get_name() == "timer_auto_reset_b":
self._memobj.timer.auto_reset_b = element.value
elif element.get_name() == "timer_time_out":
self._memobj.timer.time_out = element.value
elif element.get_name() == "timer_penalty":
self._memobj.timer.penalty = element.value
elif element.get_name() == "dtmf_id_out_on":
self._memobj.dtmf_id_out_on = element.value
elif element.get_name() == "time_out_timer_beep_on":
self._memobj.time_out_timer_beep_on = element.value
elif element.get_name() == "timer_lockout_penalty":
self._memobj.timer.lockout_penalty = element.value
elif element.get_name() == "timer_eptt_delay":
self._memobj.timer.eptt_delay = element.value * 10.0
elif element.get_name() == "tone_mute_eptt_on":
self._memobj.tone_mute_eptt_on = element.value
elif element.get_name() == "exo_on":
self._memobj.tone_mute_eptt_on = element.value
elif element.get_name() == "timer_exo_delay":
self._memobj.timer.exo_delay = element.value * 10.0
elif element.get_name() == "timer_horn":
self._memobj.timer.horn = element.value
elif element.get_name() == "scrambler_non_rolling":
self._memobj.scrambler_non_rolling = element.value
elif element.get_name() == "scrambler_group_code":
self._memobj.scrambler_group_code = element.value
elif element.get_name() == "scrambler_syn_capture":
self._memobj.scrambler_syn_capture = sf_sc(element.value)
elif element.get_name() == "scrambler_tone_start_timing":
self._memobj.scrambler_tone_start_timing = element.value
elif element.get_name() == "timer_emergency_switch_on":
self._memobj.timer.emergency_switch_on = element.value * 10.0
elif element.get_name() == "timer_emergency_switch_off":
self._memobj.timer.emergency_switch_off = element.value * 10.0
elif element.get_name() == "emergency_start_repeat":
self._memobj.timer.emergency_start_repeat = element.value
else:
LOG.warning(
'Attempt to set _memobj.%s to "%s"'
% (element.get_name(), element.value))
chirp-20261030/chirp/drivers/icm710.py 0000644 0001750 0001750 00000013017 14751356113 016177 0 ustar hibby hibby # Copyright 2024 Paul Decker
# Derives from template.py
#
# 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 .
import logging
from chirp import chirp_common, directory
from chirp import bitwise
from chirp.drivers import icf
LOG = logging.getLogger(__name__)
BAUDS = [4800]
POWER_LEVELS = []
DUPLEX_MODES = ['', 'split', 'off']
# do not reorder this list
# MODULATION_MODES = ["USB", "R3E", "AM", "LSB", "AFS", "FSK", "CW"]
MODULATION_MODES = ["USB", "USB", "AM", "LSB", "DIG", "FSK", "CW"]
TUNING_STEPS = [0.5]
MEM_FORMAT = """
#seekto 0xE020;
struct {
bbcd rx_freq[4];
u8 mode;
char name[7];
bbcd tx_freq[4];
}memory[232];
"""
@directory.register
class IcomM710Radio(icf.IcomCloneModeRadio):
"""ICOM IC-M710"""
VENDOR = "Icom"
MODEL = "IC-M710"
BAUD_RATE = 4800
_model = "\x162\x00\x01" # 4-byte mode string
_endframe = ''
_ignore_clone_ok = True
_memsize = 0xEFC0
_ranges = [(0xE000, 0xEFC0, 32)]
_can_hispeed = False
_double_ident = False
_raw_frames = False
_highbit_flip = False
# 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_index = False
rf.has_dtcs = False
rf.has_rx_dtcs = False
rf.has_dtcs_polarity = False
rf.has_mode = True
rf.has_offset = False
rf.has_name = True
rf.has_bank = False
rf.has_bank_names = False
rf.has_tuning_step = False
rf.has_ctone = False
rf.has_cross = False
rf.has_infinite_number = False
rf.has_nostep_tuning = True
rf.has_comment = False
rf.has_settings = False # TODO radio does have settings
rf.has_variable_power = False
rf.memory_bounds = (0, 231)
rf.valid_bands = [(1000000, 29999999)]
rf.valid_duplexes = DUPLEX_MODES
rf.can_odd_split = True
rf.valid_modes = MODULATION_MODES
rf.valid_power_levels = POWER_LEVELS
rf.valid_characters = chirp_common.CHARSET_UPPER_NUMERIC + '/-'
rf.valid_tones = []
rf.valid_tmodes = []
rf.valid_skips = []
return rf
# 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])
def sync_out(self):
self.pipe.baudrate = 4800
icf.clone_to_radio(self)
# 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):
# this just gets the 160 common
# the 72 ITU channels are probably working
# there may be some strange behavior with
# the SITOR FSK "hidden" channels
# best be careful about trying to set a fixed
# simplex channel with a duplex frequency
# TODO test, test, test, all the combinations.
# Get a low-level memory object mapped to the image
_mem = self._memobj.memory[int(number)]
# Create a high-level memory object to return to the UI
mem = chirp_common.Memory()
mem.number = number
# Convert your low-level frequency to Hertz.
mem.freq = int(_mem.rx_freq)
mem.name = str(_mem.name).rstrip() # Set the alpha tag
mem.mode = MODULATION_MODES[0]
if (_mem.mode > -1) and (_mem.mode < 7):
mem.mode = MODULATION_MODES[int(_mem.mode)]
# if the frequency is above 30MHz
# (166666665 or FF,FF,FF,FF), it's simplex
if (int(_mem.tx_freq) > 29999999):
mem.duplex = 'off'
elif (int(_mem.rx_freq) == int(_mem.tx_freq)):
mem.duplex = ''
else:
mem.duplex = 'split'
mem.offset = int(_mem.tx_freq)
# We'll consider any blank (i.e. 0 MHz frequency) to be empty
if (mem.freq < 1000000) or (mem.freq > 29999999):
mem.empty = True
return mem
# Store details about a high-level memory to the memory map
# This is called when a user edits a memory in the UI
def set_memory(self, mem):
# Get a low-level memory object mapped to the image
_mem = self._memobj.memory[int(mem.number)]
_mem.name = mem.name.ljust(7)[:7]
if mem.empty:
_mem.rx_freq.set_raw(b'\xFF' * 4)
_mem.mode.set_raw(b'\x00')
_mem.tx_freq.set_raw(b'\xFF' * 4)
else:
_mem.rx_freq = mem.freq
_mem.mode = MODULATION_MODES.index(mem.mode)
if mem.duplex == 'off':
_mem.tx_freq.set_raw(b'\xFF' * 4)
elif mem.duplex == '':
_mem.tx_freq = _mem.rx_freq
else:
_mem.tx_freq = mem.offset
chirp-20261030/chirp/drivers/icomciv.py 0000644 0001750 0001750 00000145576 15154752252 016652 0 ustar hibby hibby # Latest update: Jan, 2025 add IC7410 support
import struct
import logging
from chirp.drivers import icf
from chirp import chirp_common, util, errors, bitwise, directory
from chirp.memmap import MemoryMapBytes
from chirp.settings import RadioSetting, RadioSettingGroup, \
RadioSettingValueList, RadioSettingValueBoolean
LOG = logging.getLogger(__name__)
MEM_FORMAT = """
bbcd number[2];
u8 unknown:3
split:1,
unknown_0:4;
lbcd freq[5];
u8 unknown2:5,
mode:3;
u8 filter;
u8 unknown_1:3,
dig:1,
unknown_2:4;
"""
# http://www.vk4adc.com/
# web/index.php/reference-information/49-general-ref-info/182-civ7400
MEM_IC7000_FORMAT = """
u8 bank;
bbcd number[2];
u8 spl:4,
skip:4;
lbcd freq[5];
u8 mode;
u8 filter;
u8 duplex:4,
tmode:4;
bbcd rtone[3];
bbcd ctone[3];
u8 dtcs_polarity;
bbcd dtcs[2];
lbcd freq_tx[5];
u8 mode_tx;
u8 filter_tx;
u8 duplex_tx:4,
tmode_tx:4;
bbcd rtone_tx[3];
bbcd ctone_tx[3];
u8 dtcs_polarity_tx;
bbcd dtcs_tx[2];
char name[9];
"""
MEM_IC7100_FORMAT = """
u8 bank; // 1 bank number
bbcd number[2]; // 2,3
u8 splitSelect; // 4 split and select memory settings
lbcd freq[5]; // 5-9 operating freq
u8 mode; // 10 operating mode
u8 filter; // 11 filter
u8 dataMode; // 12 data mode setting (on or off)
u8 duplex:4, // 13 duplex on/-/+
tmode:4; // 13 tone
u8 dsql:4, // 14 digital squelch
unknown1:4; // 14 zero
bbcd rtone[3]; // 15-17 repeater tone freq
bbcd ctone[3]; // 18-20 tone squelch setting
u8 dtcsPolarity; // 21 DTCS polarity
u8 unknown2:4, // 22 zero
firstDtcs:4; // 22 first digit of DTCS code
u8 secondDtcs:4, // 23 second digit DTCS
thirdDtcs:4; // 23 third digit DTCS
u8 digitalSquelch; // 24 Digital code squelch setting
lbcd duplexOffset[3]; // 25-27 duplex offset freq
char destCall[8]; // 28-35 destination call sign
char accessRepeaterCall[8];// 36-43 access repeater call sign
char linkRepeaterCall[8]; // 44-51 gateway/link repeater call sign
bbcd duplexSettings[47]; // repeat of 5-51 for duplex
char name[16]; // 52-60 Name of station
"""
MEM_IC910_FORMAT = """
u8 bank; // 1 bank number
bbcd number[2]; // 2,3
lbcd freq[5]; // 4-8 operating freq
u8 mode; // 9 operating mode
u8 filter; // 10 filter
u8 tmode:4, // 11 tone
duplex:4; // 11 duplex off/-/+
bbcd rtone[3]; // 12-14 repeater tone freq
bbcd ctone[3]; // 15-17 tone squelch setting
lbcd duplexOffset[3]; // 18-20 duplex offset freq
"""
mem_duptone_format = """
bbcd number[2];
u8 unknown1;
lbcd freq[5];
u8 unknown2:5,
mode:3;
u8 unknown1;
u8 unknown2:2,
duplex:2,
unknown3:1,
tmode:3;
u8 unknown4;
bbcd rtone[2];
u8 unknown5;
bbcd ctone[2];
u8 dtcs_polarity;
bbcd dtcs[2];
u8 unknown[11];
char name[9];
"""
MEM_IC7300_FORMAT = """
bbcd number[2]; // 1,2
u8 spl:4, // 3 split and select memory settings
select:4;
lbcd freq[5]; // 4-8 receive freq
u8 mode; // 9 operating mode
u8 filter; // 10 filter 1-3 (undocumented)
u8 dataMode:4, // 11 data mode setting (on or off)
tmode:4; // 11 tone type
char pad1;
bbcd rtone[2]; // 12-14 tx tone freq
char pad2;
bbcd ctone[2]; // 15-17 tone rx squelch setting
// This is duplicated from 4-17 above, even duplicated by number in the
// manual!
lbcd freq_tx[5]; // 4-8 receive freq
u8 mode_tx; // 9 operating mode
u8 filter_tx; // 10 filter 1-3 (undocumented)
u8 dataMode_tx:4, // 11 data mode setting (on or off)
tmode_tx:4; // 11 tone type
char pad1_tx;
bbcd rtone_tx[2]; // 12-14 tx tone freq
char pad2_tx;
bbcd ctone_tx[2]; // 15-17 tone rx squelch setting
// End TX duplicate block
char name[10]; // 18-27 Callsign
"""
MEM_IC9700_FORMAT = """
u8 bank;
bbcd number[2];
u8 select_memory;
lbcd freq[5];
bbcd mode;
u8 filter;
bbcd data_mode;
u8 duplex:4,
tmode:4;
u8 dig_sql:4,
unused1:4;
bbcd rtone[3];
bbcd ctone[3];
u8 dtcs_polarity;
bbcd dtcs[2];
u8 dig_code;
lbcd duplexOffset[3];
char urcall[8];
char rpt1call[8];
char rpt2call[8];
char name[16];
"""
MEM_IC9700_SAT_FORMAT = """
bbcd number[2];
lbcd freq[5];
bbcd mode;
u8 filter;
bbcd data_mode;
u8 unused1:4,
tmode:4;
u8 unused2:4,
dig_sql:4;
bbcd rtone[3];
bbcd ctone[3];
u8 dtcs_polarity;
bbcd dtcs[2];
u8 dig_code;
char urcall[8];
char rpt1call[8];
char rpt2call[8];
struct {
lbcd freq[5];
bbcd mode;
u8 filter;
bbcd data_mode;
u8 unused1:4,
tmode:4;
u8 unused2:4,
dig_sql:4;
bbcd rtone[3];
bbcd ctone[3];
u8 dtcs_polarity;
bbcd dtcs[2];
u8 dig_code;
char urcall[8];
char rpt1call[8];
char rpt2call[8];
} tx;
char name[16];
"""
MEM_IC7400_FORMAT = """
bbcd number[2];
u8 unknown1;
lbcd freq[5];
u8 mode;
u8 filter;
u8 unknown2:7,
dig:1;
u8 unknown3:2,
duplex:2,
unknown4:1,
tmode:3;
bbcd rtone[3];
bbcd ctone[3];
u8 dtcs_polarity;
bbcd dtcs[2];
// As with IC-7300 it seems the following are duplicated parameters save for
// `dig` which seem to be zeroed in unknown5
lbcd freq_tx[5];
u8 mode_tx;
u8 filter_tx;
u8 unknown5;
u8 unknown6:2,
duplex_tx:2,
unknown7:1,
tmode_tx:3;
bbcd rtone_tx[3];
bbcd ctone_tx[3];
u8 dtcs_polarity_tx;
bbcd dtcs_tx[2];
// End TX duplicate block
char name[8];
"""
MEM_IC7410_FORMAT = """
bbcd number[2];
u8 duplex:4, // 3 split and select memory settings
select:4;
lbcd freq[5];
u8 mode;
u8 filter;
u8 unknown2:7,
dig:1;
u8 unknown3:6,
tmode:2;
bbcd rtone[3];
bbcd ctone[3];
// As with IC-7300 it seems the following are duplicated parameters save for
// `dig` which seem to be zeroed in unknown5
lbcd duplexOffset[5];
u8 mode_tx;
u8 filter_tx;
u8 unknown5:7,
dig_tx:1;
u8 unknown6:6,
skip:2;
bbcd rtone_tx[3];
bbcd ctone_tx[3];
// End TX duplicate block
char name[9];
"""
MEM_IC7610_FORMAT = """
bbcd number[2]; // 1,2
u8 spl:4, // 3 split and select memory settings
select:4;
lbcd freq[5]; // 4-8 receive freq
u8 mode; // 9 operating mode
u8 filter; // 10 filter 1-3 (undocumented)
u8 dataMode:4, // 11 data mode setting (on or off)
tmode:4; // 11 tone type
char pad1;
bbcd rtone[2]; // 12-14 tx tone freq
char pad2;
bbcd ctone[2]; // 15-17 tone rx squelch setting
char name[10]; // 18-27 Callsign
"""
SPLIT = ["", "spl"]
class Frame:
"""Base class for an ICF frame"""
_cmd = 0x00
_sub: int | None = 0x00
def __init__(self):
self._data = b""
def set_command(self, cmd, sub=None):
"""Set the command number (and optional subcommand)"""
self._cmd = cmd
self._sub = sub
def get_data(self):
"""Return the data payload"""
return self._data
def set_data(self, data):
"""Set the data payload"""
self._data = data
def send(self, src, dst, serial, willecho=True):
"""Send the frame over @serial, using @src and @dst addresses"""
hdr = struct.pack("BBBBB", 0xFE, 0xFE, src, dst, self._cmd)
# Some commands have no subcommand
if self._sub is not None:
hdr += bytes([self._sub])
raw = bytearray(hdr)
if isinstance(self._data, MemoryMapBytes):
data = self._data.get_packed()
else:
data = self._data
raw.extend(data)
raw.append(0xFD)
LOG.debug("%02x -> %02x (%i):\n%s" %
(src, dst, len(raw), util.hexprint(bytes(raw))))
serial.write(raw)
if willecho:
echo = serial.read(len(raw))
if echo != raw and echo:
LOG.debug("Echo differed (%i/%i)" % (len(raw), len(echo)))
LOG.debug(util.hexprint(bytes(raw)))
LOG.debug(util.hexprint(bytes(echo)))
def read(self, serial):
"""Read the frame from @serial"""
data = bytearray()
while not (data and data[-1] == 0xFD):
char = serial.read(1)
if not char:
LOG.debug("Read %i bytes total" % len(data))
raise errors.RadioError("Timeout")
data.extend(char)
if data[0] == 0xFD:
raise errors.RadioError("Radio reported error")
src, dst = struct.unpack("BB", data[2:4])
LOG.debug("%02x <- %02x:\n%s" % (dst, src, util.hexprint(bytes(data))))
self._cmd = data[4]
# If we've been set with a None subcommand, assume we don't expect
# it from the radio
if self._sub is None:
dataidx = 5
else:
self._sub = data[5]
dataidx = 6
self._data = data[dataidx:-1]
return src, dst
def get_obj(self):
raise errors.RadioError("Generic frame has no structure")
class MemFrame(Frame):
"""A memory frame"""
_cmd = 0x1A
_sub = 0x00
_loc = 0
FORMAT = MEM_FORMAT
def set_location(self, loc):
"""Set the memory location number"""
self._loc = loc
self._data = struct.pack(">H", int("%04i" % loc, 16))
def make_empty(self):
"""Mark as empty so the radio will erase the memory"""
self._data = struct.pack(">HB", int("%04i" % self._loc, 16), 0xFF)
def is_empty(self):
"""Return True if memory is marked as empty"""
return len(self._data) < 5
def get_obj(self):
"""Return a bitwise parsed object"""
# Make sure we're assignable
self._data = MemoryMapBytes(bytes(self._data))
return bitwise.parse(self.FORMAT, self._data)
def initialize(self):
"""Initialize to sane values"""
self._data = bytes(b'\x00' * (self.get_obj().size() // 8))
class BankMemFrame(MemFrame):
"""A memory frame for radios with multiple banks"""
FORMAT = MEM_IC7000_FORMAT
_bnk = 0
def set_location(self, loc, bank=1):
self._loc = loc
self._bnk = bank
self._data = struct.pack(
">BH", int("%02i" % bank, 16), int("%04i" % loc, 16))
def make_empty(self):
"""Mark as empty so the radio will erase the memory"""
self._data = struct.pack(
">BHB", int("%02i" % self._bnk, 16),
int("%04i" % self._loc, 16), 0xFF)
def get_obj(self):
# Make sure we're assignable
self._data = MemoryMapBytes(bytes(self._data))
return bitwise.parse(self.FORMAT, self._data)
class IC7100MemFrame(BankMemFrame):
FORMAT = MEM_IC7100_FORMAT
class IC910MemFrame(BankMemFrame):
FORMAT = MEM_IC910_FORMAT
class DupToneMemFrame(MemFrame):
def get_obj(self):
self._data = MemoryMapBytes(bytes(self._data))
return bitwise.parse(mem_duptone_format, self._data)
class IC7400MemFrame(MemFrame):
def get_obj(self):
self._data = MemoryMapBytes(bytes(self._data))
return bitwise.parse(MEM_IC7400_FORMAT, self._data)
class IC7410MemFrame(MemFrame):
def get_obj(self):
self._data = MemoryMapBytes(bytes(self._data))
return bitwise.parse(MEM_IC7410_FORMAT, self._data)
class IC7300MemFrame(MemFrame):
FORMAT = MEM_IC7300_FORMAT
class IC7610MemFrame(MemFrame):
FORMAT = MEM_IC7610_FORMAT
class IC9700MemFrame(BankMemFrame):
FORMAT = MEM_IC9700_FORMAT
class IC9700SatMemFrame(MemFrame):
_sub = 0x07
FORMAT = MEM_IC9700_SAT_FORMAT
class SpecialChannel(object):
"""Info for special (named) channels"""
def __init__(self):
self.name = None
self.location = None
self.channel = None
def __repr__(self):
s = "SpecialChannel(name=%r, location=%r, channel=%r)"
return s % (self.name, self.location, self.channel)
class BankSpecialChannel(SpecialChannel):
"""Info for special (named) channels for radios with multiple banks"""
def __init__(self):
super(BankSpecialChannel, self).__init__()
self.bank = None
def __repr__(self):
s = "BankSpecialChannel(name=%r, location=%r, bank=%r, channel=%r)"
return s % (self.name, self.location, self.bank, self.channel)
class IcomCIVRadio(icf.IcomLiveRadio):
"""Base class for ICOM CIV-based radios"""
BAUD_RATE = 19200
MODEL = "CIV Radio"
# RTS is interpreted as "transmit now" on some interface boxes for these
WANTS_RTS = False
_model = "\x00"
_template = 0
# complete list of modes from CI-V documentation
# each radio supports a subset
# WARNING: "S-AM" and "PSK" are not valid (yet) for chirp
_MODES = [
"LSB", "USB", "AM", "CW", "RTTY", "FM", "WFM", "CWR",
"RTTYR", "S-AM", "PSK", None, None, None, None, None,
None, None, None, None, None, None, None, None,
"DV",
]
# Unified modes where mode and filter are combined. See note at
# _unified_modes.
_UNIFIED_MODES = {
'FM': 'NFM',
'CW': 'NCW'
}
def mem_to_ch_bnk(self, mem):
if self._adjust_bank_loc_start:
mem -= 1
l, h = self._bank_index_bounds
bank_no = (mem // (h - l + 1)) + l
channel = mem % (h - l + 1) + l
return (channel, bank_no)
def _is_special(self, number):
return False
def _get_special_info(self, number):
raise errors.RadioError("Radio does not support special channels")
def _send_frame(self, frame):
return frame.send(ord(self._model), 0xE0, self.pipe,
willecho=self._willecho)
def _recv_frame(self, frame=None):
if not frame:
frame = Frame()
frame.read(self.pipe)
return frame
def _initialize(self):
pass
def _detect_echo(self):
echo_test = b"\xfe\xfe\xe0\xe0\xfa\xfd"
self.pipe.write(echo_test)
resp = self.pipe.read(6)
LOG.debug("Echo:\n%s" % util.hexprint(bytes(resp)))
return resp == echo_test
def _detect_baudrate(self):
if self._baud_detected:
return
# Don't ever try to run this twice, even if we fail
self._baud_detected = True
bauds = [9600, 19200, 38400, 57600, 115200, 4800]
bauds.remove(self.BAUD_RATE)
bauds.insert(0, self.BAUD_RATE)
self.pipe.timeout = 0.25
for baud in bauds:
LOG.debug('Trying %i baud' % baud)
self.pipe.baudrate = baud
self._willecho = self._detect_echo()
LOG.debug("Interface echo: %s" % self._willecho)
try:
self._get_template_memory()
LOG.info('Detected %i baud' % baud)
break
except errors.RadioError:
pass
else:
LOG.warning('Unable to detect baudrate, using default of %i' % (
self.BAUD_RATE))
self.pipe.baudrate = self.BAUD_RATE
# Restore the historical default of 1s timeout for this driver
self.pipe.timeout = 1
def __init__(self, *args, **kwargs):
icf.IcomLiveRadio.__init__(self, *args, **kwargs)
self._classes = {
"mem": MemFrame,
}
if self.pipe:
self._willecho = self._detect_echo()
LOG.debug("Interface echo: %s" % self._willecho)
self.pipe.timeout = 1
else:
self._willecho = False
self._baud_detected = False
# f = Frame()
# f.set_command(0x19, 0x00)
# self._send_frame(f)
#
# res = f.read(self.pipe)
# if res:
# LOG.debug("Result: %x->%x (%i)" %
# (res[0], res[1], len(f.get_data())))
# LOG.debug(util.hexprint(f.get_data()))
#
# self._id = f.get_data()[0]
self._rf = chirp_common.RadioFeatures()
# On some radios, the filter field is used to signify normal versus
# narrow modes, rather than being a distinct passband feature. As
# such, mode + filter comprises a "unified mode" value that can be
# mapped into a Chirp mode.
self._unified_modes = False
# Icom live radios with bank support present their memories to the
# user starting from 1. For some reason, IC-7000 and IC-7100 were
# implemented with the Chirp location starting from 0, so that the
# user must mentally adjust. While adding IC-910 support, allowance
# was made to provide a 1-based start, using the following setting.
# This is not currently applied to the IC-7000 or IC-7100 due to the
# inability to test, and also since changing it may cause issues if
# location limit keys have been saved in the user's config file.
self._adjust_bank_loc_start = False
self._initialize()
def get_features(self):
return self._rf
def _get_template_memory(self):
f = self._classes["mem"]()
f.set_location(self._template)
self._send_frame(f)
f.read(self.pipe)
return f
def get_raw_memory(self, number):
self._detect_baudrate()
LOG.debug("Getting %s (raw)" % number)
f = self._classes["mem"]()
if self._is_special(number):
info = self._get_special_info(number)
LOG.debug("Special info: %s" % info)
ch = info.channel
if self._rf.has_bank:
bnk = info.bank
elif self._rf.has_bank:
ch, bnk = self.mem_to_ch_bnk(number)
else:
ch = number
if self._rf.has_bank:
f.set_location(ch, bnk)
loc = "bank %i, channel %02i" % (bnk, ch)
else:
f.set_location(ch)
loc = "number %i" % ch
self._send_frame(f)
f.read(self.pipe)
try:
self.get_memory(number)
except Exception as e:
LOG.exception('Failed to get/parse test memory: %s', e)
if f.get_data() and f.get_data()[-1] == "\xFF":
return "Memory " + loc + " empty."
else:
return repr(f.get_obj())
# We have a simple mapping between the memory location in the frequency
# editor and (bank, channel) of the radio. The mapping doesn't
# change so we use a little math to calculate what bank a location
# is in. We can't change the bank a location is in so we just pass.
def _get_bank(self, loc):
if self._adjust_bank_loc_start:
loc -= 1
l, h = self._bank_index_bounds
return loc // (h - l + 1)
def _set_bank(self, loc, bank):
pass
def get_memory(self, number):
self._detect_baudrate()
LOG.debug("Getting %s" % number)
f = self._classes["mem"]()
mem = chirp_common.Memory()
if self._is_special(number):
info = self._get_special_info(number)
LOG.debug("Special info: %s" % info)
if self._rf.has_bank:
f.set_location(info.channel, info.bank)
else:
f.set_location(info.channel)
mem.number = info.location
mem.extd_number = info.name
mem.immutable = ["number", "extd_number"]
else:
if self._rf.has_bank:
ch, bnk = self.mem_to_ch_bnk(number)
f.set_location(ch, bnk)
LOG.debug("Bank %i, Channel %02i" % (bnk, ch))
else:
f.set_location(number)
mem.number = number
self._send_frame(f)
f = self._recv_frame(f)
if len(f.get_data()) == 0:
raise errors.RadioError("Radio reported error")
if f.get_data() and f.get_data()[-1] == 0xFF:
mem.empty = True
LOG.debug("Found %i empty" % mem.number)
return mem
memobj = f.get_obj()
LOG.debug(repr(memobj))
try:
if memobj.skip == 1:
mem.skip = ""
else:
mem.skip = "S"
except AttributeError:
pass
mem.freq = int(memobj.freq)
try:
# Note that memobj.mode could be a bcd on some radios, so we must
# coerce it to an int for the index operation.
mem.mode = self._MODES[int(memobj.mode)]
# We do not know what a variety of the positions between
# PSK and DV mean, so let's behave as if those values
# are not set to maintain consistency between known-unknown
# values and unknown-unknown ones.
if mem.mode is None:
raise IndexError(memobj.mode)
except IndexError:
LOG.error(
"Bank %s location %s is set for mode %s, but no known "
"mode matches that value.",
int(memobj.bank),
int(memobj.number),
repr(memobj.mode),
)
raise
if self._unified_modes and memobj.filter == 2:
try:
# Adjust mode to its narrow variant
mem.mode = self._UNIFIED_MODES[mem.mode]
except KeyError:
LOG.error(
"Bank %s location %s is set for mode %s with filter %s, "
"but no known mode matches that combination.",
int(memobj.bank),
int(memobj.number),
repr(memobj.mode),
int(memobj.filter),
)
raise
if self._rf.has_name:
mem.name = str(memobj.name).rstrip()
if self._rf.valid_tmodes:
self._decode_tmode(mem, memobj)
if self._rf.has_dtcs_polarity:
if memobj.dtcs_polarity == 0x11:
mem.dtcs_polarity = "RR"
elif memobj.dtcs_polarity == 0x10:
mem.dtcs_polarity = "RN"
elif memobj.dtcs_polarity == 0x01:
mem.dtcs_polarity = "NR"
else:
mem.dtcs_polarity = "NN"
if self._rf.has_dtcs:
mem.dtcs = int(memobj.dtcs)
if "Tone" in self._rf.valid_tmodes:
mem.rtone = int(memobj.rtone) / 10.0
if "TSQL" in self._rf.valid_tmodes and self._rf.has_ctone:
mem.ctone = int(memobj.ctone) / 10.0
if self._rf.valid_duplexes:
mem.duplex = self._rf.valid_duplexes[memobj.duplex]
if self._rf.can_odd_split and memobj.spl:
if hasattr(memobj, "duplex"):
mem.duplex = "split"
mem.offset = int(memobj.freq_tx)
mem.immutable = []
elif hasattr(memobj, "duplexOffset"):
mem.offset = int(memobj.duplexOffset) * 100
else:
mem.immutable = ["offset"]
try:
dig = RadioSetting("dig", "Digital",
RadioSettingValueBoolean(bool(memobj.dig)))
except AttributeError:
pass
else:
dig.set_doc("Enable digital mode")
if not mem.extra:
mem.extra = RadioSettingGroup("extra", "Extra")
mem.extra.append(dig)
if not self._unified_modes:
options = ["Wide", "Mid", "Narrow"]
try:
fil = RadioSetting(
"filter", "Filter",
RadioSettingValueList(options,
current_index=memobj.filter - 1))
except AttributeError:
pass
else:
fil.set_doc("Filter settings")
if not mem.extra:
mem.extra = RadioSettingGroup("extra", "Extra")
mem.extra.append(fil)
return mem
def set_memory(self, mem):
self._detect_baudrate()
LOG.debug("Setting %s(%s)" % (mem.number, mem.extd_number))
f = self._get_template_memory()
if self._is_special(mem.number):
info = self._get_special_info(mem.number)
LOG.debug("Special info: %s" % info)
ch = info.channel
if self._rf.has_bank:
bnk = info.bank
elif self._rf.has_bank:
ch, bnk = self.mem_to_ch_bnk(mem.number)
LOG.debug("Bank %i, Channel %02i" % (bnk, ch))
else:
ch = mem.number
if mem.empty:
if self._rf.has_bank:
f.set_location(ch, bnk)
else:
f.set_location(ch)
LOG.debug("Making %i empty" % mem.number)
f.make_empty()
self._send_frame(f)
# The next two lines accept the radio's status after setting the memory
# and reports the results to the debug log. This is needed for the
# IC-7000. No testing was done to see if it breaks memory delete on the
# IC-746 or IC-7200.
f = self._recv_frame()
LOG.debug("Result:\n%s" % util.hexprint(bytes(f.get_data())))
return
# f.set_data(MemoryMap(self.get_raw_memory(mem.number)))
# f.initialize()
memobj = f.get_obj()
if self._rf.has_bank:
memobj.bank = bnk
memobj.number = ch
else:
memobj.number = ch
if mem.skip == "S":
memobj.skip = 0
else:
try:
memobj.skip = 1
except KeyError:
pass
memobj.freq = int(mem.freq)
mode = mem.mode
if self._unified_modes:
lookup = [
k for k, v in self._UNIFIED_MODES.items() if v == mode]
if lookup:
mode = lookup[0]
memobj.filter = 2
else:
memobj.filter = 1
memobj.mode = self._MODES.index(mode)
if self._rf.has_name:
name_length = len(memobj.name.get_value())
memobj.name = mem.name.ljust(name_length)[:name_length]
if self._rf.valid_tmodes:
self._encode_tmode(mem, memobj)
if self._rf.has_ctone:
memobj.ctone = int(mem.ctone * 10)
memobj.rtone = int(mem.rtone * 10)
if self._rf.has_dtcs_polarity:
if mem.dtcs_polarity == "RR":
memobj.dtcs_polarity = 0x11
elif mem.dtcs_polarity == "RN":
memobj.dtcs_polarity = 0x10
elif mem.dtcs_polarity == "NR":
memobj.dtcs_polarity = 0x01
else:
memobj.dtcs_polarity = 0x00
if self._rf.has_dtcs:
memobj.dtcs = mem.dtcs
if self._rf.can_odd_split and mem.duplex == "split":
memobj.spl = 1
if hasattr(memobj, "duplex"):
memobj.duplex = 0
memobj.freq_tx = int(mem.offset)
memobj.tmode_tx = memobj.tmode
memobj.ctone_tx = memobj.ctone
memobj.rtone_tx = memobj.rtone
if self._rf.has_dtcs:
memobj.dtcs_polarity_tx = memobj.dtcs_polarity
memobj.dtcs_tx = memobj.dtcs
elif self._rf.valid_duplexes:
memobj.duplex = self._rf.valid_duplexes.index(mem.duplex)
if hasattr(memobj, "duplexOffset"):
memobj.duplexOffset = int(mem.offset) // 100
for setting in mem.extra:
if setting.get_name() == "filter":
setattr(memobj, setting.get_name(), int(setting.value) + 1)
else:
setattr(memobj, setting.get_name(), setting.value)
LOG.debug(repr(memobj))
self._send_frame(f)
f = self._recv_frame()
LOG.debug("Result:\n%s" % util.hexprint(bytes(f.get_data())))
def _encode_tmode(self, mem, memobj):
memobj.tmode = self._rf.valid_tmodes.index(mem.tmode)
def _decode_tmode(self, mem, memobj):
mem.tmode = self._rf.valid_tmodes[memobj.tmode]
@directory.register
class Icom7200Radio(IcomCIVRadio):
"""Icom IC-7200"""
MODEL = "IC-7200"
_model = "\x76"
_template = 201
_num_banks = 1 # Banks not supported
def _initialize(self):
self._rf.has_bank = False
self._rf.has_dtcs_polarity = False
self._rf.has_dtcs = False
self._rf.has_ctone = False
self._rf.has_offset = False
self._rf.has_name = False
self._rf.has_tuning_step = False
self._rf.valid_modes = ["LSB", "USB", "AM", "CW", "RTTY",
"CWR", "RTTYR"]
self._rf.valid_tmodes = []
self._rf.valid_duplexes = []
self._rf.valid_bands = [(30000, 60000000)]
self._rf.valid_skips = []
self._rf.memory_bounds = (1, 201)
@directory.register
class Icom7000Radio(IcomCIVRadio):
"""Icom IC-7000"""
MODEL = "IC-7000"
_model = "\x70"
_template = 102
_num_banks = 5 # Banks A-E
_bank_index_bounds = (1, 99)
_bank_class = icf.IcomBank
def _initialize(self):
self._classes["mem"] = BankMemFrame
self._rf.has_bank = True
self._rf.has_dtcs_polarity = True
self._rf.has_dtcs = True
self._rf.has_ctone = True
self._rf.has_offset = True
self._rf.has_name = True
self._rf.has_tuning_step = False
self._rf.valid_modes = ["LSB", "USB", "AM", "CW", "RTTY", "FM", "WFM"]
self._rf.valid_tmodes = ["", "Tone", "TSQL", "DTCS"]
self._rf.valid_duplexes = ["", "-", "+", "split"]
self._rf.valid_bands = [(30000, 199999999), (400000000, 470000000)]
self._rf.valid_tuning_steps = []
self._rf.valid_skips = ["S", ""]
self._rf.valid_name_length = 9
self._rf.valid_characters = chirp_common.CHARSET_ASCII
self._rf.memory_bounds = (0, 99 * self._num_banks - 1)
self._rf.can_odd_split = True
@directory.register
class Icom7100Radio(IcomCIVRadio):
"""Icom IC-7100"""
MODEL = "IC-7100"
_model = "\x88"
_template = 102
_num_banks = 5
_bank_index_bounds = (1, 99)
_bank_class = icf.IcomBank
def _initialize(self):
self._classes["mem"] = IC7100MemFrame
self._rf.has_bank = True
self._rf.has_bank_index = False
self._rf.has_bank_names = False
self._rf.has_dtcs_polarity = False
self._rf.has_dtcs = False
self._rf.has_ctone = True
self._rf.has_offset = True
self._rf.has_name = True
self._rf.has_tuning_step = False
self._rf.valid_modes = [
"LSB", "USB", "AM", "CW", "RTTY", "FM", "WFM", "CWR", "RTTYR", "DV"
]
self._rf.valid_tmodes = ["", "Tone", "TSQL", "DTCS"]
self._rf.valid_duplexes = ["", "-", "+"]
self._rf.valid_bands = [(30000, 199999999), (400000000, 470000000)]
self._rf.valid_tuning_steps = []
self._rf.valid_skips = []
self._rf.valid_name_length = 16
self._rf.valid_characters = chirp_common.CHARSET_ASCII
self._rf.memory_bounds = (0, 99 * self._num_banks - 1)
@directory.register
class Icom746Radio(IcomCIVRadio):
"""Icom IC-746"""
MODEL = "IC-746"
BAUD_RATE = 9600
_model = "\x56"
_template = 102
_num_banks = 1 # Banks not supported
def _initialize(self):
self._classes["mem"] = DupToneMemFrame
self._rf.has_bank = False
self._rf.has_dtcs_polarity = False
self._rf.has_dtcs = False
self._rf.has_ctone = True
self._rf.has_offset = False
self._rf.has_name = True
self._rf.has_tuning_step = False
self._rf.valid_modes = ["LSB", "USB", "AM", "CW", "RTTY", "FM"]
self._rf.valid_tmodes = ["", "Tone", "TSQL"]
self._rf.valid_duplexes = ["", "-", "+"]
self._rf.valid_bands = [(30000, 199999999)]
self._rf.valid_tuning_steps = []
self._rf.valid_skips = []
self._rf.valid_name_length = 9
self._rf.valid_characters = chirp_common.CHARSET_ASCII
self._rf.memory_bounds = (1, 99)
@directory.register
class Icom7400Radio(IcomCIVRadio):
"""Icom IC-7400"""
MODEL = "IC-7400"
BAUD_RATE = 9600
_model = "\x66"
_template = 102
_num_banks = 1 # Banks not supported
def _initialize(self):
self._classes["mem"] = IC7400MemFrame
self._rf.has_bank = False
self._rf.has_dtcs_polarity = True
self._rf.has_dtcs = True
self._rf.has_ctone = True
self._rf.has_offset = False
self._rf.has_name = True
self._rf.has_tuning_step = False
self._rf.valid_modes = [
"LSB", "USB", "AM", "CW", "RTTY", "FM", "USB", "CWR", "RTTYR"
]
self._rf.valid_tmodes = ["", "Tone", "TSQL", "DTCS"]
self._rf.valid_duplexes = ["", "-", "+"]
self._rf.valid_bands = [(30000, 174000000)]
self._rf.valid_tuning_steps = []
self._rf.valid_skips = []
self._rf.valid_name_length = 8
self._rf.valid_characters = " !#$%&'()*+,-./" \
"0123456789" \
":;<=>?" \
"ABCDEFGHIJKLMNOPQRSTUVWXYZ" \
"[\\]^_" \
"abcdefghijklmnopqrstuvwxyz" \
"{|}~"
self._rf.memory_bounds = (1, 99)
@directory.register
class Icom7410Radio(IcomCIVRadio):
"""Icom IC-7410"""
MODEL = "IC-7410"
BAUD_RATE = 9600
_model = "\x80"
_template = 99
_num_banks = 1 # Banks not supported
_SPECIAL_CHANNELS = {
"P1": 100,
"P2": 101,
}
_SPECIAL_CHANNELS_REV = dict(zip(_SPECIAL_CHANNELS.values(),
_SPECIAL_CHANNELS.keys()))
def _is_special(self, number):
return isinstance(number, str) or number > 99
def _get_special_info(self, number):
info = SpecialChannel()
if isinstance(number, str):
info.name = number
info.channel = self._SPECIAL_CHANNELS[number]
info.location = info.channel
else:
info.location = number
info.name = self._SPECIAL_CHANNELS_REV[number]
info.channel = info.location
return info
def _initialize(self):
self._classes["mem"] = IC7410MemFrame
self._rf.has_bank = False
self._rf.has_dtcs_polarity = False
self._rf.has_dtcs = False
self._rf.has_ctone = True
self._rf.has_offset = False
self._rf.has_name = True
self._rf.has_tuning_step = False
self._rf.valid_modes = [
"LSB", "USB", "AM", "CW", "RTTY", "FM", "CWR", "RTTYR",
"Data+LSB", "Data+USB", "Data+AM", "N/A", "N/A", "Data+FM"
]
self._rf.valid_tmodes = ["", "Tone", "TSQL"]
self._rf.valid_duplexes = ["", "split"]
self._rf.valid_bands = [(30000, 60000000)]
self._rf.valid_tuning_steps = []
self._rf.valid_skips = []
self._rf.valid_name_length = 9
self._rf.valid_characters = " !#$%&'()*+,-./" \
"0123456789" \
":;<=>?" \
"ABCDEFGHIJKLMNOPQRSTUVWXYZ" \
"[\\]^_" \
"abcdefghijklmnopqrstuvwxyz" \
"{|}~"
self._rf.memory_bounds = (1, 99)
self._rf.valid_special_chans = sorted(self._SPECIAL_CHANNELS.keys())
@directory.register
class Icom910Radio(IcomCIVRadio):
"""Icom IC-910"""
MODEL = "IC-910"
BAUD_RATE = 19200
_model = "\x60"
_template = 100
_num_banks = 3 # Banks for 2m, 70cm, 23cm
_bank_index_bounds = (1, 99)
_bank_class = icf.IcomBank
_SPECIAL_CHANNELS = {
"1A": 100,
"1b": 101,
"2A": 102,
"2b": 103,
"3A": 104,
"3b": 105,
"C": 106,
}
_SPECIAL_CHANNELS_REV = {v: k for k, v in _SPECIAL_CHANNELS.items()}
_SPECIAL_BANKS = {
"2m": 1,
"70cm": 2,
"23cm": 3,
}
_SPECIAL_BANKS_REV = {v: k for k, v in _SPECIAL_BANKS.items()}
def _get_special_names(self, band):
return sorted([band + "-" + key
for key in self._SPECIAL_CHANNELS.keys()])
def _is_special(self, number):
return isinstance(number, str) or number >= 1000
def _get_special_info(self, number):
info = BankSpecialChannel()
if isinstance(number, str):
info.name = number
(band_name, chan_name) = number.split("-")
info.bank = self._SPECIAL_BANKS[band_name]
info.channel = self._SPECIAL_CHANNELS[chan_name]
info.location = info.bank * 1000 + info.channel
else:
info.location = number
(info.bank, info.channel) = divmod(number, 1000)
band_name = self._SPECIAL_BANKS_REV[info.bank]
chan_name = self._SPECIAL_CHANNELS_REV[info.channel]
info.name = band_name + "-" + chan_name
return info
# The IC-910 has a bank of memories for each band. The 23cm band is only
# available when the optional UX-910 unit is installed, but there is no
# direct means of detecting its presence. Instead, attempt to access the
# first memory in the 23cm bank. If that's successful, the unit is there,
# and we can present all 3 banks to the user. Otherwise, the unit is not
# installed, so we present 2 banks to the user, for 2m and 70cm.
def _detect_23cm_unit(self):
if not self.pipe:
return True
self._detect_baudrate()
f = IC910MemFrame()
f.set_location(1, 3) # First memory in 23cm bank
self._send_frame(f)
f.read(self.pipe)
if f._cmd == 0xFA: # Error code lands in command field
self._num_banks = 2
LOG.debug("UX-910 unit is %sinstalled" %
("not " if self._num_banks == 2 else ""))
return self._num_banks == 3
def _initialize(self):
self._classes["mem"] = IC910MemFrame
self._has_23cm_unit = self._detect_23cm_unit()
self._rf.has_bank = True
self._rf.has_dtcs_polarity = False
self._rf.has_dtcs = False
self._rf.has_ctone = True
self._rf.has_offset = True
self._rf.has_name = False
self._rf.has_tuning_step = False
self._rf.valid_modes = ["LSB", "USB", "CW", "NCW", "FM", "NFM"]
self._rf.valid_tmodes = ["", "Tone", "TSQL"]
self._rf.valid_duplexes = ["", "-", "+"]
self._rf.valid_bands = [(136000000, 174000000),
(420000000, 480000000)]
self._rf.valid_tuning_steps = []
self._rf.valid_skips = []
self._rf.valid_special_chans = (self._get_special_names("2m") +
self._get_special_names("70cm"))
self._rf.memory_bounds = (1, 99 * self._num_banks)
if self._has_23cm_unit:
self._rf.valid_bands.append((1240000000, 1320000000))
self._rf.valid_special_chans += self._get_special_names("23cm")
# Combine mode and filter into unified mode
self._unified_modes = True
# Use Chirp locations starting with 1
self._adjust_bank_loc_start = True
@directory.register
class Icom7300Radio(IcomCIVRadio): # Added March, 2021 by Rick DeWitt
"""Icom IC-7300"""
MODEL = "IC-7300"
BAUD_RATE = 115200
_model = "\x94"
_template = 100 # Use P1 as blank template
_SPECIAL_CHANNELS = {
"P1": 100,
"P2": 101,
}
_SPECIAL_CHANNELS_REV = dict(zip(_SPECIAL_CHANNELS.values(),
_SPECIAL_CHANNELS.keys()))
def _is_special(self, number):
return isinstance(number, str) or number > 99
def _get_special_info(self, number):
info = SpecialChannel()
if isinstance(number, str):
info.name = number
info.channel = self._SPECIAL_CHANNELS[number]
info.location = info.channel
else:
info.location = number
info.name = self._SPECIAL_CHANNELS_REV[number]
info.channel = info.location
return info
def _initialize(self):
self._classes["mem"] = IC7300MemFrame
self._rf.has_name = True
self._rf.has_dtcs = False
self._rf.has_dtcs_polarity = False
self._rf.has_bank = False
self._rf.has_tuning_step = False
self._rf.has_nostep_tuning = True
self._rf.can_odd_split = True
self._rf.memory_bounds = (1, 99)
self._rf.valid_modes = [
"LSB", "USB", "AM", "CW", "RTTY", "FM", "CWR", "RTTYR",
"Data+LSB", "Data+USB", "Data+AM", "N/A", "N/A", "Data+FM"
]
self._rf.valid_tmodes = ["", "Tone", "TSQL"]
# self._rf.valid_duplexes = ["", "-", "+", "split"]
self._rf.valid_duplexes = [] # To prevent using memobj.duplex
self._rf.valid_bands = [(30000, 74800000)]
self._rf.valid_skips = []
self._rf.valid_name_length = 10
self._rf.valid_characters = chirp_common.CHARSET_ASCII
self._rf.valid_special_chans = sorted(self._SPECIAL_CHANNELS.keys())
@directory.register
class Icom7610Radio(Icom7300Radio):
MODEL = "IC-7610"
_model = '\x98'
def _initialize(self):
super()._initialize()
self._classes['mem'] = IC7610MemFrame
@directory.register
class Icom9700Radio(IcomCIVRadio):
MODEL = 'IC-9700'
_model = '\xA2'
_template = 100
BANDS = {
1: (144, 148),
2: (430, 450),
3: (1240, 1300),
}
_MODES = [
"LSB", "USB", "AM", "CW", "RTTY", "FM", "CWR",
"RTTY-R", None, None, None, None, None, None, None, None, None,
"DV", None, None, None, None, "DD", None, None, None, None, None,
]
_CROSS_MODES = {
4: 'DTCS->',
5: 'Tone->DTCS',
6: 'DTCS->Tone',
7: 'Tone->Tone',
}
def get_sub_devices(self):
return [Icom9700RadioBand(self, 1),
Icom9700RadioBand(self, 2),
Icom9700RadioBand(self, 3),
Icom9700SatelliteBand(self)]
def _initialize(self):
super()._initialize()
self._rf.has_sub_devices = True
self._rf.memory_bounds = (1, 99)
self._classes['mem'] = IC9700MemFrame
def _decode_tmode(self, mem, memobj):
if int(memobj.tmode) in self._CROSS_MODES:
mem.tmode = 'Cross'
mem.cross_mode = self._CROSS_MODES[int(memobj.tmode)]
else:
return super()._decode_tmode(mem, memobj)
def _encode_tmode(self, mem, memobj):
cmr = {v: k for k, v in self._CROSS_MODES.items()}
if mem.tmode == 'Cross':
memobj.tmode = cmr[mem.cross_mode]
print('Setting tmode to %s for %s' % (memobj.tmode, mem.tmode))
else:
return super()._encode_tmode(mem, memobj)
class Icom9700RadioBand(Icom9700Radio):
_SPECIAL_CHANNELS = {
"1A": 100,
"1B": 101,
"2A": 102,
"2B": 103,
"3A": 104,
"4B": 105,
"C1": 106,
"C2": 107,
}
_SPECIAL_CHANNELS_REV = dict(zip(_SPECIAL_CHANNELS.values(),
_SPECIAL_CHANNELS.keys()))
def _detect_echo(self):
self._parent._willecho
def _is_special(self, number):
return isinstance(number, str) or number > 99
def _get_special_info(self, number):
info = BankSpecialChannel()
info.bank = self._band
if isinstance(number, str):
info.name = number
info.channel = self._SPECIAL_CHANNELS[number]
info.location = info.channel
else:
info.location = number
info.name = self._SPECIAL_CHANNELS_REV[number]
info.channel = info.location
return info
def __init__(self, parent, band):
self._parent = parent
self._band = band
self.VARIANT = '%i band' % (self.BANDS[band][0])
super().__init__(parent.pipe)
def mem_to_ch_bnk(self, mem):
return mem, self._band
def _initialize(self):
super()._initialize()
self._rf.has_name = True
self._rf.valid_tmodes = ['', 'Tone', 'TSQL', 'DTCS', 'Cross']
self._rf.valid_cross_modes = list(self._CROSS_MODES.values())
self._rf.has_dtcs = True
self._rf.has_dtcs_polarity = True
self._rf.has_bank = True
self._rf.has_tuning_step = False
self._rf.has_nostep_tuning = True
self._rf.can_odd_split = False
self._rf.memory_bounds = (1, 99)
self._rf.valid_bands = [(x * 1000000, y * 1000000) for x, y in
[self.BANDS[self._band]]]
self._rf.valid_name_length = 16
self._rf.valid_characters = (chirp_common.CHARSET_ALPHANUMERIC +
'!#$%&\\?"\'`^+-*/.,:=<>()[]{}|_~@')
self._rf.valid_special_chans = sorted(self._SPECIAL_CHANNELS.keys())
# Last item is RPS for DD mode
self._rf.valid_duplexes = ['', '-', '+']
self._rf.valid_modes = [x for x in self._MODES if x]
if self._band != 3:
self._rf.valid_modes.remove('DD')
self._classes['mem'] = IC9700MemFrame
class Icom9700SatelliteBand(Icom9700Radio):
VARIANT = 'Satellite'
def __init__(self, parent):
self._parent = parent
super().__init__(parent.pipe)
def _detect_echo(self):
self._parent._willecho
def _initialize(self):
super()._initialize()
self._rf.has_name = True
self._rf.valid_tmodes = ['', 'Tone', 'TSQL', 'DTCS']
self._rf.has_ctone = True
self._rf.has_dtcs = True
self._rf.has_dtcs_polarity = True
self._rf.has_bank = False
self._rf.has_tuning_step = False
self._rf.has_nostep_tuning = True
self._rf.can_odd_split = True
self._rf.memory_bounds = (1, 99)
self._rf.valid_bands = [(x * 1000000, y * 1000000) for x, y in
self.BANDS.values()]
self._rf.valid_name_length = 16
self._rf.valid_characters = (chirp_common.CHARSET_ALPHANUMERIC +
'!#$%&\\?"\'`^+-*/.,:=<>()[]{}|_~@')
# Last item is RPS for DD mode
self._rf.valid_duplexes = ['split']
self._rf.valid_modes = [x for x in self._MODES if x]
self._rf.valid_modes.remove('DD')
self._classes['mem'] = IC9700SatMemFrame
def _get_template_memory(self):
f = self._classes["mem"]()
f.initialize()
return f
def get_memory(self, number):
self._detect_baudrate()
LOG.debug("Getting %s" % number)
f = self._classes["mem"]()
mem = chirp_common.Memory()
f.set_location(number)
mem.number = number
self._send_frame(f)
f = self._recv_frame(f)
if len(f.get_data()) == 0:
raise errors.RadioError("Radio reported error")
if f.get_data() and f.get_data()[-1] == 0xFF:
mem.empty = True
mem.duplex = 'split'
LOG.debug("Found %i empty" % mem.number)
return mem
memobj = f.get_obj()
LOG.debug(repr(memobj))
mem.freq = int(memobj.freq)
mem.mode = self._MODES[int(memobj.mode)]
mem.name = str(memobj.name).rstrip()
mem.tmode = self._rf.valid_tmodes[memobj.tmode]
if memobj.dtcs_polarity == 0x11:
mem.dtcs_polarity = "RR"
elif memobj.dtcs_polarity == 0x10:
mem.dtcs_polarity = "RN"
elif memobj.dtcs_polarity == 0x01:
mem.dtcs_polarity = "NR"
else:
mem.dtcs_polarity = "NN"
mem.dtcs = int(memobj.dtcs)
mem.rtone = int(memobj.rtone) / 10.0
mem.ctone = int(memobj.ctone) / 10.0
mem.duplex = 'split'
mem.offset = int(memobj.tx.freq)
mem.immutable = ["duplex"]
try:
dig = RadioSetting("dig", "Digital",
RadioSettingValueBoolean(bool(memobj.dig)))
except AttributeError:
pass
else:
dig.set_doc("Enable digital mode")
if not mem.extra:
mem.extra = RadioSettingGroup("extra", "Extra")
mem.extra.append(dig)
options = ["Wide", "Mid", "Narrow"]
fil = RadioSetting(
"filter", "Filter",
RadioSettingValueList(options,
current_index=memobj.filter - 1))
fil.set_doc("Filter settings")
if not mem.extra:
mem.extra = RadioSettingGroup("extra", "Extra")
mem.extra.append(fil)
return mem
def set_memory(self, mem):
self._detect_baudrate()
LOG.debug("Setting %s(%s)" % (mem.number, mem.extd_number))
f = self._get_template_memory()
ch = mem.number
if mem.empty:
LOG.debug("Making %i empty" % mem.number)
f.set_location(ch)
f.make_empty()
self._send_frame(f)
f = self._recv_frame()
LOG.debug("Result (%r):\n%s",
f._cmd, util.hexprint(bytes(f.get_data())))
return
memobj = f.get_obj()
memobj.number = ch
memobj.freq = int(mem.freq)
mode = mem.mode
memobj.mode = self._MODES.index(mode)
name_length = len(memobj.name.get_value())
memobj.name = mem.name.ljust(name_length)[:name_length]
memobj.tmode = self._rf.valid_tmodes.index(mem.tmode)
memobj.ctone = int(mem.ctone * 10)
memobj.rtone = int(mem.rtone * 10)
if mem.dtcs_polarity == "RR":
memobj.dtcs_polarity = 0x11
elif mem.dtcs_polarity == "RN":
memobj.dtcs_polarity = 0x10
elif mem.dtcs_polarity == "NR":
memobj.dtcs_polarity = 0x01
else:
memobj.dtcs_polarity = 0x00
memobj.dtcs = mem.dtcs
memobj.tx.freq = int(mem.offset)
memobj.tx.tmode = memobj.tmode
memobj.tx.ctone = memobj.ctone
memobj.tx.rtone = memobj.rtone
memobj.tx.dtcs_polarity = memobj.dtcs_polarity
memobj.tx.dtcs = memobj.dtcs
memobj.urcall = memobj.rpt1call = memobj.rpt2call = ' ' * 8
memobj.tx.urcall = memobj.tx.rpt1call = memobj.tx.rpt2call = ' ' * 8
memobj.filter = memobj.tx.filter = 1
for setting in mem.extra:
if setting.get_name() == "filter":
setattr(memobj, setting.get_name(), int(setting.value) + 1)
else:
setattr(memobj, setting.get_name(), setting.value)
LOG.debug(repr(memobj))
self._send_frame(f)
f = self._recv_frame()
LOG.debug("Result (%r):\n%s",
f._cmd, util.hexprint(bytes(f.get_data())))
if f._cmd == 0xFA:
LOG.error('Radio refused memory')
def probe_model(ser):
"""Probe the radio attached to @ser for its model"""
f = Frame()
f.set_command(0x19, 0x00)
models = {}
for rclass in directory.DRV_TO_RADIO.values():
if issubclass(rclass, IcomCIVRadio):
models[rclass.MODEL] = rclass
for rclass in models.values():
model = ord(rclass._model)
f.send(model, 0xE0, ser)
try:
f.read(ser)
except errors.RadioError:
continue
if len(f.get_data()) == 1:
md = f.get_data()[0]
if (md == model):
return rclass
if f.get_data():
LOG.debug("Got data, but not 1 byte:")
LOG.debug(util.hexprint(bytes(f.get_data())))
raise errors.RadioError("Unknown response")
raise errors.RadioError("Unsupported model")
chirp-20261030/chirp/drivers/icp7.py 0000644 0001750 0001750 00000016554 15266622247 016060 0 ustar hibby hibby # Copyright 2017 SASANO Takayoshi (JG1UAA)
#
# 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.drivers import icf
from chirp import chirp_common, directory, bitwise
from chirp import errors
# memory number:
# 000 - 999 regular memory channels (supported, others not)
# 1000 - 1049 scan edges
# 1050 - 1249 auto write channels
# 1250 call channel (C0)
# 1251 call channel (C1)
MEM_FORMAT = """
struct {
ul32 freq;
ul32 offset;
ul16 train_sql:2,
tmode:3,
duplex:2,
train_tone:9;
ul16 tuning_step:4,
rtone:6,
ctone:6;
ul16 unknown0:6,
mode:3,
dtcs:7;
u8 unknown1:6,
dtcs_polarity:2;
char name[6];
} memory[1251];
#seekto 0x6b1e;
struct {
u8 bank;
u8 index;
} banks[1050];
#seekto 0x689e;
u8 used[132];
#seekto 0x6922;
u8 skips[132];
#seekto 0x69a6;
u8 pskips[132];
#seekto 0x7352;
struct {
char name[6];
} bank_names[18];
"""
MODES = ["FM", "WFM", "AM", "Auto"]
TMODES = ["", "Tone", "TSQL", "", "DTCS"]
DUPLEX = ["", "-", "+"]
DTCS_POLARITY = ["NN", "NR", "RN", "RR"]
TUNING_STEPS = [5.0, 6.25, 8.33, 9.0, 10.0, 12.5, 15.0, 20.0,
25.0, 30.0, 50.0, 100.0, 200.0, 0.0] # 0.0 as "Auto"
class ICP7Bank(icf.IcomBank):
"""ICP7 bank"""
def get_name(self):
_bank = self._model._radio._memobj.bank_names[self.index]
return str(_bank.name).rstrip()
def set_name(self, name):
_bank = self._model._radio._memobj.bank_names[self.index]
_bank.name = name.ljust(6)[:6]
@directory.register
class ICP7Radio(icf.IcomCloneModeRadio):
"""Icom IC-P7"""
VENDOR = "Icom"
MODEL = "IC-P7"
_model = "\x28\x69\x00\x01"
_memsize = 0x7500
_endframe = "Icom Inc\x2e\x41\x38"
_ranges = [(0x0000, 0x7500, 32)]
_num_banks = 18
_bank_class = ICP7Bank
_can_hispeed = True
@classmethod
def match_model(cls, filedata, filename):
return (super().match_model(filedata, filename) or
(cls._memsize and len(filedata) == cls._memsize))
def _get_bank(self, loc):
_bank = self._memobj.banks[loc]
if _bank.bank != 0xff:
return _bank.bank
else:
return None
def _set_bank(self, loc, bank):
_bank = self._memobj.banks[loc]
if bank is None:
_bank.bank = 0xff
else:
_bank.bank = bank
def _get_bank_index(self, loc):
_bank = self._memobj.banks[loc]
return _bank.index
def _set_bank_index(self, loc, index):
_bank = self._memobj.banks[loc]
_bank.index = index
def get_features(self):
rf = chirp_common.RadioFeatures()
rf.memory_bounds = (0, 999)
rf.valid_tmodes = TMODES
rf.valid_duplexes = DUPLEX
rf.valid_modes = MODES
rf.valid_bands = [(495000, 999990000)]
rf.valid_skips = ["", "S", "P"]
rf.valid_tuning_steps = [x for x in TUNING_STEPS if x]
rf.valid_name_length = 6
rf.has_settings = True
rf.has_ctone = True
rf.has_bank = True
rf.has_bank_index = True
rf.has_bank_names = True
return rf
def process_mmap(self):
self._memobj = bitwise.parse(MEM_FORMAT, self._mmap)
def get_raw_memory(self, number):
return repr(self._memobj.memory[number])
def get_memory(self, number):
bit = 1 << (number % 8)
byte = int(number / 8)
_mem = self._memobj.memory[number]
_usd = self._memobj.used[byte]
_skp = self._memobj.skips[byte]
_psk = self._memobj.pskips[byte]
mem = chirp_common.Memory()
mem.number = number
if _usd & bit:
mem.empty = True
return mem
mem.freq = _mem.freq // 3
mem.offset = _mem.offset // 3
mem.tmode = TMODES[_mem.tmode]
mem.duplex = DUPLEX[_mem.duplex]
mem.tuning_step = TUNING_STEPS[_mem.tuning_step]
if mem.tuning_step == 0:
mem.tuning_step = self._valid_tuning_step(mem)
mem.rtone = chirp_common.TONES[_mem.rtone]
mem.ctone = chirp_common.TONES[_mem.ctone]
mem.mode = MODES[_mem.mode]
mem.dtcs = chirp_common.DTCS_CODES[_mem.dtcs]
mem.dtcs_polarity = DTCS_POLARITY[_mem.dtcs_polarity]
mem.name = str(_mem.name).rstrip()
if _skp & bit:
mem.skip = "P" if _psk & bit else "S"
else:
mem.skip = ""
return mem
def _valid_tuning_step(self, mem):
try:
return chirp_common.required_step(
mem.freq, self.get_features().valid_tuning_steps)
except errors.InvalidDataError:
return self.get_features().valid_tuning_steps[0]
def set_memory(self, mem):
bit = 1 << (mem.number % 8)
byte = int(mem.number / 8)
_mem = self._memobj.memory[mem.number]
_usd = self._memobj.used[byte]
_skp = self._memobj.skips[byte]
_psk = self._memobj.pskips[byte]
if mem.empty:
_usd |= bit
# We use default value instead of zero-fill
# to avoid unexpected behavior.
_mem.freq = 15000
_mem.offset = 479985000
_mem.train_sql = ~0
_mem.tmode = ~0
_mem.duplex = ~0
_mem.train_tone = ~0
_mem.tuning_step = ~0
_mem.rtone = ~0
_mem.ctone = ~0
_mem.unknown0 = 0
_mem.mode = ~0
_mem.dtcs = ~0
_mem.unknown1 = ~0
_mem.dtcs_polarity = ~0
_mem.name = " "
_skp |= bit
_psk |= bit
else:
_usd &= ~bit
_mem.freq = mem.freq * 3
_mem.offset = mem.offset * 3
_mem.train_sql = 0 # Train SQL mode (0:off 1:Tone 2:MSK)
_mem.tmode = TMODES.index(mem.tmode)
_mem.duplex = DUPLEX.index(mem.duplex)
_mem.train_tone = 228 # Train SQL Tone (x10 Hz)
tuning_step = mem.tuning_step
if tuning_step == 0:
tuning_step = self._valid_tuning_step(mem)
_mem.tuning_step = TUNING_STEPS.index(tuning_step)
_mem.rtone = chirp_common.TONES.index(mem.rtone)
_mem.ctone = chirp_common.TONES.index(mem.ctone)
_mem.unknown0 = 0 # unknown (always zero)
_mem.mode = MODES.index(mem.mode)
_mem.dtcs = chirp_common.DTCS_CODES.index(mem.dtcs)
_mem.unknown1 = ~0 # unknown (always one)
_mem.dtcs_polarity = DTCS_POLARITY.index(mem.dtcs_polarity)
_mem.name = mem.name.ljust(6)[:6]
if mem.skip == "S":
_skp |= bit
_psk &= ~bit
elif mem.skip == "P":
_skp |= bit
_psk |= bit
else:
_skp &= ~bit
_psk &= ~bit
chirp-20261030/chirp/drivers/icq7.py 0000644 0001750 0001750 00000027223 15257672455 016062 0 ustar hibby hibby # 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 logging
from chirp.drivers import icf
from chirp import chirp_common, directory, bitwise
from chirp.chirp_common import to_GHz
from chirp.settings import RadioSetting, RadioSettingGroup, \
RadioSettingValueBoolean, RadioSettingValueList, \
RadioSettingValueInteger, RadioSettings
LOG = logging.getLogger(__name__)
MEM_FORMAT = """
struct {
bbcd freq[3];
u8 fractional:1,
unknown:7;
bbcd offset[2];
u16 ctone:6,
rtone:6,
tune_step:4;
} memory[200];
#seekto 0x0690;
struct {
u8 tmode:2,
duplex:2,
skip:1,
pskip:1,
mode:2;
} flags[200];
#seekto 0x0690;
u8 flags_whole[200];
#seekto 0x0767;
struct {
i8 rit;
u8 squelch;
u8 lock:1,
ritfunct:1,
unknown:6;
u8 unknown1[6];
u8 d_sel;
u8 autorp;
u8 priority;
u8 resume;
u8 pause;
u8 p_scan;
u8 bnk_scan;
u8 expand;
u8 ch;
u8 beep;
u8 light;
u8 ap_off;
u8 p_save;
u8 monitor;
u8 speed;
u8 edge;
u8 lockgroup;
} settings;
"""
TMODES = ["", "", "Tone", "TSQL", "TSQL"] # last one is pocket beep
DUPLEX = ["", "", "-", "+"]
MODES = ["FM", "WFM", "AM", "Auto"]
STEPS = [5.0, 6.25, 10.0, 12.5, 15.0, 20.0, 25.0, 30.0, 50.0, 100.0]
AUTORP_LIST = ["Off", "Duplex Only", "Duplex and Tone"]
LOCKGROUP_LIST = ["Normal", "No Squelch", "No Volume", "All"]
SQUELCH_LIST = ["Open", "Auto"] + ["L%s" % x for x in range(1, 10)]
MONITOR_LIST = ["Push", "Hold"]
LIGHT_LIST = ["Off", "On", "Auto"]
PRIORITY_LIST = ["Off", "On", "Bell"]
BANKSCAN_LIST = ["Off", "Bank 0", "Bank 1"]
EDGE_LIST = ["%sP" % x for x in range(0, 20)] + ["Band", "All"]
PAUSE_LIST = ["%s sec" % x for x in range(2, 22, 2)] + ["Hold"]
RESUME_LIST = ["%s sec" % x for x in range(0, 6)]
APOFF_LIST = ["Off"] + ["%s min" % x for x in range(30, 150, 30)]
D_SEL_LIST = ["100 kHz", "1 MHz", "10 MHz"]
@directory.register
class ICQ7Radio(icf.IcomCloneModeRadio):
"""Icom IC-Q7A"""
VENDOR = "Icom"
MODEL = "IC-Q7A"
_model = "\x19\x95\x00\x01"
_memsize = 0x7C0
_endframe = "Icom Inc\x2e"
_ranges = [(0x0000, 0x07C0, 16)]
@classmethod
def match_model(cls, filedata, filename):
return (super().match_model(filedata, filename) or
(cls._memsize and len(filedata) == cls._memsize))
def get_features(self):
rf = chirp_common.RadioFeatures()
rf.has_settings = True
rf.memory_bounds = (0, 199)
rf.valid_modes = list(MODES)
rf.valid_tmodes = list(TMODES)
rf.valid_duplexes = list(DUPLEX)
rf.valid_tuning_steps = list(STEPS)
rf.valid_bands = [(1000000, 823995000),
(849000000, 868995000),
(894000000, 1309995000)]
rf.valid_skips = ["", "S", "P"]
rf.has_dtcs = False
rf.has_dtcs_polarity = False
rf.has_bank = False
rf.has_name = False
return rf
def process_mmap(self):
self._memobj = bitwise.parse(MEM_FORMAT, self._mmap)
def get_raw_memory(self, number):
return (repr(self._memobj.memory[number]) +
repr(self._memobj.flags[number]))
def validate_memory(self, mem):
if mem.freq < 30000000 and mem.mode != 'AM':
return [chirp_common.ValidationError(
'Only AM is allowed below 30 MHz')]
return icf.IcomCloneModeRadio.validate_memory(self, mem)
def get_memory(self, number):
_mem = self._memobj.memory[number]
_flag = self._memobj.flags[number]
mem = chirp_common.Memory()
mem.number = number
if self._memobj.flags_whole[number] == 0xFF:
mem.empty = True
return mem
mem.freq = int(_mem.freq) * 1000
if _mem.fractional:
mem.freq = chirp_common.fix_rounded_step(mem.freq)
mem.offset = int(_mem.offset) * 1000
try:
mem.rtone = chirp_common.TONES[_mem.rtone]
except IndexError:
mem.rtone = 88.5
try:
mem.ctone = chirp_common.TONES[_mem.ctone]
except IndexError:
mem.ctone = 88.5
try:
mem.tuning_step = STEPS[_mem.tune_step]
except IndexError:
LOG.error("Invalid tune step index %i" % _mem.tune_step)
mem.tmode = TMODES[_flag.tmode]
mem.duplex = DUPLEX[_flag.duplex]
if mem.freq < 30000000:
mem.mode = "AM"
else:
mem.mode = MODES[_flag.mode]
if _flag.pskip:
mem.skip = "P"
elif _flag.skip:
mem.skip = "S"
return mem
def set_memory(self, mem):
_mem = self._memobj.memory[mem.number]
_flag = self._memobj.flags[mem.number]
if mem.empty:
self._memobj.flags_whole[mem.number] = 0xFF
return
_mem.set_raw("\x00" * 8)
if mem.freq > to_GHz(1):
_mem.freq = (mem.freq // 1000) - to_GHz(1)
# For 1300MHz, but 13 in these bits
upper = (mem.freq // 100000000) << 4
_mem.freq[0].clr_bits(0xF0)
_mem.freq[0].set_bits(upper)
else:
_mem.freq = mem.freq / 1000
_mem.fractional = chirp_common.is_fractional_step(mem.freq)
_mem.offset = mem.offset / 1000
_mem.rtone = chirp_common.TONES.index(mem.rtone)
_mem.ctone = chirp_common.TONES.index(mem.ctone)
_mem.tune_step = STEPS.index(mem.tuning_step)
_flag.tmode = TMODES.index(mem.tmode)
_flag.duplex = DUPLEX.index(mem.duplex)
_flag.mode = MODES.index(mem.mode)
_flag.skip = mem.skip == "S" and 1 or 0
_flag.pskip = mem.skip == "P" and 1 or 0
def get_settings(self):
_settings = self._memobj.settings
basic = RadioSettingGroup("basic", "Basic Settings")
group = RadioSettings(basic)
rs = RadioSetting("ch", "Channel Indication Mode",
RadioSettingValueBoolean(_settings.ch))
basic.append(rs)
rs = RadioSetting("expand", "Expanded Settings Mode",
RadioSettingValueBoolean(_settings.expand))
basic.append(rs)
rs = RadioSetting("beep", "Beep Tones",
RadioSettingValueBoolean(_settings.beep))
basic.append(rs)
rs = RadioSetting("autorp", "Auto Repeater Function",
RadioSettingValueList(
AUTORP_LIST, current_index=_settings.autorp))
basic.append(rs)
rs = RadioSetting("ritfunct", "RIT Runction",
RadioSettingValueBoolean(_settings.ritfunct))
basic.append(rs)
rs = RadioSetting("rit", "RIT Shift (kHz)",
RadioSettingValueInteger(-7, 7, _settings.rit))
basic.append(rs)
rs = RadioSetting("lock", "Lock",
RadioSettingValueBoolean(_settings.lock))
basic.append(rs)
rs = RadioSetting("lockgroup", "Lock Group",
RadioSettingValueList(
LOCKGROUP_LIST,
current_index=_settings.lockgroup))
basic.append(rs)
rs = RadioSetting("squelch", "Squelch",
RadioSettingValueList(
SQUELCH_LIST, current_index=_settings.squelch))
basic.append(rs)
rs = RadioSetting("monitor", "Monitor Switch Function",
RadioSettingValueList(
MONITOR_LIST,
current_index=_settings.monitor))
basic.append(rs)
rs = RadioSetting("light", "Display Backlighting",
RadioSettingValueList(
LIGHT_LIST, current_index=_settings.light))
basic.append(rs)
rs = RadioSetting("priority", "Priority Watch Operation",
RadioSettingValueList(
PRIORITY_LIST,
current_index=_settings.priority))
basic.append(rs)
rs = RadioSetting("p_scan", "Frequency Skip Function",
RadioSettingValueBoolean(_settings.p_scan))
basic.append(rs)
rs = RadioSetting("bnk_scan", "Memory Bank Scan Selection",
RadioSettingValueList(
BANKSCAN_LIST,
current_index=_settings.bnk_scan))
basic.append(rs)
rs = RadioSetting("edge", "Band Edge Scan Selection",
RadioSettingValueList(
EDGE_LIST, current_index=_settings.edge))
basic.append(rs)
rs = RadioSetting("pause", "Scan Pause Time",
RadioSettingValueList(
PAUSE_LIST, current_index=_settings.pause))
basic.append(rs)
rs = RadioSetting("resume", "Scan Resume Time",
RadioSettingValueList(
RESUME_LIST, current_index=_settings.resume))
basic.append(rs)
rs = RadioSetting("p_save", "Power Saver",
RadioSettingValueBoolean(_settings.p_save))
basic.append(rs)
rs = RadioSetting("ap_off", "Auto Power-off Function",
RadioSettingValueList(
APOFF_LIST, current_index=_settings.ap_off))
basic.append(rs)
rs = RadioSetting("speed", "Dial Speed Acceleration",
RadioSettingValueBoolean(_settings.speed))
basic.append(rs)
rs = RadioSetting("d_sel", "Dial Select Step",
RadioSettingValueList(
D_SEL_LIST, current_index=_settings.d_sel))
basic.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
chirp-20261030/chirp/drivers/ict10.py 0000644 0001750 0001750 00000041315 14767271777 016144 0 ustar hibby hibby # 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 logging
from chirp.drivers import icf
from chirp import chirp_common, bitwise, errors, directory
from chirp.settings import RadioSetting, RadioSettingGroup, \
RadioSettingValueInteger, RadioSettingValueList, \
RadioSettingValueBoolean, RadioSettingValueString, \
RadioSettings
LOG = logging.getLogger(__name__)
ICV86_MEM_FORMAT = """
#seekto 0x020;
lbit pskips[208];
#seekto 0x040;
lbit skips[208];
#seekto 0x060;
lbit used[208];
struct {
#seekto 0x010;
char comment[16];
#seekto 0x0FD;
u8 wx_channel;
#seekto 0x100;
u8 reserved1:6,
beep:2;
u8 reserved2:3,
tot:5;
u8 reserved3;
u8 reserved4:6,
auto_pwr:2;
u8 reserved5:6,
lockout:2;
u8 reserved6:7,
sql_delay:1;
u8 reserved7;
u8 reserved8:6,
disp_type:2;
u8 reserved9;
u8 reserved10:5,
pwr_save:3;
u8 reserved11:7,
dial_speedup:1;
u8 reserved12:6,
simple_mode:2;
u8 reserved13:7,
ant:1;
u8 reserved14:7,
monitor:1;
u8 reserved15;
u8 reserved16;
u8 reserved17:6,
pause_timer:2;
u8 reserved18;
u8 reserved19:7,
skip_scan:1;
u8 reserved20:6,
lcd:2;
u8 reserved21:7,
wx:1;
u8 reserved22:6,
mic:2;
u8 reserved23:4,
vox_gain:4;
u8 reserved24:5,
vox_delay:3;
u8 reserved25:5,
vox_tot:3;
u8 reserved26;
u8 reserved27;
u8 reserved28:5,
edge:3;
u8 reserved29;
u8 reserved30:7,
auto_low:1;
u8 reserved31:7,
volt:1;
u8 reserved32:7,
ctcss_burst:1;
u8 reserved33;
u8 reserved34;
u8 reserved35;
u8 reserved36:7,
vox:1;
u8 reserved37;
u8 reserved38;
u8 reserved39:6,
p_short:2;
u8 reserved40:6,
p_long:2;
} settings;
#seekto 0x0240;
struct {
ul32 freq;
ul32 offset;
char name[6];
u8 reserved1:2,
rtone:6;
u8 reserved2:2,
ctone:6;
u8 reserved3:1,
dtcs:7;
u8 reserved4:4,
tuning_step:4;
u8 reserved5:2,
mode:1,
rev:1,
duplex:2,
reserved6:2;
u8 reserved7:2,
dtcs_polarity:2,
tmode:4;
u8 reserved8:5,
tx_inhibit:1,
power:2;
u8 reserved9;
u8 reserved10;
u8 reserved11;
} memory[208];
"""
SPECIAL = {
"1A": 200, "1B": 201,
"2A": 202, "2B": 203,
"3A": 204, "3B": 205,
"C0": 206, "C1": 207,
}
TMODES = ["", "Tone", "TSQL", "DTCS"]
MODES = ["FM", "NFM"]
SKIPS = ["", "S", "P"]
DUPLEXES = ["", "-", "+"]
DTCS_POLARITY = ["NN", "NR", "RN", "RR"]
TUNING_STEPS = [5., 10., 12.5, 15., 20., 25., 30., 50., 100., 125., 200.]
POWER_LEVELS = [
chirp_common.PowerLevel("High", watts=5.0),
chirp_common.PowerLevel("Low", watts=0.5),
chirp_common.PowerLevel("Mid", watts=2.5),
]
@directory.register
class ICVT10Radio(icf.IcomCloneModeRadio):
"""Icom IC-T10"""
VENDOR = "Icom"
MODEL = "IC-T10"
_model = "\x43\x28\x00\x01"
_memsize = 5888
_endframe = "Icom Inc\x2eB8"
_ranges = [(0x0000, 5888, 32)]
def get_features(self):
rf = chirp_common.RadioFeatures()
rf.memory_bounds = (0, 199)
rf.valid_modes = MODES
rf.valid_tmodes = TMODES
rf.valid_duplexes = DUPLEXES
rf.valid_tuning_steps = TUNING_STEPS
rf.valid_power_levels = POWER_LEVELS
rf.valid_skips = SKIPS
rf.valid_name_length = 6
rf.valid_special_chans = list(SPECIAL.keys())
rf.valid_bands = [(136000000, 174000000), (400000000, 479000000)]
rf.has_ctone = True
rf.has_offset = True
rf.has_bank = False
rf.has_settings = True
return rf
def __init__(self, pipe):
icf.IcomCloneModeRadio.__init__(self, pipe)
def sync_in(self):
icf.IcomCloneModeRadio.sync_in(self)
def sync_out(self):
icf.IcomCloneModeRadio.sync_out(self)
def process_mmap(self):
self._memobj = bitwise.parse(ICV86_MEM_FORMAT, self._mmap)
def get_settings(self):
_settings = self._memobj.settings
setmode = RadioSettingGroup("setmode", "Set Mode")
assign = RadioSettingGroup("assign", "Key Assign")
display = RadioSettingGroup("display", "Display")
sounds = RadioSettingGroup("sounds", "Sounds")
scan = RadioSettingGroup("Scan", "Scan")
fm = RadioSettingGroup("fm", "FM Radio")
wx = RadioSettingGroup("wx", "WX")
settings = RadioSettings(
setmode, assign, display, sounds, scan, fm, wx)
# Timeout Timer
opts = ["Off"] + ["%s min" % x for x in range(1, 31)]
setmode.append(
RadioSetting(
"tot", "Timeout Timer",
RadioSettingValueList(opts, current_index=_settings.tot)))
# Auto Power Off
opts = ["Off", "30 min", "1 hr", "2 hrs"]
setmode.append(
RadioSetting(
"auto_pwr", "Auto Power Off",
RadioSettingValueList(opts, current_index=_settings.auto_pwr)))
# Lockout
opts = ["Off", "Rpt", "Busy"]
setmode.append(
RadioSetting(
"lockout", "Lockout",
RadioSettingValueList(opts, current_index=_settings.lockout)))
# Squelch Delay
opts = ["Short", "Long"]
setmode.append(
RadioSetting(
"sql_delay", "Squelch Delay",
RadioSettingValueList(
opts, current_index=_settings.sql_delay)))
# Power Save
opts = ["Off", "1:2", "1:8", "1:16", "Auto"]
setmode.append(
RadioSetting(
"pwr_save", "Power Save",
RadioSettingValueList(opts, current_index=_settings.pwr_save)))
# Dial Speed-up
setmode.append(
RadioSetting(
"dial_speedup", "Dial Speed-Up",
RadioSettingValueBoolean(bool(_settings.dial_speedup))))
# Mic Simple Mode
opts = ["Simple", "Normal-1", "Normal-2"]
setmode.append(
RadioSetting(
"simple_mode", "Mic Simple Mode",
RadioSettingValueList(
opts, current_index=_settings.simple_mode)))
# Mic Gain
rs = RadioSetting(
"mic", "Mic Gain",
RadioSettingValueInteger(1, 4, _settings.mic + 1))
rs.set_apply_callback(
lambda s, obj: setattr(obj, s.get_name(), int(s.value) - 1),
self._memobj.settings)
setmode.append(rs)
# Auto Low Power
setmode.append(
RadioSetting(
"auto_low", "Auto Low Power",
RadioSettingValueBoolean(bool(_settings.auto_low))))
# Vox Function
setmode.append(
RadioSetting(
"vox", "Vox Function",
RadioSettingValueBoolean(bool(_settings.vox))))
# Vox Gain
opts = ["Off"] + ["%s" % x for x in range(1, 11)]
setmode.append(
RadioSetting(
"vox_gain", "Vox Gain",
RadioSettingValueList(
opts, current_index=_settings.vox_gain)))
# Vox Delay
opts = ["0.5 seconds", "1.0 seconds", "1.5 seconds", "2.0 seconds",
"2.5 seconds", "3.0 seconds"]
setmode.append(
RadioSetting(
"vox_delay", "Vox Delay",
RadioSettingValueList(
opts, current_index=_settings.vox_delay)))
# VOX Time out Timer
opts = ["Off", "1 min", "2 min", "3 min", "4 min", "5 min",
"10 min", "15 min"]
setmode.append(
RadioSetting(
"vox_tot", "VOX Time-Out Timer",
RadioSettingValueList(opts, current_index=_settings.vox_tot)))
# CTCSS Burst
setmode.append(
RadioSetting(
"ctcss_burst", "CTCSS Burst",
RadioSettingValueBoolean(bool(_settings.ctcss_burst))))
# Monitor
opts = ["Push", "Hold"]
setmode.append(
RadioSetting(
"monitor", "Monitor",
RadioSettingValueList(opts, current_index=_settings.monitor)))
# Comment
setmode.append(
RadioSetting(
"comment", "Comment",
RadioSettingValueString(
0, 16, str(_settings.comment).rstrip(), autopad=False)))
# P (Short)
opts = ["Null", "TS", "MHz", "T-CALL"]
assign.append(
RadioSetting(
"p_short", "P (Short)",
RadioSettingValueList(opts, current_index=_settings.p_short)))
# P (Long)
opts = ["Null", "TS", "MHz", "T-CALL"]
assign.append(
RadioSetting(
"p_long", "P (Short)",
RadioSettingValueList(opts, current_index=_settings.p_long)))
# Backlight
opts = ["Off", "On", "Auto"]
display.append(
RadioSetting(
"lcd", "Backlight",
RadioSettingValueList(opts, current_index=_settings.lcd)))
# Display Type
opts = ["Frequency", "Channel", "Name"]
display.append(
RadioSetting(
"disp_type", "Display Type",
RadioSettingValueList(
opts, current_index=_settings.disp_type)))
# Voltage Indicator
opts = ["Off", "On"]
display.append(
RadioSetting(
"volt", "Voltage Indicator",
RadioSettingValueList(opts, current_index=_settings.volt)))
# Timeout Timer
opts = ["Off"] + ["%d" % x for x in range(1, 4)]
sounds.append(
RadioSetting(
"beep", "Beep Level",
RadioSettingValueList(opts, current_index=_settings.beep)))
# Program Skip Scan
scan.append(
RadioSetting(
"skip_scan", "Program Skip Scan",
RadioSettingValueBoolean(bool(_settings.skip_scan))))
# Edge
opts = ["All", "Band", "P1", "P2", "P3"]
scan.append(
RadioSetting(
"edge", "Edge",
RadioSettingValueList(opts, current_index=_settings.edge)))
# Pause Timer
opts = ["T-5", "T-10", "T-15", "P-2"]
scan.append(
RadioSetting(
"pause_timer", "Pause Timer",
RadioSettingValueList(
opts, current_index=_settings.pause_timer)))
# Earphone Antenna
opts = ["Not Used", "Use"]
fm.append(
RadioSetting(
"ant", "Earphone Antenna",
RadioSettingValueList(opts, current_index=_settings.ant)))
# WX Alert
wx.append(
RadioSetting(
"wx", "WX Alert",
RadioSettingValueBoolean(bool(_settings.wx))))
# Mic Gain
rs = RadioSetting(
"wx_channel", "WX Channel",
RadioSettingValueInteger(1, 10, _settings.wx_channel + 1))
rs.set_apply_callback(
lambda s, obj: setattr(obj, s.get_name(), int(s.value) - 1),
self._memobj.settings)
wx.append(rs)
return settings
def set_settings(self, settings):
_settings = self._memobj.settings
for element in settings:
if not isinstance(element, RadioSetting):
self.set_settings(element)
continue
if not element.changed():
continue
try:
if element.has_apply_callback():
LOG.debug("Using apply callback")
element.run_apply_callback()
else:
setting = element.get_name()
LOG.debug("Setting %s = %s" % (setting, element.value))
setattr(_settings, setting, element.value)
except Exception:
LOG.debug(element.get_name())
raise
def _get_extra(self, mem):
ext = RadioSettingGroup("extra", "Extra")
rev = RadioSetting("rev", "Reverse duplex",
RadioSettingValueBoolean(bool(mem.rev)))
rev.set_doc("Reverse duplex")
ext.append(rev)
tx_inhibit = RadioSetting("tx_inhibit", "Tx inhibit",
RadioSettingValueBoolean(
bool(mem.tx_inhibit)))
tx_inhibit.set_doc("Tx inhibit")
ext.append(tx_inhibit)
return ext
def _get_memory(self, number, extd_number):
mem = chirp_common.Memory()
mem.number = number
_mem = self._memobj.memory[number]
if extd_number is None:
_usd = self._memobj.used[number]
_skp = self._memobj.skips[number]
_psk = self._memobj.pskips[number]
else:
mem.extd_number = extd_number
mem.immutable = ["number", "extd_number"]
if (number >= 206):
mem.immutable.append("skip")
_usd = self._memobj.used[number] if (number < 206) else None
_skp = self._memobj.skips[number] if (number < 206) else None
_psk = self._memobj.pskips[number] if (number < 206) else None
if _usd is not None and _usd:
mem.empty = True
return mem
mem.freq = _mem.freq
mem.offset = int(_mem.offset)
mem.name = str(_mem.name).rstrip()
mem.rtone = chirp_common.TONES[_mem.rtone]
mem.ctone = chirp_common.TONES[_mem.ctone]
mem.dtcs = chirp_common.DTCS_CODES[_mem.dtcs]
mem.tuning_step = TUNING_STEPS[_mem.tuning_step]
mem.mode = MODES[_mem.mode]
mem.duplex = DUPLEXES[_mem.duplex]
mem.dtcs_polarity = DTCS_POLARITY[_mem.dtcs_polarity]
mem.tmode = TMODES[_mem.tmode]
mem.power = POWER_LEVELS[_mem.power]
mem.extra = self._get_extra(_mem)
if _skp is not None:
mem.skip = (_psk and "P") or (_skp and "S") or ""
return mem
def get_memory(self, number):
extd_number = None
if isinstance(number, str):
try:
extd_number = number
number = dict(SPECIAL)[number]
except KeyError:
raise errors.InvalidMemoryLocation(
"Unknown channel %s" % number)
return self._get_memory(number, extd_number)
def _fill_memory(self, number):
_mem = self._memobj.memory[number]
# Default values
_mem.fill_raw(b'\x00')
_mem.freq = 146010000
_mem.offset = 146010000
_mem.name = str("").ljust(6)
_mem.rtone = 0x8
_mem.ctone = 0x8
def _set_memory(self, mem):
_mem = self._memobj.memory[mem.number]
_usd = self._memobj.used[mem.number] if mem.number < 206 else None
_skp = self._memobj.skips[mem.number] if mem.number < 206 else None
_psk = self._memobj.pskips[mem.number] if mem.number < 206 else None
if mem.empty:
self._fill_memory(mem.number)
if _usd is not None:
_usd |= 1
return
if _usd is None or _usd:
self._fill_memory(mem.number)
_mem.freq = mem.freq
_mem.offset = int(mem.offset)
_mem.name = str(mem.name).ljust(6)
_mem.rtone = chirp_common.TONES.index(mem.rtone)
_mem.ctone = chirp_common.TONES.index(mem.ctone)
_mem.dtcs = chirp_common.DTCS_CODES.index(mem.dtcs)
_mem.tuning_step = TUNING_STEPS.index(mem.tuning_step)
_mem.mode = MODES.index(mem.mode)
_mem.duplex = DUPLEXES.index(mem.duplex)
_mem.dtcs_polarity = DTCS_POLARITY.index(mem.dtcs_polarity)
_mem.tmode = TMODES.index(mem.tmode)
_mem.power = 0 if mem.power is None else POWER_LEVELS.index(mem.power)
for setting in mem.extra:
setattr(_mem, setting.get_name(), setting.value)
if _usd is not None:
_usd &= 0
if _skp is not None:
_skp &= 0
_psk &= 0
if mem.skip == "S":
_skp |= 1
elif mem.skip == "P":
_psk |= 1
def set_memory(self, mem):
return self._set_memory(mem)
def get_raw_memory(self, number):
return repr(self._memobj.memory[number]) + \
repr(self._memobj.used[(number)])
chirp-20261030/chirp/drivers/ict70.py 0000644 0001750 0001750 00000014473 15257672455 016150 0 ustar hibby hibby # Copyright 2011 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.drivers import icf
from chirp import chirp_common, directory, bitwise
MEM_FORMAT = """
struct {
u24 freq;
ul16 offset;
char name[6];
u8 unknown2:2,
rtone:6;
u8 unknown3:2,
ctone:6;
u8 unknown4:1,
dtcs:7;
u8 tuning_step:4,
narrow:1,
unknown5:1,
duplex:2;
u8 unknown6:1,
power:2,
dtcs_polarity:2,
tmode:3;
} memory[300];
#seekto 0x12E0;
u8 used[38];
#seekto 0x1306;
u8 skips[38];
#seekto 0x132C;
u8 pskips[38];
#seekto 0x1360;
struct {
u8 bank;
u8 index;
} banks[300];
#seekto 0x16D0;
struct {
char name[6];
} bank_names[26];
"""
TMODES = ["", "Tone", "TSQL", "TSQL", "DTCS", "DTCS"]
DUPLEX = ["", "-", "+"]
DTCS_POLARITY = ["NN", "NR", "RN", "RR"]
TUNING_STEPS = [5.0, 5.0, 5.0, 5.0, 10.0, 12.5, 15.0, 20.0, 25.0, 30.0,
50.0, 100.0, 125.0, 200.0]
POWER_LEVELS = [chirp_common.PowerLevel("High", watts=5),
chirp_common.PowerLevel("Low", watts=0.5),
chirp_common.PowerLevel("Mid", watts=1.0),
]
class ICT70Bank(icf.IcomBank):
"""ICT70 bank"""
def get_name(self):
_bank = self._model._radio._memobj.bank_names[self.index]
return str(_bank.name).rstrip()
def set_name(self, name):
_bank = self._model._radio._memobj.bank_names[self.index]
_bank.name = name.ljust(6)[:6]
@directory.register
class ICT70Radio(icf.IcomCloneModeRadio):
"""Icom IC-T70"""
VENDOR = "Icom"
MODEL = "IC-T70"
_model = "\x32\x53\x00\x01"
_memsize = 0x19E0
_endframe = "Icom Inc\x2eCF"
_ranges = [(0x0000, 0x19E0, 32)]
@classmethod
def match_model(cls, filedata, filename):
return (super().match_model(filedata, filename) or
(cls._memsize and len(filedata) == cls._memsize))
_num_banks = 26
_bank_class = ICT70Bank
def _get_bank(self, loc):
_bank = self._memobj.banks[loc]
if _bank.bank != 0xFF:
return _bank.bank
else:
return None
def _set_bank(self, loc, bank):
_bank = self._memobj.banks[loc]
if bank is None:
_bank.bank = 0xFF
else:
_bank.bank = bank
def _get_bank_index(self, loc):
_bank = self._memobj.banks[loc]
return _bank.index
def _set_bank_index(self, loc, index):
_bank = self._memobj.banks[loc]
_bank.index = index
def get_features(self):
rf = chirp_common.RadioFeatures()
rf.memory_bounds = (0, 299)
rf.valid_tmodes = TMODES
rf.valid_duplexes = DUPLEX
rf.valid_power_levels = POWER_LEVELS
rf.valid_modes = ["FM", "NFM"]
rf.valid_bands = [(136000000, 174000000), (400000000, 479000000)]
rf.valid_skips = ["", "S", "P"]
rf.valid_tuning_steps = TUNING_STEPS
rf.valid_name_length = 6
rf.has_ctone = True
rf.has_bank = True
rf.has_bank_index = True
rf.has_bank_names = True
return rf
def process_mmap(self):
self._memobj = bitwise.parse(MEM_FORMAT, self._mmap)
def get_raw_memory(self, number):
return repr(self._memobj.memory[number])
def get_memory(self, number):
bit = 1 << (number % 8)
byte = int(number / 8)
_mem = self._memobj.memory[number]
_usd = self._memobj.used[byte]
_skp = self._memobj.skips[byte]
_psk = self._memobj.pskips[byte]
mem = chirp_common.Memory()
mem.number = number
if _usd & bit:
mem.empty = True
return mem
if _mem.freq & 0x800000:
mem.freq = (_mem.freq & ~0x800000) * 6250
else:
mem.freq = _mem.freq * 5000
mem.offset = _mem.offset * 5000
mem.name = str(_mem.name).rstrip()
mem.rtone = chirp_common.TONES[_mem.rtone]
mem.ctone = chirp_common.TONES[_mem.ctone]
mem.dtcs = chirp_common.DTCS_CODES[_mem.dtcs]
mem.tuning_step = TUNING_STEPS[_mem.tuning_step]
mem.mode = _mem.narrow and "NFM" or "FM"
mem.duplex = DUPLEX[_mem.duplex]
mem.power = POWER_LEVELS[_mem.power]
mem.dtcs_polarity = DTCS_POLARITY[_mem.dtcs_polarity]
mem.tmode = TMODES[_mem.tmode]
mem.skip = (_psk & bit and "P") or (_skp & bit and "S") or ""
return mem
def set_memory(self, mem):
bit = 1 << (mem.number % 8)
byte = int(mem.number / 8)
_mem = self._memobj.memory[mem.number]
_usd = self._memobj.used[byte]
_skp = self._memobj.skips[byte]
_psk = self._memobj.pskips[byte]
_mem.set_raw("\x00" * (_mem.size() // 8))
if mem.empty:
_usd |= bit
return
_usd &= ~bit
if chirp_common.is_12_5(mem.freq):
_mem.freq = (mem.freq // 6250) | 0x800000
else:
_mem.freq = mem.freq // 5000
_mem.offset = mem.offset // 5000
_mem.name = mem.name.ljust(6)[:6]
_mem.rtone = chirp_common.TONES.index(mem.rtone)
_mem.ctone = chirp_common.TONES.index(mem.ctone)
_mem.dtcs = chirp_common.DTCS_CODES.index(mem.dtcs)
_mem.tuning_step = TUNING_STEPS.index(mem.tuning_step)
_mem.narrow = mem.mode == "NFM"
_mem.duplex = DUPLEX.index(mem.duplex)
_mem.dtcs_polarity = DTCS_POLARITY.index(mem.dtcs_polarity)
_mem.tmode = TMODES.index(mem.tmode)
if mem.power:
_mem.power = POWER_LEVELS.index(mem.power)
else:
_mem.power = 0
if mem.skip == "S":
_skp |= bit
_psk &= ~bit
elif mem.skip == "P":
_skp &= ~bit
_psk |= bit
else:
_skp &= ~bit
_psk &= ~bit
chirp-20261030/chirp/drivers/ict7h.py 0000644 0001750 0001750 00000007446 15257672455 016242 0 ustar hibby hibby # Copyright 2012 Eric Allen
#
# 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.drivers import icf
from chirp import chirp_common, directory, bitwise
mem_format = """
struct {
bbcd freq[2];
u8 lastfreq:4,
fraction:4;
bbcd offset[2];
u8 unknown;
u8 rtone;
u8 ctone;
} memory[60];
#seekto 0x0270;
struct {
u8 empty:1,
tmode:2,
duplex:2,
unknown3:1,
skip:1,
unknown4:1;
} flags[60];
"""
TMODES = ["", "", "Tone", "TSQL", "TSQL"] # last one is pocket beep
DUPLEX = ["", "", "-", "+"]
MODES = ["FM"]
STEPS = [5.0, 10.0, 12.5, 15.0, 20.0, 25.0, 30.0, 50.0]
@directory.register
class ICT7HRadio(icf.IcomCloneModeRadio):
VENDOR = "Icom"
MODEL = "IC-T7H"
_model = "\x18\x10\x00\x01"
_memsize = 0x03B0
_endframe = "Icom Inc\x2e"
_ranges = [(0x0000, _memsize, 16)]
@classmethod
def match_model(cls, filedata, filename):
return (super().match_model(filedata, filename) or
(cls._memsize and len(filedata) == cls._memsize))
def get_features(self):
rf = chirp_common.RadioFeatures()
rf.memory_bounds = (1, 60)
rf.valid_modes = list(MODES)
rf.valid_tmodes = list(TMODES)
rf.valid_duplexes = list(DUPLEX)
rf.valid_tuning_steps = list(STEPS)
rf.valid_bands = [(118000000, 174000000),
(400000000, 470000000)]
rf.valid_skips = ["", "S"]
rf.has_dtcs = False
rf.has_dtcs_polarity = False
rf.has_bank = False
rf.has_name = False
rf.has_tuning_step = False
return rf
def process_mmap(self):
self._memobj = bitwise.parse(mem_format, self._mmap)
def get_raw_memory(self, number):
return repr(self._memobj.memory[number])
def get_memory(self, number):
_mem = self._memobj.memory[number - 1]
_flag = self._memobj.flags[number - 1]
mem = chirp_common.Memory()
mem.number = number
mem.empty = _flag.empty == 1 and True or False
mem.freq = int(_mem.freq) * 100000
mem.freq += _mem.lastfreq * 10000
mem.freq += int(_mem.fraction / 2.0 * 1000)
mem.offset = int(_mem.offset) * 10000
mem.rtone = chirp_common.TONES[_mem.rtone - 1]
mem.ctone = chirp_common.TONES[_mem.ctone - 1]
mem.tmode = TMODES[_flag.tmode]
mem.duplex = DUPLEX[_flag.duplex]
mem.mode = "FM"
if _flag.skip:
mem.skip = "S"
return mem
def set_memory(self, mem):
_mem = self._memobj.memory[mem.number - 1]
_flag = self._memobj.flags[mem.number - 1]
_mem.freq = int(mem.freq / 100000)
topfreq = int(mem.freq / 100000) * 100000
lastfreq = int((mem.freq - topfreq) / 10000)
_mem.lastfreq = lastfreq
midfreq = (mem.freq - topfreq - lastfreq * 10000)
_mem.fraction = midfreq // 500
_mem.offset = mem.offset / 10000
_mem.rtone = chirp_common.TONES.index(mem.rtone) + 1
_mem.ctone = chirp_common.TONES.index(mem.ctone) + 1
_flag.tmode = TMODES.index(mem.tmode)
_flag.duplex = DUPLEX.index(mem.duplex)
_flag.skip = mem.skip == "S" and 1 or 0
_flag.empty = mem.empty and 1 or 0
chirp-20261030/chirp/drivers/ict8.py 0000644 0001750 0001750 00000010527 15257672455 016065 0 ustar hibby hibby # Copyright 2012 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.drivers import icf
from chirp import chirp_common, directory
from chirp import bitwise
mem_format = """
struct memory {
bbcd freq[3];
bbcd offset[3];
u8 rtone;
u8 ctone;
};
struct flags {
u8 empty:1,
skip:1,
tmode:2,
duplex:2,
unknown2:2;
};
struct memory memory[100];
#seekto 0x0400;
struct {
char name[4];
} names[100];
#seekto 0x0600;
struct flags flags[100];
"""
DUPLEX = ["", "", "-", "+"]
TMODES = ["", "", "Tone", "TSQL"]
def _get_freq(bcd_array):
lastnibble = bcd_array[2].get_bits(0x0F)
return (int(bcd_array) - lastnibble) * 1000 + lastnibble * 500
def _set_freq(bcd_array, freq):
bcd_array.set_value(freq / 1000)
bcd_array[2].set_raw(bcd_array[2].get_bits(0xF0) + freq % 10000 // 500)
@directory.register
class ICT8ARadio(icf.IcomCloneModeRadio):
"""Icom IC-T8A"""
VENDOR = "Icom"
MODEL = "IC-T8A"
_model = "\x19\x03\x00\x01"
_memsize = 0x07B0
_endframe = "Icom Inc\x2e"
_ranges = [(0x0000, 0x07B0, 16)]
@classmethod
def match_model(cls, filedata, filename):
return (super().match_model(filedata, filename) or
(cls._memsize and len(filedata) == cls._memsize))
def get_features(self):
rf = chirp_common.RadioFeatures()
rf.valid_tmodes = TMODES
rf.valid_duplexes = DUPLEX
rf.valid_bands = [(50000000, 54000000),
(118000000, 174000000),
(400000000, 470000000)]
rf.valid_skips = ["", "S"]
rf.valid_modes = ["FM"]
rf.memory_bounds = (0, 99)
rf.valid_name_length = 4
rf.valid_characters = chirp_common.CHARSET_UPPER_NUMERIC
rf.has_dtcs = False
rf.has_dtcs_polarity = False
rf.valid_tuning_steps = [5, 10, 12.5, 15, 20, 25, 30, 50, 100]
rf.has_tuning_step = False
rf.has_mode = False
rf.has_bank = False
return rf
def process_mmap(self):
self._memobj = bitwise.parse(mem_format, self._mmap)
def get_raw_memory(self, number):
return (str(self._memobj.memory[number]) +
str(self._memobj.names[number]) +
str(self._memobj.flags[number]))
def get_memory(self, number):
_mem = self._memobj.memory[number]
_flg = self._memobj.flags[number]
_name = self._memobj.names[number]
mem = chirp_common.Memory()
mem.number = number
if _flg.empty:
mem.empty = True
return mem
mem.freq = _get_freq(_mem.freq)
mem.offset = _get_freq(_mem.offset)
mem.rtone = chirp_common.TONES[_mem.rtone - 1]
mem.ctone = chirp_common.TONES[_mem.ctone - 1]
mem.duplex = DUPLEX[_flg.duplex]
mem.tmode = TMODES[_flg.tmode]
mem.skip = _flg.skip and "S" or ""
if _name.name.get_raw(asbytes=False) != "\xFF\xFF\xFF\xFF":
mem.name = str(_name.name).rstrip().rstrip('\x00')
return mem
def set_memory(self, mem):
_mem = self._memobj.memory[mem.number]
_flg = self._memobj.flags[mem.number]
_name = self._memobj.names[mem.number]
if mem.empty:
_flg.empty = True
return
_mem.set_raw("\x00" * 8)
_flg.set_raw("\x00")
_set_freq(_mem.freq, mem.freq)
_set_freq(_mem.offset, mem.offset)
_mem.rtone = chirp_common.TONES.index(mem.rtone) + 1
_mem.ctone = chirp_common.TONES.index(mem.ctone) + 1
_flg.duplex = DUPLEX.index(mem.duplex)
_flg.tmode = TMODES.index(mem.tmode)
_flg.skip = mem.skip == "S"
if mem.name:
_name.name = mem.name.ljust(4)
else:
_name.name = "\xFF\xFF\xFF\xFF"
chirp-20261030/chirp/drivers/icv80.py 0000644 0001750 0001750 00000036756 15047574664 016163 0 ustar hibby hibby # 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 logging
from chirp.drivers import icf
from chirp import chirp_common, bitwise, errors, directory
from chirp.settings import RadioSetting, RadioSettingGroup, \
RadioSettingValueList, RadioSettingValueBoolean, \
RadioSettings
LOG = logging.getLogger(__name__)
ICV80_MEM_FORMAT = """
struct {
ul16 freq;
ul16 offset;
char name[5];
u8 unknown0:2,
rtone:6;
u8 unknown1:2,
ctone:6;
u8 unknown2:1,
dtcs:7;
u8 unknown3:5,
tuning_step:3;
u8 unknown4:2,
mode:1,
reverse_duplex:1,
duplex:2,
mult:2;
u8 unknown6:2,
dtcs_polarity:2,
unknown8:2,
tmode:2;
u8 unknown9:5,
tx_inhibit:1,
power:2;
} memory[207];
u8 skip[32];
u8 unused[32];
#seekto 0x0e50;
struct {
u8 unknown1:6,
beep:2;
u8 unknown2:3,
tot:5;
u8 unknown3;
u8 unknown4:6,
auto_pwr_off:2;
u8 unknown5:6,
lockout:2;
u8 unknown6:7,
squelch_delay:1;
u8 unknown7_0;
u8 unknown7_1:6,
mem_display1:2;
u8 unknown8:6,
mem_display2:2; // CS stores the display value here as well
u8 unknown9:7,
dial_func:1;
u8 unknown10:7,
lcd:1;
u8 unknown11:5,
pwr_save:3;
u8 unknown12:7,
sel_speed:1;
u8 unknown13:6,
mic_mode:2;
u8 unknown14:6,
battery_save:2;
u8 unknown15;
u8 unknown16:6,
resume:2;
u8 unknown17:5,
func_mode:3;
u8 unknown18:6,
backlight:2;
u8 unknown19;
u8 unknown:4,
vox_gain:4;
u8 unknown20:6,
mic_gain:2;
u8 unknown21:5,
vox_delay:3;
u8 unknown22:4,
vox_tot:4;
u8 unknown23[2];
u8 unknown24:6,
edge:2;
u8 unknown25;
u8 unknown26:7,
auto_low_pwr:1;
u8 unknown27[3];
} settings;
"""
SPECIAL_CHANNELS = {
"1A": 200, "1B": 201,
"2A": 202, "2B": 203,
"3A": 204, "3B": 205,
"C": 206,
}
TMODES = ["", "Tone", "TSQL", "DTCS"]
MODES = ["FM", "NFM"]
SKIPS = ["", "S"]
DUPLEXES = ["", "-", "+"]
DTCS_POLARITY = ["NN", "NR", "RN", "RR"]
TUNING_STEPS = [5.0, 10.0, 12.5, 15.0, 20.0, 25.0, 30.0, 50.0]
POWER_LEVELS = [
chirp_common.PowerLevel("High", watts=5.5),
chirp_common.PowerLevel("Low", watts=0.5),
chirp_common.PowerLevel("Mid", watts=2.5)
]
@directory.register
class ICV80Radio(icf.IcomCloneModeRadio, chirp_common.ExperimentalRadio):
"""Icom IC-V80"""
VENDOR = "Icom"
MODEL = "IC-V80"
_model = "\x32\x54\x00\x01"
_memsize = 0x0E80
_endframe = "Icom Inc\x2e7C"
_can_hispeed = True
_ranges = [(0x0000, 0x0CE0, 32),
(0x0CE0, 0x0D40, 16),
(0x0D40, 0x0E00, 32),
(0x0E00, 0x0E20, 16),
(0x0E20, 0x0E60, 32),
(0x0E60, 0x0E70, 16),
(0x0E70, 0x0E72, 2),
(0x0E72, 0x0E77, 5),
(0x0E77, 0x0E78, 1),
(0x0E78, 0x0E80, 8),
]
@classmethod
def get_prompts(cls):
rp = chirp_common.RadioPrompts()
rp.experimental = ("This radio driver is currently under development, "
"and not all the features or functions may work as"
"expected. You should proceed with caution.")
return rp
def get_features(self):
rf = chirp_common.RadioFeatures()
rf.memory_bounds = (0, 199)
rf.valid_modes = MODES
rf.valid_tmodes = TMODES
rf.valid_duplexes = DUPLEXES + ['off']
rf.valid_tuning_steps = TUNING_STEPS
rf.valid_power_levels = POWER_LEVELS
rf.valid_skips = SKIPS
rf.valid_name_length = 5
rf.valid_special_chans = sorted(SPECIAL_CHANNELS.keys())
rf.valid_bands = [(136000000, 174000000)]
rf.has_ctone = True
rf.has_offset = True
rf.has_bank = False
rf.has_settings = True
return rf
def process_mmap(self):
self._memobj = bitwise.parse(ICV80_MEM_FORMAT, self._mmap)
def get_settings(self):
_settings = self._memobj.settings
setmode = RadioSettingGroup("setmode", "Set Mode")
display = RadioSettingGroup("display", "Display")
sounds = RadioSettingGroup("sounds", "Sounds")
scan = RadioSettingGroup("scan", "Scan")
settings = RadioSettings(setmode, display, sounds, scan)
# TOT
opts = ["Off"] + ["%d min" % t for t in range(1, 31)]
setmode.append(
RadioSetting(
"tot", "Time out Timer",
RadioSettingValueList(opts, current_index=_settings.tot)))
# Lockout
opts = ["Off", "Rpt", "Busy"]
setmode.append(
RadioSetting(
"lockout", "Lockout",
RadioSettingValueList(opts, current_index=_settings.lockout)))
# Auto Power Off
opts = ["Off", "30 min", "1 hr", "2 hrs"]
setmode.append(
RadioSetting(
"auto_pwr_off", "Auto Power Off",
RadioSettingValueList(
opts, current_index=_settings.auto_pwr_off)))
# Power Save
opts = ["Off", "1:2", "1:8", "1:16", "Auto"]
setmode.append(
RadioSetting(
"pwr_save", "Power Save",
RadioSettingValueList(opts, current_index=_settings.pwr_save)))
# Battery Save
opts = ["Off", "Ni-MH", "Li-Ion"]
setmode.append(
RadioSetting(
"battery_save", "Battery Save",
RadioSettingValueList(
opts, current_index=_settings.battery_save)))
# Auto Low Power
opts = ["Off", "On"]
setmode.append(
RadioSetting(
"auto_low_pwr", "Auto Low Power",
RadioSettingValueList(
opts, current_index=_settings.auto_low_pwr)))
# Squelch Delay
opts = ["Short", "Long"]
setmode.append(
RadioSetting(
"squelch_delay", "Squelch Delay",
RadioSettingValueList(
opts, current_index=_settings.squelch_delay)))
# MIC Simple Mode
opts = ["Simple", "Normal 1", "Normal 2"]
setmode.append(
RadioSetting(
"mic_mode", "Mic Simple Mode",
RadioSettingValueList(opts, current_index=_settings.mic_mode)))
# MIC Gain
opts = ["1", "2", "3", "4"]
setmode.append(
RadioSetting(
"mic_gain", "Mic Gain",
RadioSettingValueList(opts, current_index=_settings.mic_gain)))
# VOX Gain
opts = ["Off"] + ["%d" % t for t in range(1, 11)]
setmode.append(
RadioSetting(
"vox_gain", "VOX Gain",
RadioSettingValueList(opts, current_index=_settings.vox_gain)))
# VOX Delay
opts = ["0.5 sec", "1.0 sec", "1.5 sec", "2.0 sec", "2.5 sec",
"3.0 sec"]
setmode.append(
RadioSetting(
"vox_delay", "VOX Delay",
RadioSettingValueList(
opts, current_index=_settings.vox_delay)))
# VOX Time out Timer
opts = ["Off", "1 min", "2 min", "3 min", "4 min", "5 min", "10 min",
"15 min"]
setmode.append(
RadioSetting(
"vox_tot", "VOX Time-Out Timer",
RadioSettingValueList(opts, current_index=_settings.vox_tot)))
# Select Speed
opts = ["Manual", "Auto"]
setmode.append(
RadioSetting(
"sel_speed", "Select Speed",
RadioSettingValueList(
opts, current_index=_settings.sel_speed)))
# Dial Function
opts = ["Audio Volume", "Tuning Dial"]
setmode.append(
RadioSetting(
"dial_func", "Dial Function",
RadioSettingValueList(
opts, current_index=_settings.dial_func)))
# Function Mode
opts = ["0 sec", "1 sec", "2 sec", "3 sec", "Manual"]
setmode.append(
RadioSetting(
"func_mode", "Function Mode",
RadioSettingValueList(
opts, current_index=_settings.func_mode)))
# Backlight
opts = ["Off", "On", "Auto"]
display.append(
RadioSetting(
"backlight", "Backlight",
RadioSettingValueList(
opts, current_index=_settings.backlight)))
# LCD Contrast
opts = ["Low", "Auto"]
display.append(
RadioSetting(
"lcd", "LCD Contrast",
RadioSettingValueList(opts, current_index=_settings.lcd)))
# Memory Display
opts = ["Frequency", "Channel", "Name"]
display.append(
RadioSetting(
"mem_display1", "Memory Display",
RadioSettingValueList(
opts, current_index=_settings.mem_display1)))
# Beep
opts = ["Off", "1", "2", "3"]
sounds.append(
RadioSetting(
"beep", "Beep",
RadioSettingValueList(opts, current_index=_settings.beep)))
# Edge
opts = ["All", "P1", "P2", "P3"]
scan.append(
RadioSetting(
"edge", "Edge",
RadioSettingValueList(opts, current_index=_settings.edge)))
# Resume
opts = ["T-5", "T-10", "T-15", "P-2"]
scan.append(
RadioSetting(
"resume", "Resume",
RadioSettingValueList(opts, current_index=_settings.resume)))
return settings
def set_settings(self, settings):
_settings = self._memobj.settings
for element in settings:
if not isinstance(element, RadioSetting):
self.set_settings(element)
continue
if not element.changed():
continue
try:
if element.has_apply_callback():
LOG.debug("Using apply callback")
element.run_apply_callback()
else:
setting = element.get_name()
LOG.debug("Setting %s = %s" % (setting, element.value))
setattr(_settings, setting, element.value)
# This appears to need to be mirrored?
if element.get_name() == 'mem_display1':
_settings.mem_display2 = _settings.mem_display1
except Exception:
LOG.debug(element.get_name())
raise
def _get_memory(self, number, extd_number=None):
bit = 1 << (number % 8)
byte = number // 8
_mem = self._memobj.memory[number]
_unused = self._memobj.unused[byte]
_skip = self._memobj.skip[byte]
mem = chirp_common.Memory(number)
if extd_number is not None:
mem.extd_number = extd_number
mem.immutable = ["name", "number", "extd_number", "skip"]
if extd_number == "C":
_unused = False
if (_unused & bit):
mem.empty = True
return mem
if int(_mem.mult):
mult = 6250
else:
mult = 5000
mem.freq = int(_mem.freq) * mult
mem.offset = int(_mem.offset) * mult
if mem.extd_number == "":
mem.name = str(_mem.name).rstrip()
mem.skip = (_skip & bit) and "S" or ""
if not _mem.tx_inhibit:
mem.duplex = 'off'
else:
mem.duplex = DUPLEXES[_mem.duplex]
mem.power = POWER_LEVELS[_mem.power]
mem.tuning_step = TUNING_STEPS[_mem.tuning_step]
mem.mode = MODES[_mem.mode]
mem.tmode = TMODES[_mem.tmode]
mem.rtone = chirp_common.TONES[_mem.rtone]
mem.ctone = chirp_common.TONES[_mem.ctone]
mem.dtcs = chirp_common.DTCS_CODES[_mem.dtcs]
mem.dtcs_polarity = DTCS_POLARITY[_mem.dtcs_polarity]
# Reverse duplex
mem.extra = RadioSettingGroup("extra", "Extra")
rev = RadioSetting("reverse_duplex", "Reverse duplex",
RadioSettingValueBoolean(bool(_mem.reverse_duplex)))
rev.set_doc("Reverse duplex")
mem.extra.append(rev)
return mem
def get_memory(self, number):
extd_number = None
if isinstance(number, str):
try:
extd_number = number
number = SPECIAL_CHANNELS[number]
except KeyError:
raise errors.InvalidMemoryLocation(
"Unknown channel %s" % number)
return self._get_memory(number, extd_number)
def _fill_memory(self, mem):
number = mem.number
_mem = self._memobj.memory[number]
# zero-fill
_mem.freq = 146010000 // 5000
_mem.offset = 600000 // 5000
_mem.name = str("").ljust(5)
_mem.duplex = 0x0
_mem.reverse_duplex = 0x0
_mem.tx_inhibit = 0x1
_mem.power = 0x0
_mem.tuning_step = 0x0
_mem.mode = 0x0
_mem.tmode = 0x0
_mem.rtone = 0x8
_mem.ctone = 0x8
_mem.dtcs = 0x0
_mem.dtcs_polarity = 0x0
for setting in mem.extra:
setattr(_mem, setting.get_name(), setting.value)
def set_memory(self, mem):
bit = 1 << (mem.number % 8)
byte = mem.number // 8
_mem = self._memobj.memory[mem.number]
_unused = self._memobj.unused[byte]
_skip = (mem.extd_number == "") and self._memobj.skip[byte] or None
assert (_mem)
if mem.empty:
self._fill_memory(mem)
_unused |= bit
if _skip is not None:
_skip |= bit
return
_mem.set_raw(b'\x00' * 16)
if chirp_common.required_step(mem.freq) == 12.5:
mult = 6250
else:
mult = 5000
_mem.mult = 0 if mult == 5000 else 3
_mem.freq = mem.freq // mult
_mem.offset = int(mem.offset) // mult
_mem.name = str(mem.name).ljust(5)
try:
_mem.duplex = DUPLEXES.index(mem.duplex)
except ValueError:
_mem.duplex = 0
_mem.tx_inhibit = mem.duplex != 'off'
power = mem.power or POWER_LEVELS[0]
_mem.power = POWER_LEVELS.index(power)
_mem.tuning_step = TUNING_STEPS.index(mem.tuning_step)
_mem.mode = MODES.index(mem.mode)
_mem.tmode = TMODES.index(mem.tmode)
_mem.rtone = chirp_common.TONES.index(mem.rtone)
_mem.ctone = chirp_common.TONES.index(mem.ctone)
_mem.dtcs = chirp_common.DTCS_CODES.index(mem.dtcs)
_mem.dtcs_polarity = DTCS_POLARITY.index(mem.dtcs_polarity)
# Set used
_unused &= ~bit
# Set skip
if _skip is not None:
if mem.skip == "S":
_skip |= bit
else:
_skip &= ~bit
if mem.extra:
_mem.reverse_duplex = mem.extra['reverse_duplex'].value
def get_raw_memory(self, number):
return repr(self._memobj.memory[number])
chirp-20261030/chirp/drivers/icv86.py 0000644 0001750 0001750 00000024220 14670320477 016140 0 ustar hibby hibby # 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 logging
from chirp.drivers import icf
from chirp import chirp_common, bitwise, errors, directory
from chirp.settings import RadioSetting, RadioSettingGroup, \
RadioSettingValueInteger, RadioSettingValueList, \
RadioSettingValueBoolean, RadioSettings
LOG = logging.getLogger(__name__)
ICV86_MEM_FORMAT = """
#seekto 0x040;
u8 skips[26];
#seekto 0x060;
u8 used[26];
#seekto 0x00BF;
struct {
u8 reserved1[8];
u8 reserved2:6,
disp_type:2;
u8 reserved3;
u8 reserved4:7,
dial_assignment:1;
u8 reserved5[8];
u8 reserved6:6,
lcd:2;
u8 reserved7[2];
u8 reserved8:6,
mic:2;
} settings;
#seekto 0x0200;
struct {
ul32 freq;
ul32 offset;
char name[5];
u8 reserved1:2,
rtone:6;
u8 reserved2:2,
ctone:6;
u8 reserved3:1,
dtcs:7;
u8 reserved4:5,
tuning_step:3;
u8 reserved5:2,
mode:1,
rev:1,
duplex:2,
reserved6:2;
u8 reserved7:2,
dtcs_polarity:2,
tmode:4;
u8 reserved8:5,
tx:1,
power:2;
u8 reserved9;
u8 reserved10;
u8 reserved11;
u8 reserved12;
} memory[207];
"""
SPECIAL = {
"0A": 200, "0B": 201,
"1A": 202, "1B": 203,
"2A": 204, "2B": 205,
"C": 206,
}
SPECIAL_REV = {
200: "0A", 201: "0B",
202: "1A", 203: "1B",
204: "2A", 205: "2B",
206: "C",
}
TMODES = ["", "Tone", "TSQL", "DTCS", "DTCS-R"]
MODES = ["FM", "NFM"]
SKIPS = ["", "S"]
DUPLEXES = ["", "-", "+"]
DTCS_POLARITY = ["NN", "NR", "RN", "RR"]
TUNING_STEPS = [5., 10., 12.5, 15., 20., 25., 30., 50.]
POWER_LEVELS = [
chirp_common.PowerLevel("High", watts=5.5),
chirp_common.PowerLevel("Low", watts=0.5),
chirp_common.PowerLevel("Mid", watts=2.5),
chirp_common.PowerLevel("Extra High", watts=7.0),
]
@directory.register
class ICV86Radio(icf.IcomCloneModeRadio):
"""Icom IC-V86"""
VENDOR = "Icom"
MODEL = "IC-V86"
_model = "\x40\x66\x00\x01"
_memsize = 5504
_endframe = "Icom Inc\x2eAC"
_ranges = [(0x0000, 5504, 32)]
def get_features(self):
rf = chirp_common.RadioFeatures()
rf.memory_bounds = (0, 199)
rf.valid_modes = MODES
rf.valid_tmodes = TMODES
rf.valid_duplexes = DUPLEXES
rf.valid_tuning_steps = TUNING_STEPS
rf.valid_power_levels = POWER_LEVELS
rf.valid_skips = SKIPS
rf.valid_name_length = 5
rf.valid_special_chans = sorted(SPECIAL.keys())
rf.valid_bands = [(136000000, 174000000)]
rf.has_ctone = True
rf.has_offset = True
rf.has_bank = False
rf.has_settings = True
return rf
def __init__(self, pipe):
icf.IcomCloneModeRadio.__init__(self, pipe)
def sync_in(self):
icf.IcomCloneModeRadio.sync_in(self)
def sync_out(self):
icf.IcomCloneModeRadio.sync_out(self)
def process_mmap(self):
self._memobj = bitwise.parse(ICV86_MEM_FORMAT, self._mmap)
def get_settings(self):
_settings = self._memobj.settings
setmode = RadioSettingGroup("setmode", "General Settings")
settings = RadioSettings(setmode)
# LCD Backlight
opts = ["Off", "On", "Auto"]
setmode.append(
RadioSetting(
"lcd", "LCD Backlight",
RadioSettingValueList(opts, current_index=_settings.lcd)))
# Mic Gain
rs = RadioSetting("mic", "Mic Gain",
RadioSettingValueInteger(1, 4, _settings.mic + 1))
def apply_mic(s, obj):
setattr(obj, s.get_name(), int(s.value) - 1)
rs.set_apply_callback(apply_mic, self._memobj.settings)
setmode.append(rs)
# Dial Assignment
opts = ["Volume", "Tuning"]
setmode.append(
RadioSetting(
"dial_assignment", "Dial Assignment",
RadioSettingValueList(opts, current_index=_settings.dial_assignment)))
# Display Type
opts = ["Frequency", "Channel", "Name"]
setmode.append(
RadioSetting(
"disp_type", "Display Type",
RadioSettingValueList(opts, current_index=_settings.disp_type)))
return settings
def set_settings(self, settings):
_settings = self._memobj.settings
for element in settings:
if not isinstance(element, RadioSetting):
self.set_settings(element)
continue
if not element.changed():
continue
try:
if element.has_apply_callback():
LOG.debug("Using apply callback")
element.run_apply_callback()
else:
setting = element.get_name()
LOG.debug("Setting %s = %s" % (setting, element.value))
setattr(_settings, setting, element.value)
except Exception:
LOG.debug(element.get_name())
raise
def _get_memory(self, number):
bit = 1 << (number % 8)
byte = int(number / 8)
mem = chirp_common.Memory()
mem.number = number
_mem = self._memobj.memory[number]
if number < 200:
_usd = self._memobj.used[byte]
_skp = self._memobj.skips[byte]
else:
mem.extd_number = SPECIAL_REV[number]
mem.immutable = ["name", "number", "extd_number", "skip"]
_usd = self._memobj.used[byte] if (number <= 206) else None
_skp = None
if _usd is not None and (_usd & bit):
mem.empty = True
return mem
mem.freq = _mem.freq
mem.offset = int(_mem.offset)
if number < 200:
mem.name = str(_mem.name).rstrip()
mem.rtone = chirp_common.TONES[_mem.rtone]
mem.ctone = chirp_common.TONES[_mem.ctone]
mem.dtcs = chirp_common.DTCS_CODES[_mem.dtcs]
mem.tuning_step = TUNING_STEPS[_mem.tuning_step]
mem.mode = MODES[_mem.mode]
mem.duplex = DUPLEXES[_mem.duplex]
mem.dtcs_polarity = DTCS_POLARITY[_mem.dtcs_polarity]
mem.tmode = TMODES[_mem.tmode]
mem.power = POWER_LEVELS[_mem.power]
# Extras
mem.extra = RadioSettingGroup("extra", "Extra")
rev = RadioSetting("rev", "Reverse duplex",
RadioSettingValueBoolean(bool(_mem.rev)))
rev.set_doc("Reverse duplex")
mem.extra.append(rev)
tx = RadioSetting("tx", "Tx permission",
RadioSettingValueBoolean(bool(_mem.tx)))
tx.set_doc("Tx permission")
mem.extra.append(tx)
if _skp is not None:
mem.skip = (_skp & bit) and "S" or ""
return mem
def get_memory(self, number):
if not self._mmap:
self.sync_in()
assert (self._mmap)
if isinstance(number, str):
try:
number = SPECIAL[number]
except KeyError:
raise errors.InvalidMemoryLocation("Unknown channel %s" % number)
return self._get_memory(number)
def _fill_memory(self, number):
_mem = self._memobj.memory[number]
assert (_mem)
# zero-fill
_mem.freq = 146010000
_mem.offset = 146010000
_mem.name = str("").ljust(5)
_mem.reserved1 = 0x0
_mem.rtone = 0x8
_mem.reserved2 = 0x0
_mem.ctone = 0x8
_mem.reserved3 = 0x0
_mem.dtcs = 0x0
_mem.reserved4 = 0x0
_mem.tuning_step = 0x0
_mem.reserved5 = 0x0
_mem.mode = 0x0
_mem.rev = 0x0
_mem.duplex = 0x0
_mem.reserved6 = 0x0
_mem.reserved7 = 0x0
_mem.dtcs_polarity = 0x0
_mem.tmode = 0x0
_mem.tx = 0x1
_mem.reserved8 = 0x0
_mem.power = 0x0
_mem.reserved9 = 0x0
_mem.reserved10 = 0x0
_mem.reserved11 = 0x0
_mem.reserved12 = 0x0
def _set_memory(self, mem):
bit = 1 << (mem.number % 8)
byte = int(mem.number / 8)
_mem = self._memobj.memory[mem.number]
_usd = self._memobj.used[byte] if mem.number <= 206 else None
_skp = self._memobj.skips[byte] if mem.number < 200 else None
assert (_mem)
if mem.empty:
self._fill_memory(mem.number)
if _usd is not None:
_usd |= bit
return
if _usd is None or (_usd & bit):
self._fill_memory(mem.number)
_mem.freq = mem.freq
_mem.offset = int(mem.offset)
_mem.name = str(mem.name).ljust(5)
_mem.rtone = chirp_common.TONES.index(mem.rtone)
_mem.ctone = chirp_common.TONES.index(mem.ctone)
_mem.dtcs = chirp_common.DTCS_CODES.index(mem.dtcs)
_mem.tuning_step = TUNING_STEPS.index(mem.tuning_step)
_mem.mode = MODES.index(mem.mode)
_mem.duplex = DUPLEXES.index(mem.duplex)
_mem.dtcs_polarity = DTCS_POLARITY.index(mem.dtcs_polarity)
_mem.tmode = TMODES.index(mem.tmode)
_mem.power = 0 if mem.power is None else POWER_LEVELS.index(mem.power)
for setting in mem.extra:
setattr(_mem, setting.get_name(), setting.value)
if _usd is not None:
_usd &= ~bit
if _skp is not None:
if mem.skip == "S":
_skp |= bit
else:
_skp &= ~bit
def set_memory(self, mem):
if not self._mmap:
self.sync_in()
assert (self._mmap)
return self._set_memory(mem)
def get_raw_memory(self, number):
return repr(self._memobj.memory[number]) + \
repr(self._memobj.used[(number)])
chirp-20261030/chirp/drivers/icw32.py 0000644 0001750 0001750 00000015701 14751356113 016130 0 ustar hibby hibby # Copyright 2011 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 logging
from chirp.drivers import icf
from chirp import chirp_common, util, directory, bitwise
LOG = logging.getLogger(__name__)
MEM_FORMAT = """
#seekto 0x%x;
struct {
bbcd freq[3];
bbcd offset[3];
u8 ctone;
u8 rtone;
char name[8];
} memory[111];
#seekto 0x%x;
struct {
u8 empty:1,
skip:1,
tmode:2,
duplex:2,
unk3:1,
am:1;
} flag[111];
#seekto 0x0E9C;
struct {
u8 unknown1:7,
right_scan_direction:1;
u8 right_scanning:1,
unknown2:7;
u8 unknown3:7,
left_scan_direction:1;
u8 left_scanning:1,
unknown4:7;
} state;
#seekto 0x0F20;
struct {
bbcd freq[3];
bbcd offset[3];
u8 ctone;
u8 rtone;
} callchans[2];
"""
DUPLEX = ["", "", "-", "+"]
TONE = ["", "", "Tone", "TSQL"]
def _get_special():
special = {}
for i in range(0, 5):
special["M%iA" % (i+1)] = 100 + i*2
special["M%iB" % (i+1)] = 100 + i*2 + 1
return special
@directory.register
class ICW32ARadio(icf.IcomCloneModeRadio):
"""Icom IC-W32A"""
VENDOR = "Icom"
MODEL = "IC-W32A"
_model = "\x18\x82\x00\x01"
_memsize = 4064
_endframe = "Icom Inc\x2e"
_ranges = [(0x0000, 0x0FE0, 16)]
_limits = (0, 0)
_mem_positions = (0, 1)
def get_features(self):
rf = chirp_common.RadioFeatures()
rf.memory_bounds = (0, 99)
rf.valid_bands = [self._limits]
if int(self._limits[0] / 100) == 1:
rf.valid_modes = ["FM", "AM"]
else:
rf.valid_modes = ["FM"]
rf.valid_tmodes = ["", "Tone", "TSQL"]
rf.valid_name_length = 8
rf.valid_special_chans = sorted(_get_special().keys())
rf.valid_characters = chirp_common.CHARSET_UPPER_NUMERIC + '-./'
rf.valid_tuning_steps = [5.0, 10.0, 12.5, 15.0, 20.0, 25.0, 30.0, 50.0]
rf.has_sub_devices = self.VARIANT == ""
rf.has_ctone = True
rf.has_dtcs = False
rf.has_dtcs_polarity = False
rf.has_mode = "AM" in rf.valid_modes
rf.has_tuning_step = False
rf.has_bank = False
return rf
def process_mmap(self):
fmt = MEM_FORMAT % self._mem_positions
self._memobj = bitwise.parse(fmt, self._mmap)
def get_raw_memory(self, number):
return repr(self._memobj.memory[number])
def get_memory(self, number):
if isinstance(number, str):
number = _get_special()[number]
_mem = self._memobj.memory[number]
_flg = self._memobj.flag[number]
mem = chirp_common.Memory()
mem.number = number
if number < 100:
# Normal memories
mem.skip = _flg.skip and "S" or ""
else:
# Special memories
mem.extd_number = util.get_dict_rev(_get_special(), number)
if _flg.empty:
mem.empty = True
return mem
mem.freq = chirp_common.fix_rounded_step(int(_mem.freq) * 1000)
mem.offset = int(_mem.offset) * 100
if str(_mem.name)[0] != chr(0xFF):
mem.name = str(_mem.name).rstrip()
mem.rtone = chirp_common.TONES[_mem.rtone]
mem.ctone = chirp_common.TONES[_mem.ctone]
mem.mode = _flg.am and "AM" or "FM"
mem.duplex = DUPLEX[_flg.duplex]
mem.tmode = TONE[_flg.tmode]
if number > 100:
mem.immutable = ["number", "skip", "extd_number", "name"]
return mem
def set_memory(self, mem):
_mem = self._memobj.memory[mem.number]
_flg = self._memobj.flag[mem.number]
_flg.empty = mem.empty
if mem.empty:
return
_mem.freq = mem.freq / 1000
_mem.offset = mem.offset / 100
if mem.name:
_mem.name = mem.name.ljust(8)[:8]
else:
_mem.name = "".join(["\xFF" * 8])
_mem.rtone = chirp_common.TONES.index(mem.rtone)
_mem.ctone = chirp_common.TONES.index(mem.ctone)
_flg.duplex = DUPLEX.index(mem.duplex)
_flg.tmode = TONE.index(mem.tmode)
_flg.skip = mem.skip == "S"
_flg.am = mem.mode == "AM"
if self._memobj.state.left_scanning:
LOG.debug("Canceling scan on left VFO")
self._memobj.state.left_scanning = 0
if self._memobj.state.right_scanning:
LOG.debug("Canceling scan on right VFO")
self._memobj.state.right_scanning = 0
def get_sub_devices(self):
return [ICW32ARadioVHF(self._mmap), ICW32ARadioUHF(self._mmap)]
@classmethod
def match_model(cls, filedata, filename):
if not len(filedata) == cls._memsize:
return False
return filedata[-16:] == b"IcomCloneFormat3"
class ICW32ARadioVHF(ICW32ARadio):
"""ICW32 VHF subdevice"""
VARIANT = "VHF"
_limits = (118000000, 174000000)
_mem_positions = (0x0000, 0x0DC0)
class ICW32ARadioUHF(ICW32ARadio):
"""ICW32 UHF subdevice"""
VARIANT = "UHF"
_limits = (400000000, 470000000)
_mem_positions = (0x06E0, 0x0E2E)
# IC-W32E are the very same as IC-W32A but have a different _model
@directory.register
class ICW32ERadio(ICW32ARadio):
"""Icom IC-W32E"""
MODEL = "IC-W32E"
_model = "\x18\x82\x00\x02"
# an extra byte is added to distinguish file images from IC-W32A
# it will be allocated and initialized to 0x00 in _clone_from_radio
# (icf.py) but radio will not send it
# That byte is not sent to radio because the _clone_to_radio use _ranges
# for the send cycle
_memsize = ICW32ARadio._memsize + 1
def get_sub_devices(self):
# this is needed because sub devices must be of a child class
return [ICW32ERadioVHF(self._mmap), ICW32ERadioUHF(self._mmap)]
@classmethod
def match_model(cls, filedata, filename):
if not len(filedata) == cls._memsize:
return False
return filedata[-16 - 1: -1] == b"IcomCloneFormat3" and \
filedata[-1] in [0, '\x00']
# this is the very same as ICW32ARadioVHF but have ICW32ERadio as parent class
class ICW32ERadioVHF(ICW32ERadio):
"""ICW32 VHF subdevice"""
VARIANT = "VHF"
_limits = (118000000, 174000000)
_mem_positions = (0x0000, 0x0DC0)
# this is the very same as ICW32ARadioUHF but have ICW32ERadio as parent class
class ICW32ERadioUHF(ICW32ERadio):
"""ICW32 UHF subdevice"""
VARIANT = "UHF"
_limits = (400000000, 470000000)
_mem_positions = (0x06E0, 0x0E2E)
chirp-20261030/chirp/drivers/icx8x.py 0000644 0001750 0001750 00000020025 15257672455 016253 0 ustar hibby hibby # 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 logging
from chirp import bitwise
from chirp.drivers import icf
from chirp import chirp_common, directory
LOG = logging.getLogger(__name__)
MEM_FORMAT = """
struct {
ul16 freq;
ul16 offset;
char name[5];
u8 unknown1:2,
rtone:6;
u8 unknown2:2,
ctone:6;
u8 dtcs;
u8 unknown3[5];
u8 tuning_step:4,
unknown4:4;
u8 unknown5[3];
u8 mult:1,
unknown6_0:1,
narrow:1,
unknown6:1,
unknown7:2,
tmode:2;
u8 dtcs_pol:2,
duplex:2,
unknown9:4;
u8 unknown10:1,
txinhibit:1,
unknown11:2,
dvmode:1,
unknown12:3;
} memories[207];
#seekto 0x1370;
struct {
u8 unknown:2,
empty:1,
skip:1,
bank:4;
} flags[206];
struct dvcall {
char call[8];
char pad[8];
};
#seekto 0x15E0;
struct dvcall mycall[6];
#seekto 0x1640;
struct dvcall urcall[6];
#seekto 0x16A0;
struct dvcall rptcall[6];
#seekto 0x1700;
#seekto 0x1930;
u8 isuhf;
"""
TMODES = ["", "Tone", "TSQL", "DTCS"]
DUPLEXES = ["", '-', '+']
TUNING_STEPS = [5.0, 10.0, 12.5, 15.0, 20.0, 25.0, 30.0, 50.0]
DTCS_POLARITY = ['NN', 'NR', 'RN', 'RR']
SPECIALS = ['L0', 'U0', 'L1', 'U1', 'L2', 'U2', 'C']
class ICx8xRadio(icf.IcomCloneModeRadio, chirp_common.IcomDstarSupport):
"""Icom IC-V/U82"""
VENDOR = "Icom"
MODEL = "IC-V82/U82"
_model = "\x28\x26\x00\x01"
_memsize = 6464
_endframe = "Icom Inc\x2eCD"
_can_hispeed = False
_double_ident = True
@classmethod
def match_model(cls, filedata, filename):
return (super().match_model(filedata, filename) or
(cls._memsize and len(filedata) == cls._memsize))
_memories = []
_ranges = [(0x0000, 0x1340, 32),
(0x1340, 0x1360, 16),
(0x1360, 0x136B, 8),
(0x1370, 0x1440, 32),
(0x1460, 0x15D0, 32),
(0x15E0, 0x1930, 32),
(0x1938, 0x1940, 8),
]
MYCALL_LIMIT = (0, 6)
URCALL_LIMIT = (0, 6)
RPTCALL_LIMIT = (0, 6)
def _get_bank(self, loc):
bank = int(self._memobj.flags[loc].bank)
if bank > 9:
return None
else:
return bank
def _set_bank(self, loc, bank):
if bank is None:
self._memobj.flags[loc].bank = 0x0A
else:
self._memobj.flags[loc].bank = bank
def get_features(self):
rf = chirp_common.RadioFeatures()
rf.memory_bounds = (0, 199)
rf.valid_modes = ["FM", "NFM", "DV"]
rf.valid_tmodes = list(TMODES)
rf.valid_duplexes = list(DUPLEXES)
rf.valid_tuning_steps = list(TUNING_STEPS)
rf.valid_skips = ["", "S"]
rf.valid_name_length = 5
rf.valid_special_chans = list(SPECIALS)
return rf
def process_mmap(self):
self._memobj = bitwise.parse(MEM_FORMAT, self._mmap)
def get_memory(self, number):
mem = chirp_common.Memory()
if isinstance(number, str):
mem.extd_number = number
number = 200 + SPECIALS.index(number)
_mem = self._memobj.memories[number]
if number < 206:
_flg = self._memobj.flags[number]
mem.skip = _flg.skip and 'S' or ''
empty = _flg.empty
else:
empty = False
if _mem.mult:
mult = 6250
else:
mult = 5000
mem.number = number
if empty:
mem.empty = True
return mem
mem.name = str(_mem.name).rstrip()
if _mem.dvmode:
mem.mode = 'DV'
elif _mem.narrow:
mem.mode = 'NFM'
else:
mem.mode = 'FM'
mem.duplex = DUPLEXES[_mem.duplex]
mem.tmode = TMODES[_mem.tmode]
mem.tuning_step = TUNING_STEPS[_mem.tuning_step]
mem.rtone = chirp_common.TONES[_mem.rtone]
mem.ctone = chirp_common.TONES[_mem.ctone]
mem.dtcs = chirp_common.DTCS_CODES[_mem.dtcs]
mem.dtcs_polarity = DTCS_POLARITY[_mem.dtcs_pol]
mem.freq = (_mem.freq * mult) + chirp_common.to_MHz(self._base)
mem.offset = _mem.offset * 5000
return mem
def set_memory(self, memory):
_mem = self._memobj.memories[memory.number]
if memory.number < 206:
_flg = self._memobj.flags[memory.number]
_flg.skip = memory.skip == 'S'
_flg.empty = memory.empty
if memory.empty:
return
_mem.mult = chirp_common.is_fractional_step(memory.freq)
if _mem.mult:
mult = 6250
else:
mult = 5000
_mem.freq = (memory.freq - chirp_common.to_MHz(self._base)) // mult
_mem.offset = memory.offset // 5000
_mem.name = memory.name[:5].ljust(5)
_mem.dvmode = memory.mode == 'DV'
_mem.narrow = memory.mode == 'NFM'
_mem.duplex = DUPLEXES.index(memory.duplex)
_mem.tmode = TMODES.index(memory.tmode)
_mem.tuning_step = TUNING_STEPS.index(memory.tuning_step)
_mem.rtone = chirp_common.TONES.index(memory.rtone)
_mem.ctone = chirp_common.TONES.index(memory.ctone)
_mem.dtcs = chirp_common.DTCS_CODES.index(memory.dtcs)
_mem.dtcs_pol = DTCS_POLARITY.index(memory.dtcs_polarity)
def get_raw_memory(self, number):
if isinstance(number, str):
number = 200 + SPECIALS.index(number)
return repr(self._memobj.memories[number])
def get_urcall_list(self):
calls = []
for i in range(*self.URCALL_LIMIT):
calls.append(str(self._memobj.urcall[i].call).rstrip())
return calls
def get_repeater_call_list(self):
calls = []
for i in range(*self.RPTCALL_LIMIT):
calls.append(str(self._memobj.rptcall[i].call).rstrip())
return calls
def get_mycall_list(self):
calls = []
for i in range(*self.MYCALL_LIMIT):
calls.append(str(self._memobj.mycall[i].call).rstrip())
return calls
def set_urcall_list(self, calls):
for i in range(*self.URCALL_LIMIT):
try:
call = calls[i].ljust(8)
except IndexError:
call = " " * 8
self._memobj.urcall[i].call = call
def set_repeater_call_list(self, calls):
for i in range(*self.RPTCALL_LIMIT):
try:
call = calls[i].ljust(8)
except IndexError:
call = " " * 8
self._memobj.rptcall[i].call = call
def set_mycall_list(self, calls):
for i in range(*self.MYCALL_LIMIT):
try:
call = calls[i].ljust(8)
except IndexError:
call = " " * 8
self._memobj.mycall[i].call = call
@directory.register
class ICV82Radio(ICx8xRadio):
MODEL = 'IC-V82'
_base = 0
def get_features(self):
rf = super().get_features()
rf.valid_bands = [(118000000, 176000000)]
return rf
@classmethod
def match_model(cls, filedata, filename):
if super().match_model(filedata, filename):
return filedata[0x1930] == 0
@directory.register
class ICU82Radio(ICx8xRadio):
MODEL = 'IC-U82'
_base = 400
def get_features(self):
rf = super().get_features()
rf.valid_bands = [(420000000, 470000000)]
return rf
@classmethod
def match_model(cls, filedata, filename):
if super().match_model(filedata, filename):
return filedata[0x1930] == 1
chirp-20261030/chirp/drivers/icx90.py 0000644 0001750 0001750 00000111142 14751356113 016131 0 ustar hibby hibby # -*- coding: utf-8 -*-
# Copyright 2018-2020 Jaroslav Å karvada
# Based on various code from the CHIRP
# 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.drivers import icf
from chirp import chirp_common, bitwise, errors, directory
from chirp.settings import RadioSetting, RadioSettingGroup, \
RadioSettingValueInteger, RadioSettingValueList, \
RadioSettingValueBoolean, RadioSettingValueString, \
RadioSettings
import argparse
ICX90_MEM_FORMAT = """
struct mem_item {
ul24 freq;
u8 dtcs_polarity:2,
unknown_1:2,
offset_freq_mult:1,
unknown_2:2,
freq_mult:1;
u8 unknown_3:1,
duplex:2,
mode:2,
tone_mode:3;
ul16 offset_freq;
u8 dtcs;
u8 tx_tone_lo:4,
tune_step:4;
u8 rx_tone:6,
tx_tone_hi:2;
char name[6];
};
struct bank_item {
u8 invisible_channel:1,
prog_skip:1,
mem_skip:1,
bank_index:5;
u8 bank_channel;
};
struct tv_mem_item {
u8 fixed:7,
mode:1;
ul24 freq;
char name[4];
};
struct mem_item memory[500];
struct mem_item scan_edges[50];
struct bank_item banks[500];
u8 unknown_4[120];
struct mem_item vfo_a_band[10];
struct mem_item vfo_b_band[10];
struct mem_item call_channels[5];
struct tv_mem_item tv_memory[68];
u8 unknown_5[35];
ul16 mem_channel;
u8 autodial;
u8 unknown_6[8];
u8 unknown_7:6,
skip_scan:1,
unknown_8:1;
u8 squelch_level;
struct {
char dtmf_digits[16];
} dtmf_codes[10];
u8 tv_channel_skip[68];
u8 unknown_9[128];
u8 scan_resume;
u8 scan_pause;
u8 unknown_10;
u8 beep_volume;
u8 beep;
u8 backlight;
u8 busy_led;
u8 auto_power_off;
u8 power_save;
u8 monitor;
u8 dial_speedup;
u8 unknown_11;
u8 auto_repeater;
u8 dtmf_speed;
u8 hm_75a_function;
u8 wx_alert;
u8 expand_1;
u8 scan_stop_beep;
u8 scan_stop_light;
u8 unknown_12;
u8 light_position;
u8 light_color;
u8 unknown_13;
u8 band_edge_beep;
u8 auto_power_on;
u8 key_lock;
u8 ptt_lock;
u8 lcd_contrast;
u8 opening_message;
u8 expand_2;
u8 unknown_14;
u8 busy_lockout;
u8 timeout_timer;
u8 active_band;
u8 split;
u8 fm_narrow;
u8 morse_code_enable;
u8 morse_code_speed;
u8 unknown_15[22];
char opening_message_text[6];
u8 unknown_16[186];
u8 unknown_17:4,
tune_step:4;
u8 unknown_18[4];
u8 band_selected;
u8 unknown_19[2];
u8 unknown_20:2,
attenuator:1,
vfo:1,
power:1,
dial_select:1,
memory_name:1,
memory_display:1;
u8 unknown_21[2];
u8 mode:4,
unknown_22:4;
u8 unknown_23[9];
char alpha_tag[6];
u8 vfo_scan;
u8 memory_scan;
u8 unknown_24;
u8 tv_channel;
u8 wx_channel;
char comment[16];
"""
LOG = logging.getLogger(__name__)
# in bytes
MEM_ITEM_SIZE = 16
TV_MEM_ITEM_SIZE = 8
BANK_INDEX = ["A", "B", "C", "D", "E", "F", "G", "H", "J", "L", "N", "O",
"P", "Q", "R", "T", "U", "Y"]
MEM_NUM = 500
BANKS = 18
BANK_NUM = 100
BANK_INDEX_NUM = len(BANK_INDEX)
DTMF_AUTODIAL_NUM = 10
DTMF_DIGITS_NUM = 16
OPENING_MESSAGE_LEN = 6
COMMENT_LEN = 16
BANDS = 10
TV_CHANNELS = 68
CHARSET = "ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789()*+-,/|= "
NAME_LENGTH = 6
TV_NAME_LENGTH = 4
DUPLEX = ["", "-", "+", ""]
DTCS_POLARITY = ["NN", "NR", "RN", "RR"]
TONE_MODE = ["", "Tone", "TSQL", "DTCS"]
TUNE_STEP = [5.0, 6.25, 8.33, 9.0, 10.0, 12.5, 15.0, 20.0, 25.0, 30.0, 50.0, 100.0, 200.0]
TUNE_STEP_STR = [str(x) for x in TUNE_STEP]
MODE = ["FM", "WFM", "AM"]
TV_MODE = ["WFM", "AM"]
SQUELCH_LEVEL = ["Open", "Auto", "Level 1", "Level 2", "Level 3", "Level 4", "Level 5",
"Level 6", "Level 7", "Level 8", "Level 9"]
AUTODIAL = ["Tone call", "D0", "D1", "D2", "D3", "D4", "D5", "D6", "D7", "D8", "D9"]
DTMF_SPEED = ["100 ms", "200 ms", "300 ms", "400 ms"]
SCAN_RESUME = ["0 s", "1 s", "2 s", "3 s", "4 s", "5 s", "Hold"]
SCAN_PAUSE = ["2 s", "4 s", "6 s", "8 s", "10 s", "12 s", "14 s", "16 s", "18 s", "20 s", "Hold"]
BEEP_VOLUME = ["Volume", "0", "1", "2", "3", "4", "5", "6", "7", "8", "9", "10", "11", "12",
"13", "14", "15", "16", "17", "18", "19", "20", "21", "22", "23", "24", "25",
"26", "27", "28", "29", "30", "31"]
BACKLIGHT = ["Off", "On", "Auto"]
AUTO_POWER_OFF = ["Off", "30 min", "60 min", "90 min", "120 min"]
POWER_SAVE = ["Off", "1:1", "1:4", "1:8", "1:16", "Auto"]
MONITOR = ["Push", "Hold"]
AUTO_REPEATER = ["Off", "Duplex only", "Duplex & tone"]
HM_75A_FUNCTION = ["Simple", "Normal 1", "Normal 2"]
LIGHT_POSITION = ["LCD", "Key", "All"]
LIGHT_COLOR = ["Green", "Orange", "Red"]
AUTO_POWER_ON = ["Off", "00:30", "01:00", "01:30", "02:00", "02:30", "03:00", "03:30", "04:00",
"04:30", "05:00", "05:30", "06:00", "06:30", "07:00", "07:30", "08:00",
"08:30", "09:00", "09:30", "10:00", "10:30", "11:00", "11:30", "12:00",
"12:30", "13:00", "13:30", "14:00", "14:30", "15:00", "15:30", "16:00",
"16:30", "17:00", "17:30", "18:00", "18:30", "19:00", "19:30", "20:00",
"20:30", "21:00", "21:30", "22:00", "22:30", "23:00", "23:30", "24:00"]
KEY_LOCK = ["Normal", "No squelch", "No volume", "All"]
LCD_CONTRAST = ["1", "2", "3", "4"]
TIMEOUT_TIMER = ["Off", "1 min", "3 min", "5 min", "10 min"]
ACTIVE_BAND = ["All", "Single"]
MORSE_CODE_SPEED = ["10 WPM", "15 WPM", "20 WPM", "25 WPM"]
WX_CHANNEL = ["WX01", "WX02", "WX03", "WX04", "WX05", "WX06", "WX07", "WX08", "WX09", "WX10"]
MEMORY_DISPLAY = ["Channel", "Bank"]
DIAL_SELECT = ["Normal", "Volume"]
POWER = ["High", "Low"]
VFO = ["A", "B"]
OPERATION_MODE = ["VFO", "Memory", "Call channel", "TV"]
VFO_SCAN = ["All", "Band", "P0", "P1", "P2", "P3", "P4", "P5", "P6", "P7", "P8",
"P9", "P10", "P11", "P12", "P13", "P14", "P15", "P16", "P17", "P18",
"P19", "P20", "P21", "P22", "P23", "P24"]
MEMORY_SCAN = ["All", "Bank", "Sel BC", "Sel 5 MHz", "Sel 50 MHz", "Sel WFM", "Sel Air",
"Sel 144 MHz", "Sel 220 MHz", "Sel 300 MHz", "Sel 440 MHz", "Sel 800 MHz"]
class ICx90BankModel(icf.IcomIndexedBankModel):
bank_index = BANK_INDEX
def get_mappings(self):
banks = []
if (self._radio._num_banks != len(type(self).bank_index)):
raise Exception("Invalid number of banks %d, supported only %d banks" %
(self._radio._num_banks, len(type(self).bank_index)))
for i in range(0, self._radio._num_banks):
index = type(self).bank_index[i]
bank = self._radio._bank_class(self, index, "BANK-%s" % index)
bank.index = i
banks.append(bank)
return banks
class ICT90_Alias(chirp_common.Alias):
VENDOR = "Icom"
MODEL = "IC-T90"
@directory.register
class ICx90Radio(icf.IcomCloneModeRadio):
"""Icom IC-E/T90"""
VENDOR = "Icom"
MODEL = "IC-E90"
ALIASES = [ICT90_Alias]
_model = "\x25\x07\x00\x01"
_memsize = 0x2d40
_endframe = "Icom Inc."
_ranges = [(0x0000, 0x2d40, 32)]
_num_banks = BANKS
_bank_index_bounds = (0, BANK_NUM - 1)
_can_hispeed = False
def __init__(self, pipe):
icf.IcomCloneModeRadio.__init__(self, pipe)
def special_add(self, key, item_type, num, unique_idx):
item = {}
item["item_type"] = item_type
item["num"] = num
item["uidx"] = unique_idx
self.special[key] = item
def init_special(self):
self.special = {}
i = 0
# program scan edges
for x in range(25):
self.special_add("Scan edge: %02dA" % x, "scan_edge", x * 2, i)
self.special_add("Scan edge: %02dB" % x, "scan_edge", x * 2 + 1, i + 1)
i += 2
# call channels
for x in range(5):
self.special_add("Call ch: %d" % x, "call_chan", x, i)
i += 1
# VFO A
for x in range(10):
self.special_add("VFO A: %d" % x, "vfo_a", x, i)
i += 1
# VFO B
for x in range(10):
self.special_add("VFO B: %d" % x, "vfo_b", x, i)
i += 1
def get_sub_devices(self):
return [ICx90Radio_ham(self._mmap), ICx90Radio_tv(self._mmap)]
def clear_bank(self, loc):
# it seems that empty invisible channel which isn't in bank is defined in bank_item by bytes 0x9f 0x00,
# i.e. it has invisible_channel == 1
# it seems that non-empty visible channel which isn't in bank is defined in bank_item by bytes 0x1f 0x00,
# i.e. it has invisible_channel == 0
# so do not touch the invisible_channel (the bit 7 of the first byte) here and only
# set the rest of the bits
self.memobj.banks[loc].bank_index = 0x1f
self.memobj.banks[loc].prog_skip = 0
self.memobj.banks[loc].mem_skip = 0
self.memobj.banks[loc].bank_channel = 0
# it seems the bank driver has different terminology about bank number and index
# so in fact _get_bank and _set_bank are about indexes (i.e index in the array
# of bank names - A .. Y
# and _get_bank_index and _set_bank_index are about positions in the bank (0..99)
def _get_bank(self, loc):
i = self.memobj.banks[loc].bank_index
return i if i < BANK_INDEX_NUM else None
def _set_bank(self, loc, bank):
if bank is None:
self.clear_bank(loc)
else:
self.memobj.banks[loc].bank_index = bank
# it seems if invisible_channel == 1 the channel is invisible (deleted)
# so set it explicitly as visible
self.memobj.banks[loc].invisible_channel = 0
def _get_bank_index(self, loc):
i = self.memobj.banks[loc].bank_channel
return i if i < BANK_NUM else None
def _set_bank_index(self, loc, index):
self.memobj.banks[loc].bank_channel = index
def get_features(self):
self.init_special()
rf = chirp_common.RadioFeatures()
rf.has_settings = True
rf.has_name = True
rf.has_bank = True
rf.has_bank_index = True
rf.has_bank_names = False
rf.can_delete = True
rf.has_ctone = True
rf.has_dtcs = True
rf.has_dtcs_polarity = True
rf.has_tuning_step = True
rf.has_comment = False
rf.memory_bounds = (0, MEM_NUM - 1)
rf.has_sub_devices = True
rf.valid_characters = CHARSET
rf.valid_modes = list(MODE)
rf.valid_tmodes = list(TONE_MODE)
rf.valid_duplexes = list(DUPLEX)[:-1]
rf.valid_tuning_steps = list(TUNE_STEP)
rf.valid_bands = [(495000, 999990000)]
rf.valid_skips = ["", "S", "P"]
rf.valid_name_length = NAME_LENGTH
rf.valid_special_chans = sorted(self.special.keys())
return rf
def map_dtmf_chirp2icom(self, item):
item = str(item).upper()
if item == "*":
item = "E"
elif item == "#":
item = "F"
elif item == " ":
return 0xff
try:
ret = int(item, 16)
except ValueError:
raise errors.InvalidDataError("invalid DTMF number '%s'" % item)
return ret
def dtmf_chirp2icom(self, dtmf):
return "".join(map(self.map_dtmf_chirp2icom, str(dtmf).rjust(DTMF_DIGITS_NUM)))
def map_dtmf_icom2chirp(self, item):
item = ord(item)
if item == 0xff:
return " "
else:
item &= 0x0f
if item < 10:
return str(item)
else:
return ["A", "B", "C", "D", "*", "#"][item - 10]
def dtmf_icom2chirp(self, dtmf):
return "".join(map(self.map_dtmf_icom2chirp, str(dtmf)))
def apply_dtmf_autodial(self, setting, obj):
obj = self.dtmf_chirp2icom(setting.value)
def get_settings(self):
try:
_squelch = 1
basic = RadioSettingGroup("basic", "Basic")
expand_1 = RadioSettingGroup("expand_1", "Expand 1")
expand_2 = RadioSettingGroup("expand_2", "Expand 2")
dtmf_autodial = RadioSettingGroup("dtmf_autodial", "DTMF autodial")
group = RadioSettings(basic, expand_1, expand_2, dtmf_autodial)
# basic
basic.append(RadioSetting("mem_channel", "Current memory channel",
RadioSettingValueInteger(0, MEM_NUM - 1, self.memobj.mem_channel)))
basic.append(RadioSetting("squelch_level", "Squelch level",
RadioSettingValueList(SQUELCH_LEVEL,
current_index=self.memobj.squelch_level)))
basic.append(RadioSetting("scan_resume", "Scan resume",
RadioSettingValueList(SCAN_RESUME,
current_index=self.memobj.scan_resume)))
basic.append(RadioSetting("scan_pause", "Scan pause",
RadioSettingValueList(SCAN_PAUSE,
current_index=self.memobj.scan_pause)))
basic.append(RadioSetting("beep_volume", "Beep audio",
RadioSettingValueList(BEEP_VOLUME,
current_index=self.memobj.beep_volume)))
basic.append(RadioSetting("beep", "Operation beep",
RadioSettingValueBoolean(self.memobj.beep)))
basic.append(RadioSetting("backlight", "LCD backlight",
RadioSettingValueList(BACKLIGHT,
current_index=self.memobj.backlight)))
basic.append(RadioSetting("busy_led", "Busy LED",
RadioSettingValueBoolean(self.memobj.busy_led)))
basic.append(RadioSetting("auto_power_off", "Auto power off",
RadioSettingValueList(AUTO_POWER_OFF,
current_index=self.memobj.auto_power_off)))
basic.append(RadioSetting("power_save", "Power save",
RadioSettingValueList(POWER_SAVE,
current_index=self.memobj.power_save)))
basic.append(RadioSetting("monitor", "Monitor",
RadioSettingValueList(MONITOR,
current_index=self.memobj.monitor)))
basic.append(RadioSetting("dial_speedup", "Dial speedup",
RadioSettingValueBoolean(self.memobj.dial_speedup)))
basic.append(RadioSetting("auto_repeater", "Auto repeater",
RadioSettingValueList(AUTO_REPEATER,
current_index=self.memobj.auto_repeater)))
basic.append(RadioSetting("hm_75a_function", "HM-75A function",
RadioSettingValueList(HM_75A_FUNCTION,
current_index=self.memobj.hm_75a_function)))
basic.append(RadioSetting("wx_alert", "WX alert",
RadioSettingValueBoolean(self.memobj.wx_alert)))
basic.append(RadioSetting("wx_channel", "Current WX channel",
RadioSettingValueList(WX_CHANNEL,
current_index=self.memobj.wx_channel)))
basic.append(RadioSetting("comment", "Comment",
RadioSettingValueString(0, COMMENT_LEN,
str(self.memobj.comment),
autopad=True)))
basic.append(RadioSetting("tune_step", "Current tune step",
RadioSettingValueList(TUNE_STEP_STR,
current_index=self.memobj.tune_step)))
basic.append(RadioSetting("band_selected", "Selected band",
RadioSettingValueInteger(0, BANDS - 1, self.memobj.band_selected)))
basic.append(RadioSetting("memory_display", "Memory display",
RadioSettingValueList(MEMORY_DISPLAY,
current_index=self.memobj.memory_display)))
basic.append(RadioSetting("memory_name", "Memory name",
RadioSettingValueBoolean(self.memobj.memory_name)))
basic.append(RadioSetting("dial_select", "Dial select",
RadioSettingValueList(DIAL_SELECT,
current_index=self.memobj.dial_select)))
basic.append(RadioSetting("power", "RF power",
RadioSettingValueList(POWER,
current_index=self.memobj.power)))
basic.append(RadioSetting("vfo", "Current VFO",
RadioSettingValueList(VFO,
current_index=self.memobj.vfo)))
basic.append(RadioSetting("attenuator", "RF attenuator",
RadioSettingValueBoolean(self.memobj.attenuator)))
basic.append(RadioSetting("skip_scan", "Skip scan",
RadioSettingValueBoolean(self.memobj.skip_scan)))
# TODO: this needs to be reverse engineered, because the following commented
# code does not seem correct
# basic.append(RadioSetting("mode", "Current mode",
# RadioSettingValueList(OPERATION_MODE,
# OPERATION_MODE[self.memobj.mode])))
basic.append(RadioSetting("vfo_scan", "VFO scan",
RadioSettingValueList(VFO_SCAN,
current_index=self.memobj.vfo_scan)))
basic.append(RadioSetting("memory_scan", "Memory scan",
RadioSettingValueList(MEMORY_SCAN,
current_index=self.memobj.memory_scan)))
basic.append(RadioSetting("tv_channel", "Current TV channel",
RadioSettingValueInteger(0, TV_CHANNELS - 1, self.memobj.tv_channel)))
# DTMF auto dial
dtmf_autodial.append(RadioSetting("autodial", "Autodial",
RadioSettingValueList(AUTODIAL,
current_index=self.memobj.autodial)))
dtmf_autodial.append(RadioSetting("dtmf_speed", "Speed",
RadioSettingValueList(DTMF_SPEED,
current_index=self.memobj.dtmf_speed)))
for x in range(DTMF_AUTODIAL_NUM):
rs = RadioSetting("dtmf_codes[%d].dtmf_digits" % x, "DTMF autodial: %d" % x,
RadioSettingValueString(0, DTMF_DIGITS_NUM,
self.dtmf_icom2chirp(self.memobj.dtmf_codes[x].dtmf_digits),
autopad=True, charset="0123456789ABCD*#abcd "))
rs.set_apply_callback(self.apply_dtmf_autodial, self.memobj.dtmf_codes[x].dtmf_digits)
dtmf_autodial.append(rs)
# expand 1
expand_1.append(RadioSetting("expand_1", "Expand 1",
RadioSettingValueBoolean(self.memobj.expand_1)))
expand_1.append(RadioSetting("scan_stop_beep", "Scan stop beep",
RadioSettingValueBoolean(self.memobj.scan_stop_beep)))
expand_1.append(RadioSetting("scan_stop_light", "Scan stop light",
RadioSettingValueBoolean(self.memobj.scan_stop_light)))
expand_1.append(RadioSetting("light_postion", "Light position",
RadioSettingValueList(LIGHT_POSITION,
current_index=self.memobj.light_position)))
expand_1.append(RadioSetting("light_color", "Light color",
RadioSettingValueList(LIGHT_COLOR,
current_index=self.memobj.light_color)))
expand_1.append(RadioSetting("band_edge_beep", "Band edge beep",
RadioSettingValueBoolean(self.memobj.band_edge_beep)))
expand_1.append(RadioSetting("auto_power_on", "Auto power on",
RadioSettingValueList(AUTO_POWER_ON,
current_index=self.memobj.auto_power_on)))
expand_1.append(RadioSetting("key_lock", "Key lock",
RadioSettingValueList(KEY_LOCK,
current_index=self.memobj.key_lock)))
expand_1.append(RadioSetting("ptt_lock", "PTT lock",
RadioSettingValueBoolean(self.memobj.ptt_lock)))
expand_1.append(RadioSetting("lcd_contrast", "LCD contrast",
RadioSettingValueList(LCD_CONTRAST,
current_index=self.memobj.lcd_contrast)))
expand_1.append(RadioSetting("opening_message", "Opening message",
RadioSettingValueBoolean(self.memobj.opening_message)))
expand_1.append(RadioSetting("opening_message_text", "Opening message",
RadioSettingValueString(0, OPENING_MESSAGE_LEN,
str(self.memobj.opening_message_text),
autopad=True, charset=CHARSET)))
# expand 2
expand_2.append(RadioSetting("expand_2", "Expand 2",
RadioSettingValueBoolean(self.memobj.expand_2)))
expand_2.append(RadioSetting("busy_lockout", "Busy lock out",
RadioSettingValueBoolean(self.memobj.busy_lockout)))
expand_2.append(RadioSetting("timeout_timer", "Timeout timer",
RadioSettingValueList(TIMEOUT_TIMER,
current_index=self.memobj.timeout_timer)))
expand_2.append(RadioSetting("active_band", "Active band",
RadioSettingValueList(ACTIVE_BAND,
current_index=self.memobj.active_band)))
expand_2.append(RadioSetting("fm_narrow", "FM narrow",
RadioSettingValueBoolean(self.memobj.fm_narrow)))
expand_2.append(RadioSetting("split", "Split",
RadioSettingValueBoolean(self.memobj.split)))
expand_2.append(RadioSetting("morse_code_enable", "Morse code synthesizer",
RadioSettingValueBoolean(self.memobj.morse_code_enable)))
expand_2.append(RadioSetting("morse_code_speed", "Morse code speed",
RadioSettingValueList(MORSE_CODE_SPEED,
current_index=self.memobj.morse_code_speed)))
return group
except:
import traceback
LOG.error("Failed to parse settings: %s", traceback.format_exc())
return None
def set_settings(self, settings):
for element in settings:
if not isinstance(element, RadioSetting):
self.set_settings(element)
continue
if not element.changed():
continue
try:
if element.has_apply_callback():
LOG.debug("Using apply callback")
try:
element.run_apply_callback()
except NotImplementedError as e:
LOG.error("icx90: %s", e)
continue
# Find the object containing setting.
obj = self.memobj
bits = element.get_name().split(".")
setting = bits[-1]
for name in bits[:-1]:
if name.endswith("]"):
name, index = name.split("[")
index = int(index[:-1])
obj = getattr(obj, name)[index]
else:
obj = getattr(obj, name)
try:
old_val = getattr(obj, setting)
LOG.debug("Setting %s(%r) <= %s" % (
element.get_name(), old_val, element.value))
setattr(obj, setting, element.value)
except AttributeError as e:
LOG.error("Setting %s is not in the memory map: %s" %
(element.get_name(), e))
except Exception:
LOG.debug(element.get_name())
raise
def process_mmap(self):
self.memobj = bitwise.parse(ICX90_MEM_FORMAT, self._mmap)
def sync_in(self):
icf.IcomCloneModeRadio.sync_in(self)
self.process_mmap()
def sync_out(self):
icf.IcomCloneModeRadio.sync_out(self)
def freq_chirp2icom(self, freq):
if chirp_common.is_fractional_step(freq):
mult = 6250
multr = 1
else:
mult = 5000
multr = 0
return (freq / mult, multr)
def freq_icom2chirp(self, freq, mult):
return freq * (6250 if mult else 5000)
def get_skip(self, number):
bank_item = self.memobj.banks[number]
if bank_item.prog_skip:
return "P"
elif bank_item.mem_skip:
return "S"
return ""
def set_skip(self, number, skip):
bank_item = self.memobj.banks[number]
if skip == "P":
bank_item.prog_skip = 1
bank_item.mem_skip = 1
elif skip == "S":
bank_item.prog_skip = 0
bank_item.mem_skip = 1
elif skip == "":
bank_item.prog_skip = 0
bank_item.mem_skip = 0
else:
raise errors.InvalidDataError("skip '%s' not supported" % skip)
# returns (memobj.mem_item, is_special_channel, unique_idx)
def get_mem_item(self, number):
try:
item_type = self.special[number]["item_type"]
num = self.special[number]["num"]
unique_idx = self.special[number]["uidx"]
if item_type == "scan_edge":
return (self.memobj.scan_edges[num], True, unique_idx)
elif item_type == "call_chan":
return (self.memobj.call_channels[num], True, unique_idx)
elif item_type == "vfo_a":
return (self.memobj.vfo_a_band[num], True, unique_idx)
elif item_type == "vfo_b":
return (self.memobj.vfo_b_band[num], True, unique_idx)
else:
raise errors.InvalidDataError("unknown special channel type '%s'" % item_type)
except KeyError:
return (self.memobj.memory[number], False, number)
def get_raw_memory(self, number):
(mem_item, special, unique_idx) = self.get_mem_item(number)
return repr(mem_item)
def get_memory(self, number):
mem = chirp_common.Memory()
(mem_item, special, unique_idx) = self.get_mem_item(number)
freq = self.freq_icom2chirp(mem_item.freq, mem_item.freq_mult)
if freq == 0:
mem.empty = True
else:
mem.empty = False
mem.freq = freq
if special:
mem.name = " " * NAME_LENGTH
else:
mem.name = str(mem_item.name).rstrip("\x00 ")
mem.rtone = chirp_common.TONES[(mem_item.tx_tone_hi << 4) + mem_item.tx_tone_lo]
mem.ctone = chirp_common.TONES[mem_item.rx_tone]
mem.dtcs = chirp_common.DTCS_CODES[mem_item.dtcs]
mem.dtcs_polarity = DTCS_POLARITY[mem_item.dtcs_polarity]
mem.offset = self.freq_icom2chirp(mem_item.offset_freq, mem_item.offset_freq_mult)
mem.duplex = DUPLEX[mem_item.duplex]
mem.tmode = TONE_MODE[mem_item.tone_mode]
mem.tuning_step = TUNE_STEP[mem_item.tune_step]
mem.mode = MODE[mem_item.mode]
if not special:
mem.skip = self.get_skip(number)
if special:
mem.extd_number = number
mem.number = -len(self.special) + unique_idx
else:
mem.number = number
return mem
def set_memory(self, memory):
(mem_item, special, unique_idx) = self.get_mem_item(
memory.extd_number or memory.number)
if memory.empty:
mem_item.set_raw("\x00" * MEM_ITEM_SIZE)
self.clear_bank(memory.number)
self.memobj.banks[memory.number].invisible_channel = 1
else:
(mem_item.freq, mem_item.freq_mult) = self.freq_chirp2icom(memory.freq)
if special:
mem_item.name = " " * NAME_LENGTH
else:
self.memobj.banks[memory.number].invisible_channel = 0
for x in range(NAME_LENGTH):
try:
mem_item.name[x] = str(memory.name[x])
except IndexError:
mem_item.name[x] = " "
mem_item.tx_tone_hi = chirp_common.TONES.index(memory.rtone) >> 4
mem_item.tx_tone_lo = chirp_common.TONES.index(memory.rtone) & 0x0f
mem_item.rx_tone = chirp_common.TONES.index(memory.ctone)
mem_item.dtcs = chirp_common.DTCS_CODES.index(memory.dtcs)
mem_item.dtcs_polarity = DTCS_POLARITY.index(memory.dtcs_polarity)
(mem_item.offset_freq, mem_item.offset_freq_mult) = self.freq_chirp2icom(memory.offset)
mem_item.duplex = DUPLEX.index(memory.duplex)
mem_item.tone_mode = TONE_MODE.index(memory.tmode)
mem_item.tune_step = TUNE_STEP.index(memory.tuning_step)
mem_item.mode = MODE.index(memory.mode)
if not special:
self.set_skip(memory.number, memory.skip)
def get_bank_model(self):
return ICx90BankModel(self)
class ICx90Radio_ham(ICx90Radio):
VARIANT = 'Radio'
def get_features(self):
rf = ICx90Radio.get_features(self)
rf.has_sub_devices = False
return rf
class ICx90Radio_tv(ICx90Radio):
VARIANT = "TV"
def get_features(self):
rf = chirp_common.RadioFeatures()
rf.has_settings = True
rf.has_name = True
rf.has_bank = False
rf.has_bank_index = False
rf.has_bank_names = False
rf.can_delete = True
rf.has_ctone = False
rf.has_dtcs = False
rf.has_dtcs_polarity = False
rf.has_tuning_step = False
rf.has_comment = False
rf.has_sub_devices = False
rf.memory_bounds = (0, TV_CHANNELS - 1)
rf.valid_characters = CHARSET
rf.valid_modes = list(TV_MODE)
rf.valid_tmodes = []
rf.valid_duplexes = [""]
rf.valid_tuning_steps = [5.0]
rf.valid_bands = [(46750000, 957750000)]
rf.valid_skips = ["", "S"]
rf.valid_name_length = TV_NAME_LENGTH
rf.valid_special_chans = []
return rf
def get_bank_model(self):
return None
def freq_chirp2icom(self, freq):
return freq / 5000
def freq_icom2chirp(self, freq):
return freq * 5000
def get_skip(self, number):
if self.memobj.tv_channel_skip[number] == 2:
return "S"
return ""
def set_skip(self, number, skip):
if skip == "S":
self.memobj.tv_channel_skip[number] = 2
elif skip == "":
self.memobj.tv_channel_skip[number] = 0
else:
raise errors.InvalidDataError("skip '%s' not supported" % skip)
def get_raw_memory(self, number):
return repr(self.memobj.tv_memory[number])
def get_memory(self, number):
mem = chirp_common.Memory()
mem_item = self.memobj.tv_memory[number]
mem.freq = self.freq_icom2chirp(mem_item.freq)
if self.memobj.tv_channel_skip[number] == 1:
mem.empty = True
mem.mode = TV_MODE[0]
else:
mem.empty = False
mem.name = str(mem_item.name).rstrip("\x00 ")
mem.mode = TV_MODE[mem_item.mode]
mem.skip = self.get_skip(number)
mem.number = number
return mem
def set_memory(self, memory):
mem_item = self.memobj.tv_memory[memory.number]
if memory.empty:
self.memobj.tv_channel_skip[memory.number] = 1
mem_item.set_raw("\x00" * TV_MEM_ITEM_SIZE)
else:
mem_item.freq = self.freq_chirp2icom(memory.freq)
for x in range(TV_NAME_LENGTH):
try:
mem_item.name[x] = str(memory.name[x])
except IndexError:
mem_item.name[x] = " "
mem_item.mode = TV_MODE.index(memory.mode)
self.set_skip(memory.number, memory.skip)
def dump_banks(icx90, template_file):
mb = icx90.get_features().memory_bounds
with open(template_file, "w") as f:
for mi in range(mb[0], mb[1] + 1):
mem = icx90.get_memory(mi)
bank = icx90._get_bank(mi)
if bank is not None:
bank = BANK_INDEX[bank]
bank_pos = icx90._get_bank_index(mi)
if not mem.empty and bank is not None and bank_pos is not None:
f.write("%s;%s;%d\n" % (mem.name, bank, bank_pos))
def read_template_file(template_file):
banks_templ = {}
with open(template_file, "r") as f:
for line in f:
l = line.split(";")
l[1] = BANK_INDEX.index(l[1])
banks_templ[l[0]] = l[1:]
return banks_templ
def reorder_banks(icx90, preserve_position, preserve_unknown, banks_templ):
banks_cnt = []
mb = icx90.get_features().memory_bounds
for i in range(0, BANKS):
banks_cnt.append(0)
for mi in range(mb[0], mb[1] + 1):
mem = icx90.get_memory(mi)
if preserve_unknown:
bank = icx90._get_bank(mi)
bank_pos = icx90._get_bank_index(mi)
else:
bank = None
bank_pos = 0
if not mem.empty:
if mem.name in banks_templ:
bank = int(banks_templ[mem.name][0])
if preserve_position:
bank_pos = int(banks_templ[mem.name][1])
else:
bank_pos = banks_cnt[bank]
banks_cnt[bank] += 1
print("%s\t-> %s, %02d" % (mem.name, BANK_INDEX[bank], bank_pos))
# explicitly set non empty channel as visible
icx90.memobj.banks[mi].invisible_channel = 0
if bank >= BANKS or bank_pos >= BANK_NUM:
bank = None
bank_pos = 0
icx90._set_bank(mi, bank)
icx90._set_bank_index(mi, bank_pos)
if __name__ == "__main__":
parser = argparse.ArgumentParser(description="Icom IC-E90 banks handling helper.")
parser.add_argument("icx90_img_file", help="IC-X90 IMG file.")
parser.add_argument("template_file", help="Banks template file.")
parser.add_argument("-r", "--read-banks", action="store_true",
help="Read banks content and store it to template file.")
parser.add_argument("-f", "--fix-banks", action="store_true",
help="Fix banks content, reorder channels in banks according to the provided template file and write it to IMG file.")
parser.add_argument("-p", "--preserve-position", action="store_true",
help="Preserve bank position as in the template file if possible, otherwise put channels to banks according their position in the memory.")
parser.add_argument("-u", "--preserve-unknown", action="store_true",
help="Preserve channels in banks if they aren't in the template file and don't conflicts with the template file, otherwise remove them from the banks.")
parser.add_argument("-b", "--backup_img", action="store_true",
help="Backup IMG file before changing it.")
args = vars(parser.parse_args())
options_ok = True
try:
icx90_img_file = str(args.pop("icx90_img_file"))
template_file = str(args.pop("template_file"))
except KeyError:
options_ok = False
if options_ok and (args["read_banks"] or args["fix_banks"]):
icx90 = ICx90Radio(icx90_img_file)
if args["read_banks"]:
dump_banks(icx90, template_file)
elif args["fix_banks"]:
banks_templ = read_template_file(template_file)
if args["backup_img"]:
icx90.save_mmap(icx90_img_file + ".bak")
reorder_banks(icx90, args["preserve_position"], args["preserve_unknown"], banks_templ)
icx90.save_mmap(icx90_img_file)
else:
parser.print_help()
chirp-20261030/chirp/drivers/id31.py 0000644 0001750 0001750 00000024456 15257672455 015764 0 ustar hibby hibby # Copyright 2012 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 logging
from chirp.drivers import icf
from chirp import directory, bitwise, chirp_common, settings
LOG = logging.getLogger(__name__)
MEM_FORMAT = """
struct {
u24 freq;
u16 offset;
u16 rtone:6,
ctone:6,
unknown2:1,
mode:3;
u8 dtcs;
u8 tune_step:4,
dsql_mode:2,
unknown5:2;
u8 unknown4;
u8 tmode:4,
duplex:2,
dtcs_polarity:2;
char name[16];
u8 digcode;
u8 urcall[7];
u8 rpt1call[7];
u8 rpt2call[7];
} memory[500];
#seekto 0x69C0;
u8 used_flags[70];
#seekto 0x6A06;
u8 skip_flags[69];
#seekto 0x6A4B;
u8 pskp_flags[69];
#seekto 0x6AC0;
struct {
u8 bank;
u8 index;
} banks[500];
#seekto 0x6F50;
struct {
char name[16];
} bank_names[26];
#seekto 0x74BF;
struct {
u8 unknown0;
u24 freq;
u16 offset;
u8 unknown1[3];
u8 call[7];
char name[16];
char subname[8];
u8 unknown3[9];
} repeaters[700];
#seekto 0xFABC;
struct {
u8 call[7];
} rptcall[700];
#seekto 0x10F20;
struct {
char call[8];
char tag[4];
} mycall[6];
#seekto 0x10F68;
struct {
char call[8];
} urcall[200];
"""
TMODES = ["", "Tone", "TSQL", "TSQL", "DTCS", "DTCS", "TSQL-R", "DTCS-R"]
DUPLEX = ["", "-", "+"]
DTCS_POLARITY = ["NN", "NR", "RN", "RR"]
TUNING_STEPS = [5.0, 6.25, 0, 0, 10.0, 12.5, 15.0, 20.0, 25.0, 30.0, 50.0,
100.0, 125.0, 200.0]
DSQL_MODES = ['', 'DSQL', 'CSQL']
def _decode_call(_call):
# Why Icom, why?
return ''.join(chr(x) for x in icf.warp_byte_size(_call, 7, 8))
def _encode_call(call):
return list(icf.warp_byte_size(call.ljust(8), 8, 7))
def _get_freq(_mem):
freq = int(_mem.freq)
offs = int(_mem.offset)
if freq & 0x00200000:
mult = 6250
else:
mult = 5000
if freq & 0x00040000:
omult = 6250
else:
omult = 5000
freq &= 0x0003FFFF
return (freq * mult), (offs * omult)
def _set_freq(_mem, freq, offset):
if chirp_common.is_fractional_step(freq):
mult = 6250
flag = 0x00200000
else:
mult = 5000
flag = 0x00000000
if chirp_common.is_fractional_step(offset):
omult = 6250
oflag = 0x00040000
else:
omult = 5000
oflag = 0x00000000
_mem.freq = (freq // mult) | flag | oflag
_mem.offset = (offset // omult)
class ID31Bank(icf.IcomBank):
"""A ID-31 Bank"""
def get_name(self):
_banks = self._model._radio._memobj.bank_names
return str(_banks[self.index].name).rstrip()
def set_name(self, name):
_banks = self._model._radio._memobj.bank_names
_banks[self.index].name = str(name).ljust(16)[:16]
@directory.register
class ID31Radio(icf.IcomCloneModeRadio, chirp_common.IcomDstarSupport):
"""Icom ID-31"""
MODEL = "ID-31A"
_memsize = 0x15500
_model = "\x33\x22\x00\x01"
_endframe = "Icom Inc\x2E\x41\x38"
_num_banks = 26
_bank_class = ID31Bank
_can_hispeed = True
_icf_data = {
'MapRev': 1,
'EtcData': 0x000005,
}
_ranges = [(0x00000, 0x15500, 32)]
@classmethod
def match_model(cls, filedata, filename):
return (super().match_model(filedata, filename) or
(cls._memsize and len(filedata) == cls._memsize))
MODES = {0: "FM", 1: "NFM", 5: "DV"}
def _get_bank(self, loc):
_bank = self._memobj.banks[loc]
if _bank.bank == 0xFF:
return None
else:
return _bank.bank
def _set_bank(self, loc, bank):
_bank = self._memobj.banks[loc]
if bank is None:
_bank.bank = 0xFF
else:
_bank.bank = bank
def _get_bank_index(self, loc):
_bank = self._memobj.banks[loc]
return _bank.index
def _set_bank_index(self, loc, index):
_bank = self._memobj.banks[loc]
_bank.index = index
def get_features(self):
rf = chirp_common.RadioFeatures()
rf.memory_bounds = (0, 499)
rf.valid_bands = [(400000000, 479000000)]
rf.has_settings = True
rf.has_ctone = True
rf.has_bank_index = True
rf.has_bank_names = True
rf.valid_tmodes = list(set(TMODES))
rf.valid_tuning_steps = sorted([x for x in TUNING_STEPS if x])
rf.valid_modes = list(self.MODES.values())
rf.valid_skips = ["", "S", "P"]
rf.valid_characters = chirp_common.CHARSET_ASCII
rf.valid_name_length = 16
rf.requires_call_lists = False
return rf
def process_mmap(self):
self._memobj = bitwise.parse(MEM_FORMAT, self._mmap)
def get_raw_memory(self, number):
return repr(self._memobj.memory[number])
def _get_mem_extra(self, _mem, mem):
extra = settings.RadioSettingGroup('extra', 'Extra')
rs = settings.RadioSetting(
'dsql', 'DSQL Mode',
settings.RadioSettingValueList(
DSQL_MODES, current_index=int(_mem.dsql_mode)))
extra.append(rs)
return extra
def _set_mem_extra(self, _mem, mem):
try:
_mem.dsql_mode = DSQL_MODES.index(str(mem.extra['dsql'].value))
except (KeyError, TypeError):
# No settings or dsql is not provided
pass
except ValueError:
LOG.error('Invalid extra.dsql_mode: %r' % str(
mem.extra['dsql'].value))
def get_memory(self, number):
_mem = self._memobj.memory[number]
_usd = self._memobj.used_flags[number / 8]
_skp = self._memobj.skip_flags[number / 8]
_psk = self._memobj.pskp_flags[number / 8]
bit = (1 << (number % 8))
if self.MODES[int(_mem.mode)] == "DV":
mem = chirp_common.DVMemory()
else:
mem = chirp_common.Memory()
mem.number = number
if _usd & bit:
mem.empty = True
return mem
mem.freq, mem.offset = _get_freq(_mem)
mem.name = str(_mem.name).rstrip()
mem.rtone = chirp_common.TONES[_mem.rtone]
mem.ctone = chirp_common.TONES[_mem.ctone]
mem.tmode = TMODES[_mem.tmode]
mem.duplex = DUPLEX[_mem.duplex]
mem.dtcs = chirp_common.DTCS_CODES[_mem.dtcs]
mem.dtcs_polarity = DTCS_POLARITY[_mem.dtcs_polarity]
mem.tuning_step = TUNING_STEPS[_mem.tune_step]
mem.mode = self.MODES[int(_mem.mode)]
if mem.mode == "DV":
mem.dv_urcall = _decode_call(_mem.urcall).rstrip()
mem.dv_rpt1call = _decode_call(_mem.rpt1call).rstrip()
mem.dv_rpt2call = _decode_call(_mem.rpt2call).rstrip()
mem.dv_code = int(_mem.digcode)
if _psk & bit:
mem.skip = "P"
elif _skp & bit:
mem.skip = "S"
mem.extra = self._get_mem_extra(_mem, mem)
return mem
def set_memory(self, memory):
_mem = self._memobj.memory[memory.number]
_usd = self._memobj.used_flags[memory.number / 8]
_skp = self._memobj.skip_flags[memory.number / 8]
_psk = self._memobj.pskp_flags[memory.number / 8]
bit = (1 << (memory.number % 8))
if memory.empty:
_usd |= bit
self._set_bank(memory.number, None)
return
_usd &= ~bit
_set_freq(_mem, memory.freq, memory.offset)
_mem.name = memory.name.ljust(16)[:16]
_mem.rtone = chirp_common.TONES.index(memory.rtone)
_mem.ctone = chirp_common.TONES.index(memory.ctone)
_mem.tmode = TMODES.index(memory.tmode)
if TMODES.count(memory.tmode) > 1:
_mem.tmode += 1
_mem.duplex = DUPLEX.index(memory.duplex)
_mem.dtcs = chirp_common.DTCS_CODES.index(memory.dtcs)
_mem.dtcs_polarity = DTCS_POLARITY.index(memory.dtcs_polarity)
_mem.tune_step = TUNING_STEPS.index(memory.tuning_step)
_mem.mode = next(i for i, mode in list(self.MODES.items())
if mode == memory.mode)
if isinstance(memory, chirp_common.DVMemory):
_mem.urcall = _encode_call(memory.dv_urcall.ljust(8))
_mem.rpt1call = _encode_call(memory.dv_rpt1call.ljust(8))
_mem.rpt2call = _encode_call(memory.dv_rpt2call.ljust(8))
_mem.digcode = memory.dv_code
elif memory.mode == "DV":
LOG.debug('Converting Memory to DVMemory with default values')
_mem.urcall = _encode_call('CQCQCQ'.ljust(8))
_mem.rpt1call = _encode_call(' ' * 8)
_mem.rpt2call = _encode_call(' ' * 8)
_mem.digcode = 0
if memory.skip == "S":
_skp |= bit
_psk &= ~bit
elif memory.skip == "P":
_skp |= bit
_psk |= bit
else:
_skp &= ~bit
_psk &= ~bit
self._set_mem_extra(_mem, memory)
def get_urcall_list(self):
calls = []
for i in range(0, 200):
call = str(self._memobj.urcall[i].call)
if call == "CALLSIGN":
call = ""
calls.append(call)
return calls
def get_mycall_list(self):
calls = []
for i in range(0, 6):
calls.append(str(self._memobj.mycall[i].call))
return calls
def get_repeater_call_list(self):
calls = []
for rptcall in self._memobj.rptcall:
call = _decode_call(rptcall.call)
if call.rstrip() and not call == "CALLSIGN":
calls.append(call)
for repeater in self._memobj.repeaters:
call = _decode_call(repeater.call)
if call == "CALLSIGN":
call = ""
calls.append(call.rstrip())
return calls
if __name__ == "__main__":
print(repr(_decode_call(_encode_call("KD7REX B"))))
print(repr(_decode_call(_encode_call(" B"))))
print(repr(_decode_call(_encode_call(" "))))
chirp-20261030/chirp/drivers/id51.py 0000644 0001750 0001750 00000006425 15036407672 015752 0 ustar hibby hibby # Copyright 2012 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 logging
from chirp.drivers import id31
from chirp import directory, bitwise
MEM_FORMAT = """
struct {
u24 freq;
u16 offset;
u16 rtone:6,
ctone:6,
unknown2:1,
mode:3;
u8 dtcs;
u8 tune_step:4,
dsql_mode:2,
unknown5:2;
u8 unknown4;
u8 tmode:4,
duplex:2,
dtcs_polarity:2;
char name[16];
u8 digcode;
u8 urcall[7];
u8 rpt1call[7];
u8 rpt2call[7];
} memory[500];
#seekto 0x6A40;
u8 used_flags[70];
#seekto 0x6A86;
u8 skip_flags[69];
#seekto 0x6ACB;
u8 pskp_flags[69];
#seekto 0x6B40;
struct {
u8 bank;
u8 index;
} banks[500];
#seekto 0x6FD0;
struct {
char name[16];
} bank_names[26];
#seekto 0xA8C0;
struct {
u24 freq;
u16 offset;
u8 unknown1[3];
u8 call[7];
char name[16];
char subname[8];
u8 unknown3[10];
} repeaters[750];
#seekto 0x1384E;
struct {
u8 call[7];
} rptcall[750];
#seekto 0x14E60;
struct {
char call[8];
char tag[4];
} mycall[6];
#seekto 0x14EA8;
struct {
char call[8];
} urcall[200];
"""
LOG = logging.getLogger(__name__)
@directory.register
class ID51Radio(id31.ID31Radio):
"""Icom ID-51"""
MODEL = "ID-51"
_memsize = 0x1FB40
_model = "\x33\x90\x00\x01"
_endframe = "Icom Inc\x2E\x44\x41"
_ranges = [(0x00000, 0x1FB40, 32)]
MODES = {0: "FM", 1: "NFM", 3: "AM", 5: "DV"}
@classmethod
def match_model(cls, filedata, filename):
"""Given contents of a stored file (@filedata), return True if
this radio driver handles the represented model"""
# The default check for ICOM is just to check memory size
# Since the ID-51 and ID-51 Plus/Anniversary have exactly
# the same memory size, we need to do a more detailed check.
if len(filedata) == cls._memsize:
snip = bytes(filedata[0x1AF40:0x1AF60])
if snip == bytes(b'\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF'
b'\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF'
b'\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF'
b'\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF'):
return True
if filename.lower().endswith('.icf'):
return super().match_model(filedata, filename)
return False
def get_features(self):
rf = super(ID51Radio, self).get_features()
rf.valid_bands = [(108000000, 174000000), (400000000, 479000000)]
# The ID-51 D-STAR support seems broken, so disable getting settings
# that will contain the base-class call list support.
rf.has_settings = False
return rf
def process_mmap(self):
self._memobj = bitwise.parse(MEM_FORMAT, self._mmap)
chirp-20261030/chirp/drivers/id5100.py 0000644 0001750 0001750 00000024670 14665027664 016122 0 ustar hibby hibby # Copyright 2022 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 logging
from chirp import chirp_common
from chirp import directory
from chirp.drivers import icf, id31
from chirp import errors
from chirp import bitwise
LOG = logging.getLogger(__name__)
MEM_FORMAT = """
struct {
u24 mult1:3,
mult2:3,
freq:18;
u16 offset;
u16 rtone:6,
ctone:6,
unknown1:1,
mode:3;
u8 dtcs;
u8 tuning_step:4,
unknown2:4;
u8 unknown3;
u8 tmode:4,
duplex:2,
dtcs_polarity:2;
char name[16];
u8 dv_code;
u8 urcall[7];
u8 rpt1call[7];
u8 rpt2call[7];
} memory[1004];
#seekto 0xC040;
u8 usedflags[125];
#seekto 0xC0BE;
u8 skipflags[125];
#seekto 0xC13B;
u8 pskipflags[125];
#seekto 0xC1C0;
struct {
u8 bank;
u8 index;
} bank_info[1000];
#seekto 0xC9C0;
char ICFComment[16];
struct {
char name[16];
} bank_names[26];
"""
TUNE_STEPS = [5.0, 6.25, 8.33, 5.0, 10.0, 12.5, 15.0, 20.0, 25.0, 30.0, 50.0,
5.0, 5.0, 5.0, 5.0]
MODES = ['FM', 'NFM', '2??', 'AM', 'NAM', 'DV', '6??', '7??']
TMODES = ['', 'Tone', '2??', 'TSQL', '4??', 'DTCS', 'TSQL-R', 'DTCS-R',
'DTCS-T', 'Tone->DTCS', 'DTCS->Tone', 'Tone->Tone']
DUPLEX = ['', '-', '+']
DTCS_POL = ['NN', 'NR', 'RN', 'RR']
SPECIALS = ['144-C0', '144-C1', '430-C0', '430-C1']
MULTS = [5000, 6250, 25000 / 3.0]
@directory.register
class ID4100Radio(icf.IcomCloneModeRadio, chirp_common.IcomDstarSupport):
VENDOR = 'Icom'
MODEL = 'ID-4100'
_model = b'\x38\x66\x00\x01'
_endframe = 'Icom Inc.8F'
_memsize = 0x2A3C0
_ranges = [(0, _memsize, 64)]
_raw_frames = True
_highbit_flip = True
_num_banks = 26
_bank_class = id31.ID31Bank
_can_hispeed = True
_icf_data = {
'MapRev': 1,
'EtcData': 0x400001,
}
def process_mmap(self):
self._memobj = bitwise.parse(MEM_FORMAT, self._mmap)
def get_features(self):
rf = chirp_common.RadioFeatures()
rf.has_ctone = True
rf.has_dtcs = True
rf.has_dtcs_polarity = True
rf.has_bank = True
rf.has_bank_index = True
rf.has_bank_names = True
rf.requires_call_lists = False
rf.valid_modes = [x for x in MODES
if '?' not in x]
rf.valid_tmodes = [x for x in TMODES
if '-' not in x and '?' not in x] + ['Cross']
rf.valid_cross_modes = [x for x in TMODES
if '->' in x]
rf.memory_bounds = (0, 999)
rf.valid_bands = [(118000000, 174000000),
(375000000, 550000000)]
rf.valid_skips = ['', 'S', 'P']
rf.valid_characters = chirp_common.CHARSET_ASCII
rf.valid_name_length = 16
rf.valid_tuning_steps = list(sorted(set(TUNE_STEPS)))
rf.valid_special_chans = list(SPECIALS)
return rf
def _get_bank(self, loc):
_bank = self._memobj.bank_info[loc]
_bank.bank &= 0x1F
if _bank.bank == 0x1F:
return None
else:
return _bank.bank
def _set_bank(self, loc, bank):
_bank = self._memobj.bank_info[loc]
if bank is None:
_bank.bank = 0x1F
else:
_bank.bank = bank
def _get_bank_index(self, loc):
_bank = self._memobj.bank_info[loc]
return _bank.index
def _set_bank_index(self, loc, index):
_bank = self._memobj.bank_info[loc]
_bank.index = index
def _get_raw_memory(self, number):
if isinstance(number, str):
number = 1000 + SPECIALS.index(number)
return number, self._memobj.memory[number]
def get_raw_memory(self, number):
num, mem = self._get_raw_memory(number)
return repr(mem)
def get_memory(self, number):
num, _mem = self._get_raw_memory(number)
m = chirp_common.DVMemory()
m.number = num
if isinstance(number, str):
m.extd_number = number
else:
_flg = self._memobj.usedflags[num // 8]
_skp = self._memobj.skipflags[num // 8]
_pskp = self._memobj.pskipflags[num // 8]
m.empty = bool(_flg & (1 << num % 8))
if _pskp & 1 << (num % 8):
m.skip = 'P'
elif _skp & 1 << (num % 8):
m.skip = 'S'
else:
m.skip = ''
mult = MULTS[_mem.mult1]
m.freq = int(_mem.freq * mult)
m.offset = int(_mem.offset * mult)
m.tuning_step = TUNE_STEPS[_mem.tuning_step]
m.name = str(_mem.name).rstrip()
m.mode = MODES[_mem.mode]
m.rtone = chirp_common.TONES[_mem.rtone]
m.ctone = chirp_common.TONES[_mem.ctone]
m.dtcs = chirp_common.DTCS_CODES[_mem.dtcs]
m.dtcs_polarity = DTCS_POL[_mem.dtcs_polarity]
tmode = TMODES[_mem.tmode]
if '->' in tmode:
m.tmode = 'Cross'
m.cross_mode = tmode
elif '-' in tmode and 0:
# FIXME
m.tmode, extra = tmode.split('-')
else:
m.tmode = tmode
m.duplex = DUPLEX[_mem.duplex]
m.dv_code = _mem.dv_code
m.dv_urcall = ''.join(
chr(x) for x in icf.warp_byte_size(_mem.urcall, 7, 8))
m.dv_rpt1call = ''.join(
chr(x) for x in icf.warp_byte_size(_mem.rpt1call, 7, 8))
m.dv_rpt2call = ''.join(
chr(x) for x in icf.warp_byte_size(_mem.rpt2call, 7, 8))
return m
def set_memory(self, mem):
num, _mem = self._get_raw_memory(mem.number)
if num < 1000:
_flg = self._memobj.usedflags[num // 8]
_skp = self._memobj.skipflags[num // 8]
_pskp = self._memobj.pskipflags[num // 8]
mybit = 1 << (num % 8)
_flg |= mybit
_skp &= ~mybit
_pskp &= ~mybit
if mem.empty:
return
_flg &= ~mybit
if mem.skip == 'S':
_skp |= mybit
elif mem.skip == 'P':
_pskp |= mybit
if chirp_common.is_6_25(mem.freq):
mult = MULTS[1]
elif chirp_common.is_8_33(mem.freq):
mult = MULTS[2]
else:
mult = MULTS[0]
_mem.mult1 = _mem.mult2 = MULTS.index(mult)
_mem.freq = mem.freq / mult
_mem.offset = mem.offset / mult
_mem.tuning_step = TUNE_STEPS.index(mem.tuning_step)
_mem.name = mem.name.ljust(16)
_mem.mode = MODES.index(mem.mode)
_mem.rtone = chirp_common.TONES.index(mem.rtone)
_mem.ctone = chirp_common.TONES.index(mem.ctone)
_mem.dtcs = chirp_common.DTCS_CODES.index(mem.dtcs)
_mem.dtcs_polarity = DTCS_POL.index(mem.dtcs_polarity)
if mem.tmode == 'Cross':
_mem.tmode = TMODES.index(mem.cross_mode)
else:
_mem.tmode = TMODES.index(mem.tmode)
_mem.duplex = DUPLEX.index(mem.duplex)
_mem.unknown1 = 0
_mem.unknown2 = 0
_mem.unknown3 = 0
if isinstance(mem, chirp_common.DVMemory):
_mem.dv_code = mem.dv_code
_mem.urcall = list(
icf.warp_byte_size(mem.dv_urcall.ljust(8), 8, 7))
_mem.rpt1call = list(
icf.warp_byte_size(mem.dv_rpt1call.ljust(8), 8, 7))
_mem.rpt2call = list(
icf.warp_byte_size(mem.dv_rpt2call.ljust(8), 8, 7))
@directory.register
class ID5100Radio(ID4100Radio):
MODEL = "ID-5100"
_model = b'\x34\x84\x00\x01'
# This is only for MapRev=1
_endframe = "Icom Inc.EE"
# MapRev=1 Size is 260928 0x3FB40
# MapRev=2 Size is 148160 0x242C0
# MapRev=3 Size is 172928 0x2A380
_memsize = 0x3FB40
_ranges = [(0, _memsize, 64)]
_raw_frames = True
_highbit_flip = True
def process_mmap(self):
was_memsize = self._memsize
# Apparently the 5100 has different reported memory sizes
# depending on firmware version. When we're loading an image,
# we should adjust our MapRev and _ranges to be correct for
# saving ICF files and uploading. This also means we should
# try to detect which version we are talking to and refuse to
# upload an image to a radio with a mismatch.
self._memsize = len(self._mmap)
self._ranges = [(0, self._memsize, 64)]
# Major (memory-format-changing) firmware versions, which
# correspond to MapRev in an ICF file.
maprevs = {
0x3FB40: 1,
0x242C0: 2,
0x2A380: 3,
}
# The 5100 seems to almost behave like three different radios,
# depending on firmware version. These are the endframes that
# are expected for a given MapRev.
endframes = {
1: 'Icom Inc.EE',
2: 'Icom Inc.0C',
3: 'Icom Inc.8E',
}
if self._memsize != was_memsize:
self._icf_data['MapRev'] = maprevs.get(self._memsize, 0)
if self._icf_data['MapRev'] == 0:
LOG.error('Unknown memsize %06X!', self._memsize)
raise errors.InvalidDataError('Unsupported memory format!')
self._endframe = endframes[self._icf_data['MapRev']]
LOG.info('Memory length changed from %06X to %06X; new MapRev=%i',
was_memsize, self._memsize, self._icf_data['MapRev'])
else:
LOG.debug('Unchanged memsize at %06X' % self._memsize)
self._memobj = bitwise.parse(MEM_FORMAT, self._mmap)
@classmethod
def get_prompts(cls):
rp = chirp_common.RadioPrompts()
rp.info = _('This driver has been tested with v3 of the ID-5100. '
'If your radio is not fully updated please help by '
'opening a bug report with a debug log so we can add '
'support for the other revisions.')
rp.pre_upload = rp.info
rp.experimental = rp.info
return rp
chirp-20261030/chirp/drivers/id51plus.py 0000644 0001750 0001750 00000010655 14665027664 016664 0 ustar hibby hibby # Copyright 2015 Eric Dropps
#
# 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.drivers import id31
from chirp import directory, bitwise
LOG = logging.getLogger(__name__)
MEM_FORMAT = """
struct {
u24 freq;
u16 offset;
u16 rtone:6,
ctone:6,
unknown2:1,
mode:3;
u8 dtcs;
u8 tune_step:4,
dsql_mode:2,
unknown5:2;
u8 unknown4;
u8 tmode:4,
duplex:2,
dtcs_polarity:2;
char name[16];
u8 digcode;
u8 urcall[7];
u8 rpt1call[7];
u8 rpt2call[7];
} memory[500];
#seekto 0x6A40;
u8 used_flags[70];
#seekto 0x6A86;
u8 skip_flags[69];
#seekto 0x6ACB;
u8 pskp_flags[69];
#seekto 0x6B40;
struct {
u8 unknown:3,
bank:5;
u8 index;
} banks[500];
#seekto 0x6FD0;
struct {
char name[16];
} bank_names[26];
#seekto 0xA8C0;
struct {
u24 freq;
u16 offset;
u8 unknown1[4];
u8 call[7];
char name[16];
char subname[8];
u8 unknown3[10];
} repeaters[750];
#seekto 0x1384E;
struct {
u8 call[7];
} rptcall[750];
#seekto 0x14FBE;
struct {
char name[16];
} rptgroup_names[30];
#seekto 0x1519E;
struct {
char call[8];
char tag[4];
} mycall[6];
#seekto 0x151E6;
struct {
char call[8];
} urcall[200];
#seekto 0x15826;
struct {
char name[16];
} urcallname[200];
"""
@directory.register
class ID51PLUSRadio(id31.ID31Radio):
"""Icom ID-51 Plus/50th Anniversary"""
MODEL = "ID-51 Plus"
_memsize = 0x1FB40
_model = "\x33\x90\x00\x02"
_endframe = "Icom Inc\x2E\x44\x41"
_bank_class = id31.ID31Bank
_ranges = [(0x00000, 0x1FB40, 32)]
MODES = {0: "FM", 1: "NFM", 3: "AM", 5: "DV"}
@classmethod
def match_model(cls, filedata, filename):
"""Given contents of a stored file (@filedata), return True if
this radio driver handles the represented model"""
# The default check for ICOM is just to check memory size
# Since the ID-51 and ID-51 Plus/Anniversary have exactly
# the same memory size, we need to do a more detailed check.
if len(filedata) == cls._memsize:
snip = bytes(filedata[0x1AF40:0x1AF60])
if snip != bytes(b'\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF'
b'\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF'
b'\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF'
b'\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF'):
return True
return False
def _get_bank(self, loc):
_bank = self._memobj.banks[loc]
LOG.debug("Bank Value for location %s is %s" % (loc, _bank.bank))
if _bank.bank == 0x1F:
return None
else:
return _bank.bank
def _set_bank(self, loc, bank):
_bank = self._memobj.banks[loc]
if bank is None:
_bank.bank = 0x1F
else:
_bank.bank = bank
def get_features(self):
rf = super(ID51PLUSRadio, self).get_features()
rf.valid_bands = [(108000000, 174000000), (380000000, 479000000)]
return rf
def get_repeater_call_list(self):
calls = []
# Unlike previous DStar radios, there is not a separate repeater
# callsign list. It's only the DV Memory banks.
for repeater in self._memobj.repeaters:
call = id31._decode_call(repeater.call)
if call == "CALLSIGN":
call = ""
calls.append(call.rstrip())
return calls
def process_mmap(self):
self._memobj = bitwise.parse(MEM_FORMAT, self._mmap)
@directory.register
class ID51PLUS2Radio(ID51PLUSRadio):
MODEL = 'ID-51 Plus2'
_model = b'\x33\x90\x00\x03'
_endframe = b'Icom Inc.DA'
_raw_frames = True
_highbit_flip = True
_icf_data = {
'MapRev': 1,
'EtcData': 0x400001,
}
@classmethod
def match_model(cls, filedata, filename):
# This model is always matched with metadata
return False
chirp-20261030/chirp/drivers/id800.py 0000644 0001750 0001750 00000026474 15257672455 016052 0 ustar hibby hibby # 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 .
from chirp.drivers import icf
from chirp import chirp_common, errors, directory, bitwise
MEM_FORMAT = """
#seekto 0x0020;
struct {
u24 freq;
u16 offset;
u8 power:2,
rtone:6;
u8 duplex:2,
ctone:6;
u8 dtcs;
u8 tuning_step:4,
unknown1:4;
u8 unknown2;
u8 mult_flag:1,
unknown3:5,
tmode:2;
u16 dtcs_polarity:2,
usealpha:1,
empty:1,
name1:6,
name2:6;
u24 name3:6,
name4:6,
name5:6,
name6:6;
u8 unknown5;
u8 unknown6:1,
digital_code:7;
u8 urcall;
u8 rpt1call;
u8 rpt2call;
u8 unknown7:1,
mode:3,
unknown8:4;
} memory[512];
#seekto 0x2BF4;
struct {
u8 unknown1:1,
empty:1,
pskip:1,
skip:1,
bank:4;
} flags[512];
#seekto 0x3220;
struct {
char call[8];
} mycalls[8];
#seekto 0x3250;
struct {
char call[8];
} urcalls[99];
#seekto 0x3570;
struct {
char call[8];
} rptcalls[59];
"""
MODES = ["FM", "NFM", "AM", "NAM", "DV", "FM", "FM", "FM"]
TMODES = ["", "Tone", "TSQL", "DTCS"]
DUPLEX = ["", "", "-", "+"]
DTCS_POL = ["NN", "NR", "RN", "RR"]
STEPS = [5.0, 10.0, 12.5, 15, 20.0, 25.0, 30.0, 50.0, 100.0, 200.0, 6.25]
POWER_LEVELS = [chirp_common.PowerLevel('High', watts=50),
chirp_common.PowerLevel('Low', watts=5),
chirp_common.PowerLevel('Mid', watts=15)]
ID800_SPECIAL = {
"C2": 510,
"C1": 511,
}
ID800_SPECIAL_REV = {
510: "C2",
511: "C1",
}
for i in range(0, 5):
idA = "%iA" % (i + 1)
idB = "%iB" % (i + 1)
num = 500 + i * 2
ID800_SPECIAL[idA] = num
ID800_SPECIAL[idB] = num + 1
ID800_SPECIAL_REV[num] = idA
ID800_SPECIAL_REV[num+1] = idB
ALPHA_CHARSET = " ABCDEFGHIJKLMNOPQRSTUVWXYZ"
NUMERIC_CHARSET = "0123456789+-=*/()|"
def get_name(_mem):
"""Decode the name from @_mem"""
def _get_char(val):
if val == 0:
return " "
elif val & 0x20:
return ALPHA_CHARSET[val & 0x1F]
else:
return NUMERIC_CHARSET[val & 0x0F]
name_bytes = [_mem.name1, _mem.name2, _mem.name3,
_mem.name4, _mem.name5, _mem.name6]
name = ""
for val in name_bytes:
name += _get_char(val)
return name.rstrip()
def set_name(_mem, name):
"""Encode @name in @_mem"""
def _get_index(char):
if char == " ":
return 0
elif char.isalpha():
return ALPHA_CHARSET.index(char) | 0x20
else:
return NUMERIC_CHARSET.index(char) | 0x10
name = name.ljust(6)[:6]
_mem.usealpha = bool(name.strip())
# The element override calling convention makes this harder to automate.
# It's just six, so do it manually
_mem.name1 = _get_index(name[0])
_mem.name2 = _get_index(name[1])
_mem.name3 = _get_index(name[2])
_mem.name4 = _get_index(name[3])
_mem.name5 = _get_index(name[4])
_mem.name6 = _get_index(name[5])
@directory.register
class ID800v2Radio(icf.IcomCloneModeRadio, chirp_common.IcomDstarSupport):
"""Icom ID800"""
VENDOR = "Icom"
MODEL = "ID-800H"
VARIANT = "v2"
_model = "\x27\x88\x02\x00"
_memsize = 14528
_endframe = "Icom Inc\x2eCB"
_can_hispeed = True
_memories = []
@classmethod
def match_model(cls, filedata, filename):
return (super().match_model(filedata, filename) or
(cls._memsize and len(filedata) == cls._memsize))
_ranges = [(0x0020, 0x2B18, 32),
(0x2B18, 0x2B20, 8),
(0x2B20, 0x2BE0, 32),
(0x2BE0, 0x2BF4, 20),
(0x2BF4, 0x2C00, 12),
(0x2C00, 0x2DE0, 32),
(0x2DE0, 0x2DF4, 20),
(0x2DF4, 0x2E00, 12),
(0x2E00, 0x2E20, 32),
(0x2F00, 0x3070, 32),
(0x30D0, 0x30E0, 16),
(0x30E0, 0x3160, 32),
(0x3160, 0x3180, 16),
(0x3180, 0x31A0, 32),
(0x31A0, 0x31B0, 16),
(0x3220, 0x3240, 32),
(0x3240, 0x3260, 16),
(0x3260, 0x3560, 32),
(0x3560, 0x3580, 16),
(0x3580, 0x3720, 32),
(0x3720, 0x3780, 8),
(0x3798, 0x37A0, 8),
(0x37A0, 0x37B0, 16),
(0x37B0, 0x37B1, 1),
(0x37D8, 0x37E0, 8),
(0x37E0, 0x3898, 32),
(0x3898, 0x389A, 2),
(0x38A8, 0x38C0, 16), ]
MYCALL_LIMIT = (1, 7)
URCALL_LIMIT = (1, 99)
RPTCALL_LIMIT = (1, 59)
def _get_bank(self, loc):
_flg = self._memobj.flags[loc-1]
if _flg.bank >= 0x0A:
return None
else:
return _flg.bank
def _set_bank(self, loc, bank):
_flg = self._memobj.flags[loc-1]
if bank is None:
_flg.bank = 0x0A
else:
_flg.bank = bank
def get_features(self):
rf = chirp_common.RadioFeatures()
rf.has_implicit_calls = True
rf.has_settings = True
rf.has_bank = True
rf.valid_modes = [x for x in MODES if x]
rf.valid_tmodes = ["", "Tone", "TSQL", "DTCS"]
rf.valid_duplexes = ["", "-", "+"]
rf.valid_tuning_steps = list(STEPS)
rf.valid_bands = [(118000000, 173995000), (230000000, 549995000),
(810000000, 999990000)]
rf.valid_skips = ["", "S", "P"]
rf.valid_characters = chirp_common.CHARSET_UPPER_NUMERIC + '*'
rf.valid_name_length = 6
rf.valid_special_chans = sorted(ID800_SPECIAL.keys())
rf.valid_power_levels = list(POWER_LEVELS)
rf.memory_bounds = (1, 499)
return rf
def process_mmap(self):
self._memobj = bitwise.parse(MEM_FORMAT, self._mmap)
def get_memory(self, number):
if isinstance(number, str):
try:
number = ID800_SPECIAL[number] + 1 # Because we subtract below
except KeyError:
raise errors.InvalidMemoryLocation("Unknown channel %s" %
number)
_mem = self._memobj.memory[number-1]
_flg = self._memobj.flags[number-1]
if MODES[_mem.mode] == "DV":
urcalls = self.get_urcall_list()
rptcalls = self.get_repeater_call_list()
mem = chirp_common.DVMemory()
mem.dv_urcall = urcalls[_mem.urcall]
mem.dv_rpt1call = rptcalls[_mem.rpt1call]
mem.dv_rpt2call = rptcalls[_mem.rpt2call]
mem.dv_code = _mem.digital_code
else:
mem = chirp_common.Memory()
mem.number = number
if _flg.empty:
mem.empty = True
return mem
mult = _mem.mult_flag and 6250 or 5000
mem.freq = _mem.freq * mult
mem.offset = _mem.offset * 5000
mem.duplex = DUPLEX[_mem.duplex]
mem.mode = MODES[_mem.mode]
mem.tmode = TMODES[_mem.tmode]
mem.rtone = chirp_common.TONES[_mem.rtone]
mem.ctone = chirp_common.TONES[_mem.ctone]
mem.dtcs = chirp_common.DTCS_CODES[_mem.dtcs]
mem.dtcs_polarity = DTCS_POL[_mem.dtcs_polarity]
mem.tuning_step = STEPS[_mem.tuning_step]
mem.name = get_name(_mem)
mem.power = POWER_LEVELS[_mem.power]
mem.skip = _flg.pskip and "P" or _flg.skip and "S" or ""
return mem
def set_memory(self, mem):
_mem = self._memobj.memory[mem.number-1]
_flg = self._memobj.flags[mem.number-1]
_flg.empty = mem.empty
if mem.empty:
self._set_bank(mem.number, None)
return
mult = chirp_common.is_fractional_step(mem.freq) and 6250 or 5000
_mem.mult_flag = mult == 6250
_mem.freq = mem.freq / mult
_mem.offset = mem.offset / 5000
_mem.duplex = DUPLEX.index(mem.duplex)
_mem.mode = MODES.index(mem.mode)
_mem.tmode = TMODES.index(mem.tmode)
_mem.rtone = chirp_common.TONES.index(mem.rtone)
_mem.ctone = chirp_common.TONES.index(mem.ctone)
_mem.dtcs = chirp_common.DTCS_CODES.index(mem.dtcs)
_mem.dtcs_polarity = DTCS_POL.index(mem.dtcs_polarity)
_mem.tuning_step = STEPS.index(mem.tuning_step)
if mem.power in POWER_LEVELS:
_mem.power = POWER_LEVELS.index(mem.power)
set_name(_mem, mem.name)
_flg.pskip = mem.skip == "P"
_flg.skip = mem.skip == "S"
if mem.mode == "DV":
urcalls = self.get_urcall_list()
rptcalls = self.get_repeater_call_list()
if not isinstance(mem, chirp_common.DVMemory):
raise errors.InvalidDataError("DV mode is not a DVMemory!")
try:
err = mem.dv_urcall
_mem.urcall = urcalls.index(mem.dv_urcall)
err = mem.dv_rpt1call
_mem.rpt1call = rptcalls.index(mem.dv_rpt1call)
err = mem.dv_rpt2call
_mem.rpt2call = rptcalls.index(mem.dv_rpt2call)
except IndexError:
raise errors.InvalidDataError("DV Call %s not in list" % err)
else:
_mem.urcall = 0
_mem.rpt1call = 0
_mem.rpt2call = 0
def sync_in(self):
icf.IcomCloneModeRadio.sync_in(self)
self.process_mmap()
def get_raw_memory(self, number):
return repr(self._memobj.memory[number-1])
def get_urcall_list(self):
calls = ["CQCQCQ"]
for i in range(*self.URCALL_LIMIT):
calls.append(str(self._memobj.urcalls[i-1].call).rstrip())
return calls
def get_repeater_call_list(self):
calls = ["*NOTUSE*"]
for i in range(*self.RPTCALL_LIMIT):
calls.append(str(self._memobj.rptcalls[i-1].call).rstrip())
return calls
def get_mycall_list(self):
calls = []
for i in range(*self.MYCALL_LIMIT):
calls.append(str(self._memobj.mycalls[i-1].call).rstrip())
return calls
def set_urcall_list(self, calls):
for i in range(*self.URCALL_LIMIT):
try:
call = calls[i].upper() # Skip the implicit CQCQCQ
except IndexError:
call = " " * 8
self._memobj.urcalls[i-1].call = call.ljust(8)[:8]
def set_repeater_call_list(self, calls):
for i in range(*self.RPTCALL_LIMIT):
try:
call = calls[i].upper() # Skip the implicit blank
except IndexError:
call = " " * 8
self._memobj.rptcalls[i-1].call = call.ljust(8)[:8]
def set_mycall_list(self, calls):
for i in range(*self.MYCALL_LIMIT):
try:
call = calls[i-1].upper()
except IndexError:
call = " " * 8
self._memobj.mycalls[i-1].call = call.ljust(8)[:8]
chirp-20261030/chirp/drivers/id880.py 0000644 0001750 0001750 00000026605 14665027664 016054 0 ustar hibby hibby # 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 .
from chirp.drivers import icf
from chirp import chirp_common, directory, bitwise
MEM_FORMAT = """
struct {
u24 rxmult:3,
txmult:3,
freq:18;
u16 offset;
u16 rtone:6,
ctone:6,
unknown1:1,
mode:3;
u8 dtcs;
u8 tune_step:4,
unknown2:4;
u8 unknown3;
u8 unknown4:1,
tmode:3,
duplex:2,
dtcs_polarity:2;
char name[8];
u8 unknwon5:1,
digital_code:7;
char urcall[7];
char r1call[7];
char r2call[7];
} memory[1000];
#seekto 0xAA80;
u8 used_flags[132];
#seekto 0xAB04;
u8 skip_flags[132];
u8 pskip_flags[132];
#seekto 0xAD00;
struct {
u8 bank;
u8 index;
} bank_info[1000];
#seekto 0xB550;
struct {
char name[6];
} bank_names[26];
#seekto 0xDE56;
struct {
char call[8];
char extension[4];
} mycall[6];
struct {
char call[8];
} urcall[60];
struct {
char call[8];
char extension[4];
} rptcall[99];
#seekto 0x0FB8;
u8 name_flags[132];
"""
TMODES = ["", "Tone", "TSQL", "TSQL", "DTCS", "DTCS", "TSQL-R", "DTCS-R"]
DUPLEX = ["", "-", "+", ""]
DTCSP = ["NN", "NR", "RN", "RR"]
MODES = ["FM", "NFM", "WFM", "AM", "NAM", "DV"]
STEPS = [5.0, 6.25, 8.33, 9.0, 10.0, 12.5, 15.0, 20.0, 25.0, 30.0, 50.0,
100.0, 125.0, 200.0]
FREQ_MULTIPLIER = [5000, 6250, 6250, 8333, 9000]
def decode_call(sevenbytes):
"""Decode a callsign from a packed region @sevenbytes"""
if len(sevenbytes) != 7:
raise Exception("%i (!=7) bytes to decode_call" % len(sevenbytes))
i = 0
rem = 0
call = ""
for byte in [ord(x) for x in sevenbytes]:
i += 1
# Mask is 0x01, 0x03, 0x07, etc
mask = (1 << i) - 1
# Code gets the upper bits of remainder plus all but the i lower
# bits of this byte
code = (byte >> i) | rem
call += chr(code)
# Remainder for next time are the masked bits, moved to the high
# places for the next round
rem = (byte & mask) << 7 - i
# After seven trips gathering overflow bits, we should have seven
# left, which is the final character
call += chr(rem)
return call.rstrip()
def encode_call(call):
"""Encode @call into a 7-byte region"""
call = call.ljust(8)
buf = []
for i in range(0, 8):
byte = ord(call[i])
if i > 0:
last = buf[i-1]
himask = ~((1 << (7-i)) - 1) & 0x7F
last |= (byte & himask) >> (7-i)
buf[i-1] = last
else:
himask = 0
buf.append((byte & ~himask) << (i+1))
return "".join([chr(x) for x in buf[:7]])
def _decode_freq(freq, mult):
return int(freq) * FREQ_MULTIPLIER[mult]
def _encode_freq(freq):
for i, step in list(enumerate(FREQ_MULTIPLIER)):
if freq % step == 0:
return freq / step, i
raise ValueError("%d cannot be factored by multiplier table." % freq)
def _wipe_memory(mem, char):
mem.set_raw(char * (mem.size() // 8))
class ID880Bank(icf.IcomNamedBank):
"""ID880 Bank"""
def get_name(self):
_bank = self._model._radio._memobj.bank_names[self.index]
return str(_bank.name).rstrip()
def set_name(self, name):
_bank = self._model._radio._memobj.bank_names[self.index]
_bank.name = name.ljust(6)[:6]
@directory.register
class ID880Radio(icf.IcomCloneModeRadio, chirp_common.IcomDstarSupport):
"""Icom ID880"""
VENDOR = "Icom"
MODEL = "ID-880H"
_model = "\x31\x67\x00\x01"
_memsize = 62976
_endframe = "Icom Inc\x2eB1"
_ranges = [(0x0000, 0xF5c0, 32),
(0xF5c0, 0xf5e0, 16),
(0xf5e0, 0xf600, 32)]
_num_banks = 26
_bank_class = ID880Bank
_can_hispeed = True
MYCALL_LIMIT = (1, 7)
URCALL_LIMIT = (1, 60)
RPTCALL_LIMIT = (1, 99)
def _get_bank(self, loc):
_bank = self._memobj.bank_info[loc]
if _bank.bank == 0xFF:
return None
else:
return _bank.bank
def _set_bank(self, loc, bank):
_bank = self._memobj.bank_info[loc]
if bank is None:
_bank.bank = 0xFF
else:
_bank.bank = bank
def _get_bank_index(self, loc):
_bank = self._memobj.bank_info[loc]
return _bank.index
def _set_bank_index(self, loc, index):
_bank = self._memobj.bank_info[loc]
_bank.index = index
def process_mmap(self):
self._memobj = bitwise.parse(MEM_FORMAT, self._mmap)
def get_features(self):
rf = chirp_common.RadioFeatures()
rf.requires_call_lists = False
rf.has_settings = True
rf.has_bank = True
rf.has_bank_index = True
rf.has_bank_names = True
rf.valid_modes = ["FM", "NFM", "AM", "NAM", "DV"]
rf.valid_tmodes = ['', 'Tone', 'TSQL', 'DTCS', 'TSQL-R', 'DTCS-R']
rf.valid_duplexes = list(DUPLEX)
rf.valid_tuning_steps = STEPS
rf.valid_bands = [(118000000, 173995000), (230000000, 549995000),
(810000000, 823990000), (849000000, 868990000),
(894000000, 999990000)]
rf.valid_skips = ["", "S", "P"]
rf.valid_name_length = 8
rf.valid_characters = chirp_common.CHARSET_UPPER_NUMERIC + \
r"!\"#$%&'()*+,-./:;<=>?@[\]^"
rf.memory_bounds = (0, 999)
return rf
def get_raw_memory(self, number):
return repr(self._memobj.memory[number])
def get_memory(self, number):
bytepos = number / 8
bitpos = 1 << (number % 8)
_mem = self._memobj.memory[number]
_used = self._memobj.used_flags[bytepos]
is_used = ((_used & bitpos) == 0)
if is_used and MODES[_mem.mode] == "DV":
mem = chirp_common.DVMemory()
mem.dv_urcall = decode_call(str(_mem.urcall))
mem.dv_rpt1call = decode_call(str(_mem.r1call))
mem.dv_rpt2call = decode_call(str(_mem.r2call))
else:
mem = chirp_common.Memory()
mem.number = number
if number < 1000:
_skip = self._memobj.skip_flags[bytepos]
_pskip = self._memobj.pskip_flags[bytepos]
if _skip & bitpos:
mem.skip = "S"
elif _pskip & bitpos:
mem.skip = "P"
else:
pass # FIXME: Special memories
if not is_used:
mem.empty = True
return mem
mem.freq = _decode_freq(_mem.freq, _mem.rxmult)
mem.offset = _decode_freq(_mem.offset, _mem.txmult)
mem.rtone = chirp_common.TONES[_mem.rtone]
mem.ctone = chirp_common.TONES[_mem.ctone]
mem.tmode = TMODES[_mem.tmode]
mem.duplex = DUPLEX[_mem.duplex]
mem.mode = MODES[_mem.mode]
mem.dtcs = chirp_common.DTCS_CODES[_mem.dtcs]
mem.dtcs_polarity = DTCSP[_mem.dtcs_polarity]
if _mem.tune_step >= len(STEPS):
mem.tuning_step = 5.0
else:
mem.tuning_step = STEPS[_mem.tune_step]
mem.name = str(_mem.name).rstrip()
return mem
def set_memory(self, mem):
bitpos = (1 << (mem.number % 8))
bytepos = mem.number / 8
_mem = self._memobj.memory[mem.number]
_used = self._memobj.used_flags[bytepos]
_namf = self._memobj.name_flags[bytepos]
was_empty = _used & bitpos
if mem.empty:
_used |= bitpos
_wipe_memory(_mem, "\xFF")
self._set_bank(mem.number, None)
return
_used &= ~bitpos
if was_empty:
_wipe_memory(_mem, "\x00")
_mem.freq, _mem.rxmult = _encode_freq(mem.freq)
_mem.offset, _mem.txmult = _encode_freq(mem.offset)
_mem.rtone = chirp_common.TONES.index(mem.rtone)
_mem.ctone = chirp_common.TONES.index(mem.ctone)
_mem.tmode = TMODES.index(mem.tmode)
if mem.tmode == 'DTCS':
# The first index of DTCS is "bell" mode
_mem.tmode += 1
_mem.duplex = DUPLEX.index(mem.duplex)
_mem.mode = MODES.index(mem.mode)
_mem.dtcs = chirp_common.DTCS_CODES.index(mem.dtcs)
_mem.dtcs_polarity = DTCSP.index(mem.dtcs_polarity)
_mem.tune_step = STEPS.index(mem.tuning_step)
_mem.name = mem.name.ljust(8)
if mem.name.strip():
_namf |= bitpos
else:
_namf &= ~bitpos
if isinstance(mem, chirp_common.DVMemory):
_mem.urcall = encode_call(mem.dv_urcall)
_mem.r1call = encode_call(mem.dv_rpt1call)
_mem.r2call = encode_call(mem.dv_rpt2call)
if mem.number < 1000:
skip = self._memobj.skip_flags[bytepos]
pskip = self._memobj.pskip_flags[bytepos]
if mem.skip == "S":
skip |= bitpos
else:
skip &= ~bitpos
if mem.skip == "P":
pskip |= bitpos
else:
pskip &= ~bitpos
def get_urcall_list(self):
_calls = self._memobj.urcall
calls = ["CQCQCQ"]
for i in range(*self.URCALL_LIMIT):
calls.append(str(_calls[i-1].call))
return calls
def get_mycall_list(self):
_calls = self._memobj.mycall
calls = []
for i in range(*self.MYCALL_LIMIT):
calls.append(str(_calls[i-1].call))
return calls
def get_repeater_call_list(self):
calls = ["*NOTUSE*"]
for _i in range(*self.RPTCALL_LIMIT):
# FIXME: Not sure where the repeater list actually is
calls.append("UNSUPRTD")
continue
return calls
@classmethod
def match_model(cls, filedata, filename):
if (super().match_model(filedata, filename) and
filename.lower().endswith('.icf')):
return True
# This is a horrid hack, given that people can change the GPS-A
# destination, but it should suffice in most cases until we get
# a rich container file format
return len(filedata) == cls._memsize and b"API880," in filedata
# This radio isn't really supported yet and detects as a conflict with
# the ID-880. So, don't register right now
@directory.register
class ID80Radio(ID880Radio):
"""Icom ID80"""
MODEL = "ID-80H"
_model = "\x31\x55\x00\x01"
@classmethod
def match_model(cls, filedata, filename):
if (super().match_model(filedata, filename) and
filename.lower().endswith('.icf')):
return True
# This is a horrid hack, given that people can change the GPS-A
# destination, but it should suffice in most cases until we get
# a rich container file format
return len(filedata) == cls._memsize and b"API80," in filedata
def get_features(self):
rf = super().get_features()
rf.valid_modes = ["FM", "NFM", "WFM", "AM", "NAM", "DV"]
rf.valid_bands = [(495000, 1000000000)]
return rf
chirp-20261030/chirp/drivers/idrp.py 0000644 0001750 0001750 00000011357 14665027664 016154 0 ustar hibby hibby # 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 logging
from chirp import chirp_common, errors, util
LOG = logging.getLogger(__name__)
def parse_frames(buf):
"""Parse frames from the radio"""
frames = []
while b"\xfe\xfe" in buf:
try:
start = buf.index(b"\xfe\xfe")
end = buf[start:].index(b"\xfd") + start + 1
except Exception:
LOG.error("Unable to parse frames (buffer %i)", len(buf))
break
frames.append(buf[start:end])
buf = buf[end:]
return frames
def send(pipe, buf):
"""Send data in @buf to @pipe"""
pipe.write(b"\xfe\xfe%s\xfd" % buf)
pipe.flush()
data = b""
while True:
buf = pipe.read(4096)
if not buf:
break
data += buf
LOG.debug("Got: \n%s" % util.hexprint(buf))
LOG.debug('Sent %i bytes, received %i', len(buf), len(data))
return parse_frames(data)
def send_magic(pipe):
"""Send the magic wakeup call to @pipe"""
LOG.debug('Sending magic wakeup')
send(pipe, (b"\xfe" * 15) + b"\x01\x7f\x19")
def drain(pipe):
"""Chew up any data waiting on @pipe"""
while True:
buf = pipe.read(4096)
if not buf:
break
def set_freq(pipe, freq):
"""Set the frequency of the radio on @pipe to @freq"""
freqbcd = util.bcd_encode(freq, bigendian=False, width=9)
buf = b"\x01\x7f\x05" + freqbcd
drain(pipe)
send_magic(pipe)
resp = send(pipe, buf)
for frame in resp:
if len(frame) == 6:
if frame[4] == 251:
return True
raise errors.InvalidDataError("Repeater reported error")
def get_freq(pipe):
"""Get the frequency of the radio attached to @pipe"""
buf = b"\x01\x7f\x1a\x09"
drain(pipe)
send_magic(pipe)
resp = send(pipe, buf)
for frame in resp:
if frame[4] == 3:
els = frame[5:10]
freq = int("%02x%02x%02x%02x%02x" % (els[4],
els[3],
els[2],
els[1],
els[0]))
LOG.debug("Freq: %f" % freq)
return freq
else:
LOG.debug('Unhandled frame type %i', frame[4])
raise errors.InvalidDataError("No frequency frame received")
RP_IMMUTABLE = ["number", "skip", "bank", "extd_number", "name", "rtone",
"ctone", "dtcs", "tmode", "dtcs_polarity", "skip", "duplex",
"offset", "mode", "tuning_step", "bank_index"]
class IDRPx000V(chirp_common.LiveRadio):
"""Icom IDRP-*"""
BAUD_RATE = 19200
VENDOR = "Icom"
MODEL = "ID-2000V/4000V/2D/2V"
_model = "0000" # Unknown
mem_upper_limit = 0
def get_features(self):
rf = chirp_common.RadioFeatures()
rf.valid_modes = ["DV"]
rf.valid_tmodes = []
rf.valid_characters = ""
rf.valid_duplexes = [""]
rf.valid_name_length = 0
rf.valid_skips = []
rf.valid_tuning_steps = []
rf.has_bank = False
rf.has_ctone = False
rf.has_dtcs = False
rf.has_dtcs_polarity = False
rf.has_mode = False
rf.has_name = False
rf.has_offset = False
rf.has_tuning_step = False
rf.memory_bounds = (0, 0)
return rf
def get_memory(self, number):
if number != 0:
raise errors.InvalidMemoryLocation("Repeaters have only one slot")
mem = chirp_common.Memory()
mem.number = 0
mem.freq = get_freq(self.pipe)
mem.name = "TX/RX"
mem.mode = "DV"
mem.offset = 0.0
mem.immutable = RP_IMMUTABLE
return mem
def set_memory(self, mem):
if mem.number != 0:
raise errors.InvalidMemoryLocation("Repeaters have only one slot")
set_freq(self.pipe, mem.freq)
def do_test():
"""Get the frequency of /dev/icom"""
ser = serial.Serial(port="/dev/icom", baudrate=19200, timeout=0.5)
# set_freq(pipe, 439.920)
get_freq(ser)
if __name__ == "__main__":
do_test()
chirp-20261030/chirp/drivers/iradio_common.py 0000644 0001750 0001750 00000003321 15172631605 020013 0 ustar hibby hibby # Copyright 2023 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 .
"""common function for iRadio (or similar) radios"""
import logging
from chirp import errors
LOG = logging.getLogger(__name__)
CMD_ACK = b"\x06"
def enter_programming_mode(serial, magic, timeout=0.25):
previous_timeout = serial.timeout
serial.timeout = timeout
try:
for attempt in range(0, 5):
serial.write(magic)
ack = serial.read(1)
if ack == CMD_ACK:
return
if not ack and attempt == 4:
raise errors.RadioNoResponse()
LOG.debug(f"Attempt #{attempt + 1} failed, trying again")
# retries all failed, raise error
msg = "The radio did not accept program mode after five tries.\n"
msg += "Check you interface cable and power cycle your radio."
raise errors.RadioError(msg)
finally:
serial.timeout = previous_timeout
def exit_programming_mode(serial, magic):
try:
serial.write(magic)
except Exception:
raise errors.RadioError("Radio refused to exit programming mode")
chirp-20261030/chirp/drivers/iradio_uv_5118.py 0000644 0001750 0001750 00000121563 15161444633 017645 0 ustar hibby hibby # Copyright 2022 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 .
import struct
import logging
from chirp import chirp_common, directory, memmap, checksum
from chirp import bitwise, errors, util
from chirp.settings import RadioSetting, RadioSettingGroup, \
RadioSettingValueInteger, RadioSettingValueList, \
RadioSettingValueBoolean, RadioSettingValueFloat, \
RadioSettings
from chirp.drivers.iradio_common import enter_programming_mode, \
exit_programming_mode
LOG = logging.getLogger(__name__)
MEM_FORMAT = """
#seekto 0x0010;
struct {
u8 range174_180; // 174-180 MHz
u8 range180_190; // 180-190 MHz
u8 range190_200; // 190-200 MHz
u8 range200_210; // 200-210 MHz
u8 range210_220; // 210-220 MHz
u8 range220_230; // 220-230 MHz
u8 range230_240; // 230-240 MHz
u8 range240_250; // 240-250 MHz
u8 range250_260; // 240-250 MHz
u8 range260_270; // 260-270 MHz
u8 range270_280; // 270-280 MHz
u8 range280_290; // 280-290 MHz
u8 range290_300; // 290-300 MHz
u8 range300_310; // 300-310 MHz
u8 range310_320; // 310-320 MHz
u8 range320_330; // 320-330 MHz
u8 range330_340; // 330-340 MHz
u8 range340_350; // 340-350 MHz
u8 range350_360; // 350-360 MHz
u8 range360_370; // 360-370 MHz
u8 range370_380; // 370-380 MHz
u8 range380_390; // 380-390 MHz
u8 range390_400; // 290-400 MHz
u8 range520_530; // 520-530 MHz
u8 range530_540; // 530-540 MHz
u8 range540_550; // 540-550 MHz
u8 range550_560; // 550-560 MHz
u8 range560_570; // 560-570 MHz
u8 range570_580; // 570-580 MHz
u8 range580_590; // 580-590 MHz
u8 range590_600; // 590-600 MHz
u8 range600_610; // 600-610 MHz
u8 range610_620; // 610-620 MHz
u8 range620_630; // 620-630 MHz
u8 range630_640; // 630-640 MHz
u8 range640_650; // 640-650 MHz
u8 range650_660; // 650-660 MHz
} txallow;
#seekto 0x0039;
struct {
u8 display_timer; // 0x0039 Display Timer
u8 led_timer; // 0x003A LED Timer
u8 auto_lock; // 0x003B Auto Lock
u8 pfkey1short; // 0x003C FS1 Short
u8 pfkey1long; // 0x003D FS1 Long
u8 pfkey2short; // 0x003E FS2 Short
u8 pfkey2long; // 0x003F FS2 Long
u8 unknown_0; // 0x0040
u8 voice:2, // 0x0041 Voice Prompt
beep:1, // Beep Switch
roger:2, // Roger Tone
save:3; // Power Save
u8 dispmode:1, // 0x0042 Display Mode
dstandby:1, // Dual Standby
unknown_1:1,
standby:1, // Radio Standby
squelch:4; // Squelch Level
u8 vox_level:4, // 0x0043 VOX Level
vox_delay:4; // VOX Delay
u8 brightness:2, // 0x0044 Screen Light
unknown_2:1,
alarm:1, // Alarm
scramble:4; // Scramble
u8 screen:1, // 0x0045 Screen On-off
led:1, // LED On-off
key_lock:1, // Key Lock
radio:1, // Radio On-off
compander:1, // Compander
rp:1, // Offset
freq_lock:1, // Freq Lock
vox:1; // VOX On-off
u8 unknown_4:4, // 0x0046
txpri:1, // TX PRI
unknown_5:1,
tail_tone:2; // Tail Tone
ul16 fmcur; // 0x0047 Radio Freq
u8 unknown_49; // 0x0049
u8 unknown_4A; // 0x004A
u8 unknown_4B; // 0x004B
u8 unknown_4C; // 0x004C
u8 dsrangea; // 0x004D Dual Standby Range A
u8 dsrangeb; // 0x004E Dual Standby Range B
u8 tot; // 0x004F
} settings;
struct memory {
ul32 rxfreq; // 00-03
ul16 rx_tone; // PL/DPL Decode // 04-05
ul32 txfreq; // 06-09
ul16 tx_tone; // PL/DPL Encode // 0a-0b
u8 lowpower:1, // Power Level // 0c
isnarrow:1, // Bandwidth
unknown1:1,
unknown2:3,
bcl:1, // Busy Channel Lockout
scan:1; // Scan Add
u8 unknown3[3]; // 0d-0f
};
// #seekto 0x0050;
struct memory channels[128];
"""
CMD_ACK = b"\x06"
RB15_DTCS = tuple(sorted(chirp_common.DTCS_CODES + (645,)))
_STEP_LIST = [0.25, 1.25, 2.5, 5., 6.25, 10., 12.5, 25., 50., 100.]
LIST_ALARM = ["Local", "Remote"]
LIST_BRIGHT = ["Low", "Middle", "High"]
LIST_DISPM = ["Frequency", "Channel Number"]
LIST_ROGER = ["Off", "Start", "End", "Start and End"]
LIST_SAVE = ["Off", "1:1", "1:2", "1:3", "1:4", "1:5"]
LIST_SCRAMBLE = ["Off"] + ["%s" % x for x in range(1, 9)]
LIST_SKEY = ["None", "Monitor", "Local Alarm", "Remote Alarm",
"Screen On/Off", "Frequency Lock"]
LIST_TIMER = ["Off"] + ["%s seconds" % x for x in range(15, 615, 15)]
LIST_TXPRI = ["Edit", "Busy"]
LIST_VOICE = ["Off", "Chinese", "English"]
TXALLOW_CHOICES = ["RX Only", "TX/RX"]
TXALLOW_VALUES = [0xFF, 0x00]
def _read_block(radio, block_addr, block_size):
serial = radio.pipe
cmd = struct.pack(">BH", ord(b'R'), block_addr + 0x0340)
ccs = bytes([checksum.checksum_8bit(cmd)])
expectedresponse = b"R" + cmd[1:]
cmd = cmd + ccs
LOG.debug("Reading block %04x..." % (block_addr + 0x0340))
try:
serial.write(cmd)
response = serial.read(3 + block_size + 1)
cs = checksum.checksum_8bit(response[:-1])
if response[:3] != expectedresponse:
raise Exception("Error reading block %04x." % (block_addr +
0x0340))
chunk = response[3:]
if chunk[-1] != cs:
raise Exception("Block failed checksum!")
block_data = chunk[:-1]
except:
raise errors.RadioError("Failed to read block at %04x" % block_addr +
0x0340)
return block_data
def _write_block(radio, block_addr, block_size):
serial = radio.pipe
data = radio.get_mmap()[block_addr:block_addr + block_size]
cmd = struct.pack(">BH", ord(b'W'), block_addr + 0x0340)
cs = bytes([checksum.checksum_8bit(cmd + data)])
data += cs
LOG.debug("Writing Data:")
LOG.debug(util.hexprint(cmd + data))
try:
serial.write(cmd + data)
if serial.read(1) != CMD_ACK:
raise Exception("No ACK")
except:
raise errors.RadioError("Failed to send block "
"to radio at %04x" % (block_addr + 0x0340))
def do_download(radio):
LOG.debug("download")
enter_programming_mode(radio.pipe, radio.magic_enter,
radio.program_timeout)
data = b""
status = chirp_common.Status()
status.msg = "Cloning from radio"
status.cur = 0
status.max = radio._memsize
for addr in range(0, radio._memsize, radio.BLOCK_SIZE):
status.cur = addr + radio.BLOCK_SIZE
radio.status_fn(status)
block = _read_block(radio, addr, radio.BLOCK_SIZE)
data += block
LOG.debug("Address: %04x" % addr)
LOG.debug(util.hexprint(block))
exit_programming_mode(radio.pipe, radio.magic_exit)
return memmap.MemoryMapBytes(data)
def do_upload(radio):
status = chirp_common.Status()
status.msg = "Uploading to radio"
enter_programming_mode(radio.pipe, radio.magic_enter,
radio.program_timeout)
status.cur = 0
status.max = radio._memsize
for start_addr, end_addr in radio._ranges:
for addr in range(start_addr, end_addr, radio.BLOCK_SIZE):
status.cur = addr + radio.BLOCK_SIZE
radio.status_fn(status)
_write_block(radio, addr, radio.BLOCK_SIZE)
exit_programming_mode(radio.pipe, radio.magic_exit)
class IradioUV5118(chirp_common.CloneModeRadio):
"""IRADIO UV5118"""
VENDOR = "Iradio"
MODEL = "UV-5118"
BAUD_RATE = 9600
BLOCK_SIZE = 0x10
magic_enter = b"93" + b"\x05\x10\x81"
magic_exit = b"93" + b"\x05\xEE\x5F"
program_timeout = 0.75
VALID_BANDS = [(20000000, 64000000), # RX only
(108000000, 136000000), # RX only (Air Band)
(136000000, 174000000), # TX/RX (VHF)
(174000000, 400000000), # TX/RX
(400000000, 520000000), # TX/RX (UHF)
(520000000, 660000000)] # TX/RX
POWER_LEVELS = [chirp_common.PowerLevel("High", watts=2.00),
chirp_common.PowerLevel("Low", watts=0.50)]
# Radio's memory starts at 0x0340
# Radio's memory ends at 0x0B90
_ranges = [
(0x0000, 0x0850),
]
_memsize = 0x0850
_upper = 128
def get_features(self):
rf = chirp_common.RadioFeatures()
rf.has_settings = True
rf.has_bank = False
rf.has_ctone = True
rf.has_cross = True
rf.has_rx_dtcs = True
rf.has_tuning_step = False
rf.can_odd_split = True
rf.has_name = False
rf.valid_skips = ["", "S"]
rf.valid_tmodes = ["", "Tone", "TSQL", "DTCS", "Cross"]
rf.valid_cross_modes = ["Tone->Tone", "Tone->DTCS", "DTCS->Tone",
"->Tone", "->DTCS", "DTCS->", "DTCS->DTCS"]
rf.valid_power_levels = self.POWER_LEVELS
rf.valid_duplexes = ["", "-", "+", "split"]
rf.valid_modes = ["FM", "NFM"] # 25 kHz, 12.5 kHz.
rf.valid_dtcs_codes = RB15_DTCS
rf.memory_bounds = (1, self._upper)
rf.valid_tuning_steps = _STEP_LIST
rf.valid_bands = self.VALID_BANDS
return rf
def process_mmap(self):
self._memobj = bitwise.parse(MEM_FORMAT, self._mmap)
def sync_in(self):
"""Download from radio"""
try:
data = do_download(self)
except errors.RadioError:
# Pass through any real errors we raise
raise
except:
# If anything unexpected happens, make sure we raise
# a RadioError and log the problem
LOG.exception('Unexpected error during download')
raise errors.RadioError('Unexpected error communicating '
'with the radio')
self._mmap = data
self.process_mmap()
def sync_out(self):
"""Upload to radio"""
try:
do_upload(self)
except:
# If anything unexpected happens, make sure we raise
# a RadioError and log the problem
LOG.exception('Unexpected error during upload')
raise errors.RadioError('Unexpected error communicating '
'with the radio')
def get_raw_memory(self, number):
return repr(self._memobj.memory[number - 1])
@staticmethod
def _decode_tone(toneval):
# DCS examples:
# D023N - 1013 - 0001 0000 0001 0011
# ^-DCS
# D023I - 2013 - 0010 0000 0001 0100
# ^--DCS inverted
# D754I - 21EC - 0010 0001 1110 1100
# code in octal-------^^^^^^^^^^^
if toneval == 0x3000:
return '', None, None
elif toneval & 0x1000:
# DTCS N
code = int('%o' % (toneval & 0x1FF))
return 'DTCS', code, 'N'
elif toneval & 0x2000:
# DTCS R
code = int('%o' % (toneval & 0x1FF))
return 'DTCS', code, 'R'
else:
return 'Tone', toneval / 10.0, None
@staticmethod
def _encode_tone(mode, val, pol):
if not mode:
return 0x3000
elif mode == 'Tone':
return int(val * 10)
elif mode == 'DTCS':
code = int('%i' % val, 8)
if pol == 'N':
code |= 0x1800
if pol == 'R':
code |= 0x2800
return code
else:
raise errors.RadioError('Unsupported tone mode %r' % mode)
def get_memory(self, number):
mem = chirp_common.Memory()
_mem = self._memobj.channels[number - 1]
mem.number = number
mem.freq = int(_mem.rxfreq) * 10
# We'll consider any blank (i.e. 0 MHz frequency) to be empty
if mem.freq == 0:
mem.empty = True
return mem
if _mem.rxfreq.get_raw() == b"\xFF\xFF\xFF\xFF":
mem.freq = 0
mem.empty = True
return mem
if _mem.get_raw() == (b"\xFF" * 16):
LOG.debug("Initializing empty memory")
_mem.set_raw("\xFF" * 4 + "\x00\x30" + "\xFF" * 4 + "\x00\x30" +
"\x00" * 4)
# Freq and offset
mem.freq = int(_mem.rxfreq) * 10
# TX freq set
offset = (int(_mem.txfreq) * 10) - mem.freq
if offset != 0:
if chirp_common.is_split(self.get_features().valid_bands,
mem.freq, int(_mem.txfreq) * 10):
mem.duplex = "split"
mem.offset = int(_mem.txfreq) * 10
elif offset < 0:
mem.offset = abs(offset)
mem.duplex = "-"
elif offset > 0:
mem.offset = offset
mem.duplex = "+"
else:
mem.offset = 0
mem.mode = _mem.isnarrow and "NFM" or "FM"
chirp_common.split_tone_decode(mem,
self._decode_tone(_mem.tx_tone),
self._decode_tone(_mem.rx_tone))
mem.power = self.POWER_LEVELS[_mem.lowpower]
if not _mem.scan:
mem.skip = "S"
mem.extra = RadioSettingGroup("Extra", "extra")
rs = RadioSetting("bcl", "BCL",
RadioSettingValueBoolean(_mem.bcl))
mem.extra.append(rs)
return mem
def set_memory(self, mem):
LOG.debug("Setting %i(%s)" % (mem.number, mem.extd_number))
_mem = self._memobj.channels[mem.number - 1]
# if empty memory
if mem.empty:
_mem.set_raw("\xFF" * 16)
return
_mem.set_raw("\xFF" * 4 + "\x00\x30" + "\xFF" * 4 + "\x00\x30" +
"\x00" * 4)
_mem.rxfreq = mem.freq / 10
if mem.duplex == "split":
_mem.txfreq = mem.offset / 10
elif mem.duplex == "+":
_mem.txfreq = (mem.freq + mem.offset) / 10
elif mem.duplex == "-":
_mem.txfreq = (mem.freq - mem.offset) / 10
else:
_mem.txfreq = mem.freq / 10
_mem.scan = mem.skip != "S"
_mem.isnarrow = mem.mode == "NFM"
txtone, rxtone = chirp_common.split_tone_encode(mem)
_mem.tx_tone = self._encode_tone(*txtone)
_mem.rx_tone = self._encode_tone(*rxtone)
_mem.lowpower = mem.power == self.POWER_LEVELS[1]
for setting in mem.extra:
setattr(_mem, setting.get_name(), setting.value)
def get_settings(self):
_settings = self._memobj.settings
_txallow = self._memobj.txallow
basic = RadioSettingGroup("basic", "Basic Settings")
sidekey = RadioSettingGroup("sidekey", "Side Key Settings")
dwatch = RadioSettingGroup("dwatch", "Dual Watch Settings")
txallow = RadioSettingGroup("txallow", "TX Allow")
top = RadioSettings(basic, sidekey, dwatch, txallow)
voice = RadioSetting("voice", "Language", RadioSettingValueList(
LIST_VOICE, current_index=_settings.voice))
basic.append(voice)
beep = RadioSetting("beep", "Key Beep",
RadioSettingValueBoolean(_settings.beep))
basic.append(beep)
roger = RadioSetting("roger", "Roger Tone",
RadioSettingValueList(
LIST_ROGER, current_index=_settings.roger))
basic.append(roger)
save = RadioSetting("save", "Battery Save",
RadioSettingValueList(
LIST_SAVE, current_index=_settings.save))
basic.append(save)
dispmode = RadioSetting("dispmode", "Display Mode",
RadioSettingValueList(
LIST_DISPM,
current_index=_settings.dispmode))
basic.append(dispmode)
brightness = RadioSetting("brightness", "Screen Light",
RadioSettingValueList(
LIST_BRIGHT,
current_index=_settings.brightness))
basic.append(brightness)
screen = RadioSetting("screen", "Screen",
RadioSettingValueBoolean(_settings.screen))
basic.append(screen)
display_timer = RadioSetting(
"display_timer", "Display Timer",
RadioSettingValueList(
LIST_TIMER, current_index=_settings.display_timer))
basic.append(display_timer)
led = RadioSetting("led", "LED",
RadioSettingValueBoolean(_settings.led))
basic.append(led)
led_timer = RadioSetting("led_timer", "LED Timer",
RadioSettingValueList(
LIST_TIMER,
current_index=_settings.led_timer))
basic.append(led_timer)
squelch = RadioSetting("squelch", "Squelch Level",
RadioSettingValueInteger(
0, 9, _settings.squelch))
basic.append(squelch)
vox = RadioSetting("vox", "VOX",
RadioSettingValueBoolean(_settings.vox))
basic.append(vox)
vox_level = RadioSetting("vox_level", "VOX Level",
RadioSettingValueInteger(
0, 9, _settings.vox_level))
basic.append(vox_level)
vox_delay = RadioSetting("vox_delay", "VOX Delay",
RadioSettingValueInteger(
0, 9, _settings.vox_delay))
basic.append(vox_delay)
compander = RadioSetting("compander", "Compander",
RadioSettingValueBoolean(_settings.compander))
basic.append(compander)
scramble = RadioSetting("scramble", "Scramble",
RadioSettingValueList(
LIST_SCRAMBLE,
current_index=_settings.scramble))
basic.append(scramble)
rp = RadioSetting("rp", "Offset",
RadioSettingValueBoolean(_settings.rp))
basic.append(rp)
txpri = RadioSetting("txpri", "TX Priority",
RadioSettingValueList(
LIST_TXPRI,
current_index=_settings.txpri))
basic.append(txpri)
tot = RadioSetting("tot", "Time-out Timer",
RadioSettingValueList(
LIST_TIMER,
current_index=_settings.tot))
basic.append(tot)
tail_tone = RadioSetting("tail_tone", "Tail Tone",
RadioSettingValueInteger(
0, 3, _settings.tail_tone))
basic.append(tail_tone)
alarm = RadioSetting("alarm", "Alarm",
RadioSettingValueList(
LIST_ALARM,
current_index=_settings.alarm))
basic.append(alarm)
freq_lock = RadioSetting("freq_lock", "Frequency Lock",
RadioSettingValueBoolean(_settings.freq_lock))
basic.append(freq_lock)
key_lock = RadioSetting("key_lock", "Key Lock",
RadioSettingValueBoolean(_settings.key_lock))
basic.append(key_lock)
auto_lock = RadioSetting("auto_lock", "Auto Lock Timer",
RadioSettingValueList(
LIST_TIMER,
current_index=_settings.auto_lock))
basic.append(auto_lock)
radio = RadioSetting("radio", "Broadcast FM Radio",
RadioSettingValueBoolean(_settings.radio))
basic.append(radio)
standby = RadioSetting("standby", "Radio Standby",
RadioSettingValueBoolean(_settings.standby))
basic.append(standby)
def myset_freq(setting, obj, atrb, mult):
""" Callback to set frequency by applying multiplier"""
value = int(float(str(setting.value)) * mult)
setattr(obj, atrb, value)
return
# FM Broadcast Settings
val = _settings.fmcur
val = val / 10.0
if val < 87.5 or val > 108.0:
val = 90.4
rx = RadioSettingValueFloat(87.5, 108.0, val, 0.1, 1)
rset = RadioSetting("fmcur", "Broadcast FM Freq (MHz)", rx)
rset.set_apply_callback(myset_freq, _settings, "fmcur", 10)
basic.append(rset)
# Side Key Settings
pfkey1short = RadioSetting("pfkey1short", "Side Key 1 - Short Press",
RadioSettingValueList(
LIST_SKEY,
current_index=_settings.pfkey1short))
sidekey.append(pfkey1short)
pfkey1long = RadioSetting("pfkey1long", "Side Key 1 - Long Press",
RadioSettingValueList(
LIST_SKEY,
current_index=_settings.pfkey1long))
sidekey.append(pfkey1long)
pfkey2short = RadioSetting("pfkey2short", "Side Key 2 - Short Press",
RadioSettingValueList(
LIST_SKEY,
current_index=_settings.pfkey2short))
sidekey.append(pfkey2short)
pfkey2long = RadioSetting("pfkey2long", "Side Key 2 - Long Press",
RadioSettingValueList(
LIST_SKEY,
current_index=_settings.pfkey2long))
sidekey.append(pfkey2long)
dstandby = RadioSetting("dstandby", "Dual Standby",
RadioSettingValueBoolean(_settings.dstandby))
dwatch.append(dstandby)
dsrangea = RadioSetting("dsrangea", "Dual Standby Range A",
RadioSettingValueInteger(
1, 128, _settings.dsrangea + 1))
dwatch.append(dsrangea)
dsrangeb = RadioSetting("dsrangeb", "Dual Standby Range B",
RadioSettingValueInteger(
1, 128, _settings.dsrangeb + 1))
dwatch.append(dsrangeb)
def apply_txallow_listvalue(setting, obj):
LOG.debug("Setting value: " + str(
setting.value) + " from list")
val = str(setting.value)
index = TXALLOW_CHOICES.index(val)
val = TXALLOW_VALUES[index]
obj.set_value(val)
if _txallow.range174_180 in TXALLOW_VALUES:
idx = TXALLOW_VALUES.index(_txallow.range174_180)
else:
idx = TXALLOW_VALUES.index(0xFF)
rs = RadioSettingValueList(TXALLOW_CHOICES, current_index=idx)
rset = RadioSetting("txallow.range174_180", "174-180 MHz", rs)
rset.set_apply_callback(apply_txallow_listvalue, _txallow.range174_180)
txallow.append(rset)
if _txallow.range180_190 in TXALLOW_VALUES:
idx = TXALLOW_VALUES.index(_txallow.range180_190)
else:
idx = TXALLOW_VALUES.index(0xFF)
rs = RadioSettingValueList(TXALLOW_CHOICES, current_index=idx)
rset = RadioSetting("txallow.range180_190", "180-190 MHz", rs)
rset.set_apply_callback(apply_txallow_listvalue, _txallow.range180_190)
txallow.append(rset)
if _txallow.range190_200 in TXALLOW_VALUES:
idx = TXALLOW_VALUES.index(_txallow.range190_200)
else:
idx = TXALLOW_VALUES.index(0xFF)
rs = RadioSettingValueList(TXALLOW_CHOICES, current_index=idx)
rset = RadioSetting("txallow.range190_200", "190-200 MHz", rs)
rset.set_apply_callback(apply_txallow_listvalue, _txallow.range190_200)
txallow.append(rset)
if _txallow.range200_210 in TXALLOW_VALUES:
idx = TXALLOW_VALUES.index(_txallow.range200_210)
else:
idx = TXALLOW_VALUES.index(0xFF)
rs = RadioSettingValueList(TXALLOW_CHOICES, current_index=idx)
rset = RadioSetting("txallow.range200_210", "200-210 MHz", rs)
rset.set_apply_callback(apply_txallow_listvalue, _txallow.range200_210)
txallow.append(rset)
if _txallow.range210_220 in TXALLOW_VALUES:
idx = TXALLOW_VALUES.index(_txallow.range210_220)
else:
idx = TXALLOW_VALUES.index(0xFF)
rs = RadioSettingValueList(TXALLOW_CHOICES, current_index=idx)
rset = RadioSetting("txallow.range210_220", "210-220 MHz", rs)
rset.set_apply_callback(apply_txallow_listvalue, _txallow.range210_220)
txallow.append(rset)
if _txallow.range220_230 in TXALLOW_VALUES:
idx = TXALLOW_VALUES.index(_txallow.range220_230)
else:
idx = TXALLOW_VALUES.index(0xFF)
rs = RadioSettingValueList(TXALLOW_CHOICES, current_index=idx)
rset = RadioSetting("txallow.range220_230", "220-230 MHz", rs)
rset.set_apply_callback(apply_txallow_listvalue, _txallow.range220_230)
txallow.append(rset)
if _txallow.range230_240 in TXALLOW_VALUES:
idx = TXALLOW_VALUES.index(_txallow.range230_240)
else:
idx = TXALLOW_VALUES.index(0xFF)
rs = RadioSettingValueList(TXALLOW_CHOICES, current_index=idx)
rset = RadioSetting("txallow.range230_240", "230-240 MHz", rs)
rset.set_apply_callback(apply_txallow_listvalue, _txallow.range230_240)
txallow.append(rset)
if _txallow.range240_250 in TXALLOW_VALUES:
idx = TXALLOW_VALUES.index(_txallow.range240_250)
else:
idx = TXALLOW_VALUES.index(0xFF)
rs = RadioSettingValueList(TXALLOW_CHOICES, current_index=idx)
rset = RadioSetting("txallow.range240_250", "240-250 MHz", rs)
rset.set_apply_callback(apply_txallow_listvalue, _txallow.range240_250)
txallow.append(rset)
if _txallow.range250_260 in TXALLOW_VALUES:
idx = TXALLOW_VALUES.index(_txallow.range250_260)
else:
idx = TXALLOW_VALUES.index(0xFF)
rs = RadioSettingValueList(TXALLOW_CHOICES, current_index=idx)
rset = RadioSetting("txallow.range250_260", "250-260 MHz", rs)
rset.set_apply_callback(apply_txallow_listvalue, _txallow.range250_260)
txallow.append(rset)
if _txallow.range260_270 in TXALLOW_VALUES:
idx = TXALLOW_VALUES.index(_txallow.range260_270)
else:
idx = TXALLOW_VALUES.index(0xFF)
rs = RadioSettingValueList(TXALLOW_CHOICES, current_index=idx)
rset = RadioSetting("txallow.range260_270", "260-270 MHz", rs)
rset.set_apply_callback(apply_txallow_listvalue, _txallow.range260_270)
txallow.append(rset)
if _txallow.range270_280 in TXALLOW_VALUES:
idx = TXALLOW_VALUES.index(_txallow.range270_280)
else:
idx = TXALLOW_VALUES.index(0xFF)
rs = RadioSettingValueList(TXALLOW_CHOICES, current_index=idx)
rset = RadioSetting("txallow.range270_280", "270-280 MHz", rs)
rset.set_apply_callback(apply_txallow_listvalue, _txallow.range270_280)
txallow.append(rset)
if _txallow.range280_290 in TXALLOW_VALUES:
idx = TXALLOW_VALUES.index(_txallow.range280_290)
else:
idx = TXALLOW_VALUES.index(0xFF)
rs = RadioSettingValueList(TXALLOW_CHOICES, current_index=idx)
rset = RadioSetting("txallow.range280_290", "280-290 MHz", rs)
rset.set_apply_callback(apply_txallow_listvalue, _txallow.range280_290)
txallow.append(rset)
if _txallow.range290_300 in TXALLOW_VALUES:
idx = TXALLOW_VALUES.index(_txallow.range290_300)
else:
idx = TXALLOW_VALUES.index(0xFF)
rs = RadioSettingValueList(TXALLOW_CHOICES, current_index=idx)
rset = RadioSetting("txallow.range290_300", "290-300 MHz", rs)
rset.set_apply_callback(apply_txallow_listvalue, _txallow.range290_300)
txallow.append(rset)
if _txallow.range300_310 in TXALLOW_VALUES:
idx = TXALLOW_VALUES.index(_txallow.range300_310)
else:
idx = TXALLOW_VALUES.index(0xFF)
rs = RadioSettingValueList(TXALLOW_CHOICES, current_index=idx)
rset = RadioSetting("txallow.range300_310", "300-310 MHz", rs)
rset.set_apply_callback(apply_txallow_listvalue, _txallow.range300_310)
txallow.append(rset)
if _txallow.range310_320 in TXALLOW_VALUES:
idx = TXALLOW_VALUES.index(_txallow.range310_320)
else:
idx = TXALLOW_VALUES.index(0xFF)
rs = RadioSettingValueList(TXALLOW_CHOICES, current_index=idx)
rset = RadioSetting("txallow.range310_320", "310-320 MHz", rs)
rset.set_apply_callback(apply_txallow_listvalue, _txallow.range310_320)
txallow.append(rset)
if _txallow.range320_330 in TXALLOW_VALUES:
idx = TXALLOW_VALUES.index(_txallow.range320_330)
else:
idx = TXALLOW_VALUES.index(0xFF)
rs = RadioSettingValueList(TXALLOW_CHOICES, current_index=idx)
rset = RadioSetting("txallow.range320_330", "320-330 MHz", rs)
rset.set_apply_callback(apply_txallow_listvalue, _txallow.range320_330)
txallow.append(rset)
if _txallow.range330_340 in TXALLOW_VALUES:
idx = TXALLOW_VALUES.index(_txallow.range330_340)
else:
idx = TXALLOW_VALUES.index(0xFF)
rs = RadioSettingValueList(TXALLOW_CHOICES, current_index=idx)
rset = RadioSetting("txallow.range330_340", "330-340 MHz", rs)
rset.set_apply_callback(apply_txallow_listvalue, _txallow.range330_340)
txallow.append(rset)
if _txallow.range340_350 in TXALLOW_VALUES:
idx = TXALLOW_VALUES.index(_txallow.range340_350)
else:
idx = TXALLOW_VALUES.index(0xFF)
rs = RadioSettingValueList(TXALLOW_CHOICES, current_index=idx)
rset = RadioSetting("txallow.range340_350", "340-350 MHz", rs)
rset.set_apply_callback(apply_txallow_listvalue, _txallow.range340_350)
txallow.append(rset)
if _txallow.range350_360 in TXALLOW_VALUES:
idx = TXALLOW_VALUES.index(_txallow.range350_360)
else:
idx = TXALLOW_VALUES.index(0xFF)
rs = RadioSettingValueList(TXALLOW_CHOICES, current_index=idx)
rset = RadioSetting("txallow.range350_360", "350-360 MHz", rs)
rset.set_apply_callback(apply_txallow_listvalue, _txallow.range350_360)
txallow.append(rset)
if _txallow.range360_370 in TXALLOW_VALUES:
idx = TXALLOW_VALUES.index(_txallow.range360_370)
else:
idx = TXALLOW_VALUES.index(0xFF)
rs = RadioSettingValueList(TXALLOW_CHOICES, current_index=idx)
rset = RadioSetting("txallow.range360_370", "360-370 MHz", rs)
rset.set_apply_callback(apply_txallow_listvalue, _txallow.range360_370)
txallow.append(rset)
if _txallow.range370_380 in TXALLOW_VALUES:
idx = TXALLOW_VALUES.index(_txallow.range370_380)
else:
idx = TXALLOW_VALUES.index(0xFF)
rs = RadioSettingValueList(TXALLOW_CHOICES, current_index=idx)
rset = RadioSetting("txallow.range370_380", "370-380 MHz", rs)
rset.set_apply_callback(apply_txallow_listvalue, _txallow.range370_380)
txallow.append(rset)
if _txallow.range380_390 in TXALLOW_VALUES:
idx = TXALLOW_VALUES.index(_txallow.range380_390)
else:
idx = TXALLOW_VALUES.index(0xFF)
rs = RadioSettingValueList(TXALLOW_CHOICES, current_index=idx)
rset = RadioSetting("txallow.range380_390", "380-390 MHz", rs)
rset.set_apply_callback(apply_txallow_listvalue, _txallow.range380_390)
txallow.append(rset)
if _txallow.range390_400 in TXALLOW_VALUES:
idx = TXALLOW_VALUES.index(_txallow.range390_400)
else:
idx = TXALLOW_VALUES.index(0xFF)
rs = RadioSettingValueList(TXALLOW_CHOICES, current_index=idx)
rset = RadioSetting("txallow.range390_400", "390-400 MHz", rs)
rset.set_apply_callback(apply_txallow_listvalue, _txallow.range390_400)
txallow.append(rset)
if _txallow.range520_530 in TXALLOW_VALUES:
idx = TXALLOW_VALUES.index(_txallow.range520_530)
else:
idx = TXALLOW_VALUES.index(0xFF)
rs = RadioSettingValueList(TXALLOW_CHOICES, current_index=idx)
rset = RadioSetting("txallow.range520_530", "520-530 MHz", rs)
rset.set_apply_callback(apply_txallow_listvalue, _txallow.range520_530)
txallow.append(rset)
if _txallow.range530_540 in TXALLOW_VALUES:
idx = TXALLOW_VALUES.index(_txallow.range530_540)
else:
idx = TXALLOW_VALUES.index(0xFF)
rs = RadioSettingValueList(TXALLOW_CHOICES, current_index=idx)
rset = RadioSetting("txallow.range530_540", "530-540 MHz", rs)
rset.set_apply_callback(apply_txallow_listvalue, _txallow.range530_540)
txallow.append(rset)
if _txallow.range540_550 in TXALLOW_VALUES:
idx = TXALLOW_VALUES.index(_txallow.range540_550)
else:
idx = TXALLOW_VALUES.index(0xFF)
rs = RadioSettingValueList(TXALLOW_CHOICES, current_index=idx)
rset = RadioSetting("txallow.range540_550", "540-550 MHz", rs)
rset.set_apply_callback(apply_txallow_listvalue, _txallow.range540_550)
txallow.append(rset)
if _txallow.range550_560 in TXALLOW_VALUES:
idx = TXALLOW_VALUES.index(_txallow.range550_560)
else:
idx = TXALLOW_VALUES.index(0xFF)
rs = RadioSettingValueList(TXALLOW_CHOICES, current_index=idx)
rset = RadioSetting("txallow.range550_560", "550-560 MHz", rs)
rset.set_apply_callback(apply_txallow_listvalue, _txallow.range550_560)
txallow.append(rset)
if _txallow.range560_570 in TXALLOW_VALUES:
idx = TXALLOW_VALUES.index(_txallow.range560_570)
else:
idx = TXALLOW_VALUES.index(0xFF)
rs = RadioSettingValueList(TXALLOW_CHOICES, current_index=idx)
rset = RadioSetting("txallow.range560_570", "560-570 MHz", rs)
rset.set_apply_callback(apply_txallow_listvalue, _txallow.range560_570)
txallow.append(rset)
if _txallow.range570_580 in TXALLOW_VALUES:
idx = TXALLOW_VALUES.index(_txallow.range570_580)
else:
idx = TXALLOW_VALUES.index(0xFF)
rs = RadioSettingValueList(TXALLOW_CHOICES, current_index=idx)
rset = RadioSetting("txallow.range570_580", "570-580 MHz", rs)
rset.set_apply_callback(apply_txallow_listvalue, _txallow.range570_580)
txallow.append(rset)
if _txallow.range580_590 in TXALLOW_VALUES:
idx = TXALLOW_VALUES.index(_txallow.range580_590)
else:
idx = TXALLOW_VALUES.index(0xFF)
rs = RadioSettingValueList(TXALLOW_CHOICES, current_index=idx)
rset = RadioSetting("txallow.range580_590", "580-590 MHz", rs)
rset.set_apply_callback(apply_txallow_listvalue, _txallow.range580_590)
txallow.append(rset)
if _txallow.range590_600 in TXALLOW_VALUES:
idx = TXALLOW_VALUES.index(_txallow.range590_600)
else:
idx = TXALLOW_VALUES.index(0xFF)
rs = RadioSettingValueList(TXALLOW_CHOICES, current_index=idx)
rset = RadioSetting("txallow.range590_600", "590-600 MHz", rs)
rset.set_apply_callback(apply_txallow_listvalue, _txallow.range590_600)
txallow.append(rset)
if _txallow.range600_610 in TXALLOW_VALUES:
idx = TXALLOW_VALUES.index(_txallow.range600_610)
else:
idx = TXALLOW_VALUES.index(0xFF)
rs = RadioSettingValueList(TXALLOW_CHOICES, current_index=idx)
rset = RadioSetting("txallow.range600_610", "600-610 MHz", rs)
rset.set_apply_callback(apply_txallow_listvalue, _txallow.range600_610)
txallow.append(rset)
if _txallow.range610_620 in TXALLOW_VALUES:
idx = TXALLOW_VALUES.index(_txallow.range610_620)
else:
idx = TXALLOW_VALUES.index(0xFF)
rs = RadioSettingValueList(TXALLOW_CHOICES, current_index=idx)
rset = RadioSetting("txallow.range610_620", "610-620 MHz", rs)
rset.set_apply_callback(apply_txallow_listvalue, _txallow.range610_620)
txallow.append(rset)
if _txallow.range620_630 in TXALLOW_VALUES:
idx = TXALLOW_VALUES.index(_txallow.range620_630)
else:
idx = TXALLOW_VALUES.index(0xFF)
rs = RadioSettingValueList(TXALLOW_CHOICES, current_index=idx)
rset = RadioSetting("txallow.range620_630", "620-630 MHz", rs)
rset.set_apply_callback(apply_txallow_listvalue, _txallow.range620_630)
txallow.append(rset)
if _txallow.range630_640 in TXALLOW_VALUES:
idx = TXALLOW_VALUES.index(_txallow.range630_640)
else:
idx = TXALLOW_VALUES.index(0xFF)
rs = RadioSettingValueList(TXALLOW_CHOICES, current_index=idx)
rset = RadioSetting("txallow.range630_640", "630-640 MHz", rs)
rset.set_apply_callback(apply_txallow_listvalue, _txallow.range630_640)
txallow.append(rset)
if _txallow.range640_650 in TXALLOW_VALUES:
idx = TXALLOW_VALUES.index(_txallow.range640_650)
else:
idx = TXALLOW_VALUES.index(0xFF)
rs = RadioSettingValueList(TXALLOW_CHOICES, current_index=idx)
rset = RadioSetting("txallow.range640_650", "640-650 MHz", rs)
rset.set_apply_callback(apply_txallow_listvalue, _txallow.range640_650)
txallow.append(rset)
if _txallow.range650_660 in TXALLOW_VALUES:
idx = TXALLOW_VALUES.index(_txallow.range650_660)
else:
idx = TXALLOW_VALUES.index(0xFF)
rs = RadioSettingValueList(TXALLOW_CHOICES, current_index=idx)
rset = RadioSetting("txallow.range650_660", "650-660 MHz", rs)
rset.set_apply_callback(apply_txallow_listvalue, _txallow.range650_660)
txallow.append(rset)
return top
def set_settings(self, settings):
for element in settings:
if not isinstance(element, RadioSetting):
self.set_settings(element)
continue
else:
try:
if "." in element.get_name():
bits = element.get_name().split(".")
obj = self._memobj
for bit in bits[:-1]:
obj = getattr(obj, bit)
setting = bits[-1]
else:
obj = self._memobj.settings
setting = element.get_name()
if element.has_apply_callback():
LOG.debug("Using apply callback")
element.run_apply_callback()
elif setting == "dsrangea":
setattr(obj, setting, int(element.value) - 1)
elif setting == "dsrangeb":
setattr(obj, setting, int(element.value) - 1)
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
@classmethod
def match_model(cls, filedata, filename):
# This radio has always been post-metadata, so never do
# old-school detection
return False
@directory.register
class AbbreeAR518Radio(IradioUV5118):
"""ABBREE AR518"""
VENDOR = "Abbree"
MODEL = "AR-518"
chirp-20261030/chirp/drivers/iradio_uv_5118plus.py 0000644 0001750 0001750 00000110426 15172631605 020544 0 ustar hibby hibby # Copyright 2023 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 .
import struct
import logging
from chirp import chirp_common, directory, memmap, checksum
from chirp import bitwise, errors, util
from chirp.settings import RadioSetting, RadioSettingGroup, \
RadioSettingValueInteger, RadioSettingValueList, \
RadioSettingValueBoolean, RadioSettingValueString, \
RadioSettingValueFloat, RadioSettings
from chirp.drivers.iradio_common import enter_programming_mode, \
exit_programming_mode
LOG = logging.getLogger(__name__)
MEM_FORMAT = """
// #seekto 0x0000;
struct {
char startuplabel[32]; // Startup Label 0000-001f
char personalid[16]; // Personal ID 0020-002f
u8 displaylogo:1, // Display Startup Logo 0030
displayvoltage:1, // Display Voltage
displaylabel:1, // Display Startup Label
tailtone:1, // Tail Tone
startupringtone:1, // Startup Ringtone
voiceprompt:1, // Voice Prompt
keybeep:1, // Key Beep
unknown_0:1;
u8 txpriority:1, // TX Priority 0031
rogerbeep:2, // Roger Beep
savemode:1, // Save Mode
frequencystep:4; // Frequency Step
u8 squelch:4, // Squelch 0032
talkaround:2, // Talkaround
noaaalarm:1, // NOAA Alarm
dualdisplay:1; // Dual Display
u8 displaytimer; // Display Timer 0033
u8 locktimer; // Lock Timer 0034
u8 timeouttimer; // Timeout Timer 0035
u8 voxlevel:4, // VOX Level 0036
voxdelay:4; // Delay
ul16 tonefrequency; // Tone Frequency 0037-0038
ul16 fmfrequency; // FM Frequency 0039-003a
u8 fmstandby:1, // FM Standby 003b
dualstandby:1, // Dual Standby
standbyarea:1, // Standby Area
scandirection:1, // Scan Direction
unknown_2:2,
workmode:1, // Work Mode
unknown_3:1;
ul16 areaach; // Area A CH 003c-003d
ul16 areabch; // Area B CH 003e-003f
u8 unused_0:4, // 0040
key1long:4; // Key 1 Long
u8 unused_1:4, // 0041
key1short:4; // Key 1 Short
u8 unused_2:4, // 0042
key2long:4; // Key 2 Long
u8 unused_3:4, // 0043
key2short:4; // Key 2 Short
u8 unknown_4:4, // 0044
vox:1, // VOX
unknown_5:3;
u8 xposition; // X position (0-159) 0045
u8 yposition; // Y position (0-110) 0046
ul16 bordercolor; // Border Color 0047-0048
u8 unknown_6[9]; // 0x00 0049-0051
u8 unknown_7[2]; // 0xFF 0052-0053
u8 range174_240; // 174-240 MHz 0054
u8 range240_320; // 240-320 MHz 0055
u8 range320_400; // 320-400 MHz 0056
u8 range480_560; // 480-560 MHz 0057
u8 unused_4[7]; // 0xFF 0058-005e
u8 unknown_8; // 0x00 005f
u8 unused_5[12]; // 0xFF 0060-006b
u8 unknown_9[4]; // 0x00 006c-006f
ul16 quickch2; // Quick CH 2 0070-0071
ul16 quickch1; // Quick CH 1 0072-0073
ul16 quickch4; // Quick CH 4 0074-0075
ul16 quickch3; // Quick CH 3 0076-0077
} settings;
struct memory {
ul32 rxfreq; // RX Frequency 00-03
ul16 rx_tone; // PL/DPL Decode 04-05
ul32 txfreq; // TX Frequency 06-09
ul16 tx_tone; // PL/DPL Encode 0a-0b
ul24 mutecode; // Mute Code 0c-0e
u8 unknown_0:2, // 0f
mutetype:2, // Mute Type
unknown_1:4; //
u8 isnarrow:1, // Bandwidth 00
lowpower:1, // Power
scan:1, // Scan Add
bcl:2, // Busy Lock
is_airband:1, // Air Band (AM)
unknown_3:1, //
unknown_4:1; //
u8 unknown_5; // 01
u8 unused_0:4, // 02
scno:4; // SC No.
u8 unknown_6[3]; // 03-05
char name[10]; // 06-0f
};
#seekto 0x1000;
struct memory channels[999];
#seekto 0x8D20;
struct {
u8 senddelay; // Send Delay 8d20
u8 sendinterval; // Send Interval 8d21
u8 unused_0:6, // 8d22
sendmode:2; // Send Mode
u8 unused_2:4, // 8d23
sendselect:4; // Send Select
u8 unused_3:7, // 8d24
recvdisplay:1; // Recv Display
u8 encodegain; // Encode Gain 8d25
u8 decodeth; // Decode TH 8d26
} dtmf;
#seekto 0x8D30;
struct {
char code[14]; // DTMF code
u8 unused_ff;
u8 code_len; // DTMF code length
} dtmfcode[16];
// #seekto 0x8E30;
struct {
char kill[14]; // Remotely Kill 8e30-8e3d
u8 unknown_0; // 8e3e
u8 kill_len; // Remotely Kill Length 83ef
char stun[14]; // Remotely Stun 8e40-834d
u8 unknown_1; // 8e4e
u8 stun_len; // Remotely Stun Length 8e4f
char wakeup[14]; // Wake Up 8e50-8e5d
u8 unknown_2; // 8e5e
u8 wakeup_len; // Wake Up Length 8e5f
} dtmf2;
"""
CMD_ACK = b"\x06"
DTCS_CODES = tuple(sorted(chirp_common.DTCS_CODES + (645,)))
_STEP_LIST = [0.25, 1.25, 2.5, 5., 6.25, 10., 12.5, 25., 50., 100., 500.,
1000., 5000.]
LIST_AB = ["A", "B"]
LIST_BCL = ["Off", "Carrier", "CTC/DCS"]
LIST_DELAY = ["%s ms" % x for x in range(0, 2100, 100)]
LIST_DIRECTION = ["Up", "Down"]
LIST_FREQSTEP = ["0.25K", "1.25K", "2.5K", "5K", "6.25K", "10K", "12.5K",
"20K", "25K", "50K", "100K", "500K", "1M", "5M"]
LIST_INTERVAL = ["%s ms" % x for x in range(30, 210, 10)]
LIST_MUTETYPE = ["Off", "-", "23b", "24b"]
LIST_ROGER = ["Off", "Roger 1", "Roger 2", "Send ID"]
LIST_SENDM = ["Off", "TX Start", "TX End", "Start and End"]
LIST_SENDS = ["DTMF %s" % x for x in range(1, 17)]
LIST_SKEY = ["None", "Monitor", "Frequency Detect", "Talkaround",
"Quick CH", "Local Alarm", "Remote Alarm", "Weather CH",
"Send Tone", "Roger Beep"]
LIST_REPEATER = ["Off", "Talkaround", "Frequency Reversal"]
LIST_TIMER = ["Off", "5 seconds", "10 seconds"] + [
"%s seconds" % x for x in range(15, 615, 15)]
LIST_TXPRI = ["Edit", "Busy"]
LIST_WORKMODE = ["Frequency", "Channel"]
TXALLOW_CHOICES = ["RX Only", "TX/RX"]
TXALLOW_VALUES = [0xFF, 0x00]
VALID_CHARS = chirp_common.CHARSET_ALPHANUMERIC + \
"`{|}!\"#$%&'()*+,-./:;<=>?@[]^_"
DTMF_CHARS = list("0123456789ABCD*#")
def _read_block(radio, block_addr, block_size):
serial = radio.pipe
cmd = struct.pack(">BH", ord(b'R'), block_addr + radio.READ_OFFSET)
ccs = bytes([checksum.checksum_8bit(cmd)])
expectedresponse = b"R" + cmd[1:]
cmd = cmd + ccs
LOG.debug("Reading block %04x..." % block_addr)
try:
serial.write(cmd)
response = serial.read(3 + block_size + 1)
cs = checksum.checksum_8bit(response[:-1])
if response[:3] != expectedresponse:
raise Exception("Error reading block %04x." % block_addr)
chunk = response[3:]
if chunk[-1] != cs:
raise Exception("Block failed checksum!")
block_data = chunk[:-1]
except Exception:
raise errors.RadioError("Failed to read block at %04x" % block_addr)
return block_data
def _write_block(radio, block_addr, block_size):
serial = radio.pipe
# map the upload address to the mmap start and end addresses
start_addr = block_addr * block_size
end_addr = start_addr + block_size
data = radio.get_mmap()[start_addr:end_addr]
cmd = struct.pack(">BH", ord(b'I'), block_addr)
cs = bytes([checksum.checksum_8bit(cmd + data)])
data += cs
LOG.debug("Writing Data:")
LOG.debug(util.hexprint(cmd + data))
try:
serial.write(cmd + data)
if serial.read(1) != CMD_ACK:
raise Exception("No ACK")
except Exception:
raise errors.RadioError("Failed to send block "
"to radio at %04x" % block_addr)
def do_download(radio):
LOG.debug("download")
enter_programming_mode(radio.pipe, radio.magic_enter)
data = b""
status = chirp_common.Status()
status.msg = "Cloning from radio"
status.cur = 0
status.max = radio.END_ADDR
for addr in range(radio.START_ADDR, radio.END_ADDR, 1):
status.cur = addr
radio.status_fn(status)
block = _read_block(radio, addr, radio.BLOCK_SIZE)
data += block
LOG.debug("Address: %04x" % addr)
LOG.debug(util.hexprint(block))
exit_programming_mode(radio.pipe, radio.magic_exit)
return memmap.MemoryMapBytes(data)
def do_upload(radio):
status = chirp_common.Status()
status.msg = "Uploading to radio"
enter_programming_mode(radio.pipe, radio.magic_enter)
status.cur = 0
status.max = radio._memsize
# The OEM software reads the 1st block from the radio before commencing
# with the upload. That behavior will be mirrored here.
_read_block(radio, radio.START_ADDR, radio.BLOCK_SIZE)
for start_addr, end_addr in radio._ranges:
for addr in range(start_addr, end_addr, 1):
status.cur = addr * radio.BLOCK_SIZE
radio.status_fn(status)
_write_block(radio, addr, radio.BLOCK_SIZE)
exit_programming_mode(radio.pipe, radio.magic_exit)
class IradioUV5118plus(chirp_common.CloneModeRadio):
"""IRADIO UV5118plus"""
VENDOR = "Iradio"
MODEL = "UV-5118plus"
NAME_LENGTH = 10
BAUD_RATE = 115200
BLOCK_SIZE = 0x80
magic_enter = b"58" + b"\x05\x10\x82"
magic_exit = b"58" + b"\x05\xEE\x60"
VALID_BANDS = [(108000000, 136000000), # RX only (Air Band)
(136000000, 174000000), # TX/RX (VHF)
(174000000, 240000000), # TX/RX
(240000000, 320000000), # TX/RX
(320000000, 400000000), # TX/RX
(400000000, 480000000), # TX/RX (UHF)
(480000000, 560000000)] # TX/RX
POWER_LEVELS = [chirp_common.PowerLevel("High", watts=2.00),
chirp_common.PowerLevel("Low", watts=0.50)]
# Radio's write address starts at 0x0000
# Radio's write address ends at 0x0140
START_ADDR = 0
END_ADDR = 0x0140
# Radio's read address starts at 0x7820
# Radio's read address ends at 0x795F
READ_OFFSET = 0x7820
_ranges = [
(0x0000, 0x0140),
]
_memsize = 0xA000 # 0x0140 * 0x80
_upper = 999
def get_features(self):
rf = chirp_common.RadioFeatures()
rf.has_settings = True
rf.has_bank = False
rf.has_ctone = True
rf.has_cross = True
rf.has_rx_dtcs = True
rf.has_tuning_step = False
rf.can_odd_split = True
rf.has_name = True
rf.valid_name_length = self.NAME_LENGTH
rf.valid_characters = chirp_common.CHARSET_ASCII
rf.valid_skips = ["", "S"]
rf.valid_tmodes = ["", "Tone", "TSQL", "DTCS", "Cross"]
rf.valid_cross_modes = ["Tone->Tone", "Tone->DTCS", "DTCS->Tone",
"->Tone", "->DTCS", "DTCS->", "DTCS->DTCS"]
rf.valid_power_levels = self.POWER_LEVELS
rf.valid_duplexes = ["", "-", "+", "split"]
rf.valid_modes = ["FM", "NFM"] # 25 kHz, 12.5 kHz.
rf.valid_dtcs_codes = DTCS_CODES
rf.memory_bounds = (1, self._upper)
rf.valid_tuning_steps = _STEP_LIST
rf.valid_bands = self.VALID_BANDS
return rf
def process_mmap(self):
self._memobj = bitwise.parse(MEM_FORMAT, self._mmap)
def sync_in(self):
"""Download from radio"""
try:
data = do_download(self)
except errors.RadioError:
# Pass through any real errors we raise
raise
except Exception:
# If anything unexpected happens, make sure we raise
# a RadioError and log the problem
LOG.exception('Unexpected error during download')
raise errors.RadioError('Unexpected error communicating '
'with the radio')
self._mmap = data
self.process_mmap()
def sync_out(self):
"""Upload to radio"""
try:
do_upload(self)
except errors.RadioError:
raise
except Exception:
# If anything unexpected happens, make sure we raise
# a RadioError and log the problem
LOG.exception('Unexpected error during upload')
raise errors.RadioError('Unexpected error communicating '
'with the radio')
def get_raw_memory(self, number):
return repr(self._memobj.memory[number - 1])
@staticmethod
def _decode_tone(toneval):
# DCS examples:
# D023N - 1013 - 0001 0000 0001 0011
# ^-DCS
# D023I - 2013 - 0010 0000 0001 0100
# ^--DCS inverted
# D754I - 21EC - 0010 0001 1110 1100
# code in octal-------^^^^^^^^^^^
if toneval == 0x3000:
return '', None, None
elif toneval & 0x1000:
# DTCS N
code = int('%o' % (toneval & 0x1FF))
return 'DTCS', code, 'N'
elif toneval & 0x2000:
# DTCS R
code = int('%o' % (toneval & 0x1FF))
return 'DTCS', code, 'R'
else:
return 'Tone', toneval / 10.0, None
@staticmethod
def _encode_tone(mode, val, pol):
if not mode:
return 0x3000
elif mode == 'Tone':
return int(val * 10)
elif mode == 'DTCS':
code = int('%i' % val, 8)
if pol == 'N':
code |= 0x1800
if pol == 'R':
code |= 0x2800
return code
else:
raise errors.RadioError('Unsupported tone mode %r' % mode)
def get_memory(self, number):
mem = chirp_common.Memory()
_mem = self._memobj.channels[number - 1]
mem.number = number
mem.freq = int(_mem.rxfreq) * 10
# We'll consider any blank (i.e. 0 MHz frequency) to be empty
if mem.freq == 0:
mem.empty = True
return mem
if _mem.rxfreq.get_raw() == b"\xFF\xFF\xFF\xFF":
mem.freq = 0
mem.empty = True
return mem
# Freq and offset
mem.freq = int(_mem.rxfreq) * 10
# TX freq set
offset = (int(_mem.txfreq) * 10) - mem.freq
if offset != 0:
if chirp_common.is_split(self.get_features().valid_bands,
mem.freq, int(_mem.txfreq) * 10):
mem.duplex = "split"
mem.offset = int(_mem.txfreq) * 10
elif offset < 0:
mem.offset = abs(offset)
mem.duplex = "-"
elif offset > 0:
mem.offset = offset
mem.duplex = "+"
else:
mem.offset = 0
mem.name = str(_mem.name).rstrip(" ").replace("\xFF", " ")
mem.mode = _mem.isnarrow and "NFM" or "FM"
if mem.freq < 136000000:
_mem.is_airband = True
else:
_mem.is_airband = False
chirp_common.split_tone_decode(mem,
self._decode_tone(_mem.tx_tone),
self._decode_tone(_mem.rx_tone))
mem.power = self.POWER_LEVELS[_mem.lowpower]
if not _mem.scan:
mem.skip = "S"
mem.extra = RadioSettingGroup("Extra", "extra")
rs = RadioSettingValueList(LIST_BCL, current_index=_mem.bcl)
rset = RadioSetting("bcl", "Busy Channel Lockout", rs)
mem.extra.append(rset)
rs = RadioSettingValueList(LIST_MUTETYPE, current_index=_mem.mutetype)
rset = RadioSetting("mutetype", "Mute Type", rs)
mem.extra.append(rset)
rs = RadioSettingValueInteger(0, 16777215, _mem.mutecode)
rset = RadioSetting("mutecode", "Mute Code", rs)
rset.set_doc('Value between 0-16777215')
mem.extra.append(rset)
rs = RadioSettingValueInteger(0, 8, _mem.scno)
rset = RadioSetting("scno", "SC No.", rs)
rset.set_doc('Value between 0-8')
mem.extra.append(rset)
return mem
def set_memory(self, mem):
LOG.debug("Setting %i(%s)" % (mem.number, mem.extd_number))
_mem = self._memobj.channels[mem.number - 1]
# if empty memory
if mem.empty:
_mem.set_raw("\xFF" * 22 + "\x20" * 10)
return
_mem.set_raw("\xFF" * 4 + "\x00\x30" + "\xFF" * 4 + "\x00\x30" +
"\x00" * 10 + "\x20" * 10)
_mem.rxfreq = mem.freq / 10
if mem.duplex == "split":
_mem.txfreq = mem.offset / 10
elif mem.duplex == "+":
_mem.txfreq = (mem.freq + mem.offset) / 10
elif mem.duplex == "-":
_mem.txfreq = (mem.freq - mem.offset) / 10
else:
_mem.txfreq = mem.freq / 10
_mem.name = mem.name.rstrip('\xFF').ljust(10, '\x20')
_mem.scan = mem.skip != "S"
_mem.isnarrow = mem.mode == "NFM"
# dtcs_pol = ["N", "N"]
txtone, rxtone = chirp_common.split_tone_encode(mem)
_mem.tx_tone = self._encode_tone(*txtone)
_mem.rx_tone = self._encode_tone(*rxtone)
_mem.lowpower = mem.power == self.POWER_LEVELS[1]
for setting in mem.extra:
setattr(_mem, setting.get_name(), setting.value)
def get_settings(self):
_dtmf = self._memobj.dtmf
_dtmf2 = self._memobj.dtmf2
_settings = self._memobj.settings
basic = RadioSettingGroup("basic", "Basic Settings")
dtmf = RadioSettingGroup("dtmf", "DTMF Settings")
startup = RadioSettingGroup("startup", "Startup Settings")
txallow = RadioSettingGroup("txallow", "TX Allow Settings")
top = RadioSettings(basic, dtmf, startup, txallow)
# Basic Settings
# Menu 21 - Personal ID
_codeobj = _settings.personalid
_code = str(_codeobj).rstrip('\x20')
rs = RadioSettingValueString(0, 16, _code, True)
rset = RadioSetting("personalid", "Personal ID", rs)
basic.append(rset)
rs = RadioSettingValueList(LIST_WORKMODE,
current_index=_settings.workmode)
rset = RadioSetting("workmode", "Work Mode", rs)
basic.append(rset)
# Menu 05 - Voice Prompt
rs = RadioSettingValueBoolean(_settings.voiceprompt)
rset = RadioSetting("voiceprompt", "Voice Prompt", rs)
basic.append(rset)
# Menu 06 - Key Beep
rs = RadioSettingValueBoolean(_settings.keybeep)
rset = RadioSetting("keybeep", "Key Beep", rs)
basic.append(rset)
# Menu 07 - Roger Beep
rs = RadioSettingValueList(LIST_ROGER,
current_index=_settings.rogerbeep)
rset = RadioSetting("rogerbeep", "Roger Beep", rs)
basic.append(rset)
# Menu 09 - TX Priority
rs = RadioSettingValueList(LIST_TXPRI,
current_index=_settings.txpriority)
rset = RadioSetting("txpriority", "TX Priority", rs)
basic.append(rset)
# Menu 10 - Save Mode
rs = RadioSettingValueBoolean(_settings.savemode)
rset = RadioSetting("savemode", "Save Mode", rs)
basic.append(rset)
# Menu 11 - Freq Step
val = min(_settings.frequencystep, 0x0D)
rs = RadioSettingValueList(LIST_FREQSTEP, current_index=val)
rset = RadioSetting("frequencystep", "Frequency Step", rs)
basic.append(rset)
# Menu 12 - SQ Level
val = min(_settings.squelch, 0x09)
rs = RadioSettingValueInteger(0, 9, val)
rset = RadioSetting("squelch", "Squelch Level (0-9)", rs)
basic.append(rset)
# Menu 13 - LED Timer
val = min(_settings.displaytimer, 0x2A)
rs = RadioSettingValueList(LIST_TIMER, current_index=val)
rset = RadioSetting("displaytimer", "Display Timer", rs)
basic.append(rset)
# Menu 14 - Lcok Timer
val = min(_settings.locktimer, 0x2A)
rs = RadioSettingValueList(LIST_TIMER, current_index=val)
rset = RadioSetting("locktimer", "Lock Timer", rs)
basic.append(rset)
# Menu 15 - TOT
val = min(_settings.timeouttimer, 0x2A)
rs = RadioSettingValueList(LIST_TIMER, current_index=val)
rset = RadioSetting("timeouttimer", "Timeout Timer", rs)
basic.append(rset)
rs = RadioSettingValueBoolean(_settings.vox)
rset = RadioSetting("vox", "VOX", rs)
basic.append(rset)
# Menu 16 - VOX Level
val = min(_settings.voxlevel, 0x09)
rs = RadioSettingValueInteger(0, 9, val)
rset = RadioSetting("voxlevel", "VOX Level (0-9)", rs)
basic.append(rset)
# Menu 17 - VOX Delay
val = min(_settings.voxdelay, 0x09)
rs = RadioSettingValueInteger(0, 9, val)
rset = RadioSetting("voxdelay", "VOX Delay (0-9)", rs)
basic.append(rset)
# Menu 18 - NOAA Monitor
rs = RadioSettingValueBoolean(_settings.noaaalarm)
rset = RadioSetting("noaaalarm", "NOAA Alarm", rs)
basic.append(rset)
# Menu 19 - FM Standby
rs = RadioSettingValueBoolean(_settings.fmstandby)
rset = RadioSetting("fmstandby", "FM Standby", rs)
basic.append(rset)
def myset_freq(setting, obj, atrb, mult):
""" Callback to set frequency by applying multiplier"""
value = int(float(str(setting.value)) * mult)
setattr(obj, atrb, value)
return
# FM Broadcast Settings
val = _settings.fmfrequency
val = val / 10.0
if val < 64.0 or val > 108.0:
val = 90.4
rx = RadioSettingValueFloat(64.0, 108.0, val, 0.1, 1)
rset = RadioSetting("fmfrequency", "Broadcast FM Freq (MHz)", rx)
rset.set_apply_callback(myset_freq, _settings, "fmfrequency", 10)
basic.append(rset)
# Menu 20 - Tail Tone
rs = RadioSettingValueBoolean(_settings.tailtone)
rset = RadioSetting("tailtone", "Tail Tone", rs)
basic.append(rset)
# Menu 21 - Scan DIR
rs = RadioSettingValueList(LIST_DIRECTION,
current_index=_settings.scandirection)
rset = RadioSetting("scandirection", "Scan Direction", rs)
basic.append(rset)
# Menu 08 - Dual Display
rs = RadioSettingValueBoolean(_settings.dualdisplay)
rset = RadioSetting("dualdisplay", "Dual Display", rs)
basic.append(rset)
# Menu 23 - Repeater Mode
val = min(_settings.talkaround, 0x02)
rs = RadioSettingValueList(LIST_REPEATER, current_index=val)
rset = RadioSetting("talkaround", "Talkaround", rs)
basic.append(rset)
# Menu 37 - K1 Short
val = min(_settings.key1short, 0x09)
rs = RadioSettingValueList(LIST_SKEY, current_index=val)
rset = RadioSetting("key1short", "Key 1 Short", rs)
basic.append(rset)
# Menu 36 - K1 Long
val = min(_settings.key1long, 0x09)
rs = RadioSettingValueList(LIST_SKEY, current_index=val)
rset = RadioSetting("key1long", "Key 1 Long", rs)
basic.append(rset)
# Menu 39 - K2 Short
val = min(_settings.key2short, 0x09)
rs = RadioSettingValueList(LIST_SKEY, current_index=val)
rset = RadioSetting("key2short", "Key 2 Short", rs)
basic.append(rset)
# Menu 38 - K2 Long
val = min(_settings.key2long, 0x09)
rs = RadioSettingValueList(LIST_SKEY, current_index=val)
rset = RadioSetting("key2long", "Key 2 Long", rs)
basic.append(rset)
rs = RadioSettingValueInteger(0, 20000, _settings.tonefrequency)
rset = RadioSetting("tonefrequency", "Tone Frequency (0-2000)", rs)
basic.append(rset)
rs = RadioSettingValueBoolean(_settings.dualstandby)
rset = RadioSetting("dualstandby", "Dual Standby", rs)
basic.append(rset)
rs = RadioSettingValueList(LIST_AB,
current_index=_settings.standbyarea)
rset = RadioSetting("standbyarea", "Standby Area", rs)
basic.append(rset)
rs = RadioSettingValueInteger(1, 999, _settings.areaach + 1)
rset = RadioSetting("areaach", "Area A CH (1-999)", rs)
basic.append(rset)
rs = RadioSettingValueInteger(1, 999, _settings.areabch + 1)
rset = RadioSetting("areabch", "Area B CH (1-999)", rs)
basic.append(rset)
rs = RadioSettingValueInteger(1, 999, _settings.quickch1 + 1)
rset = RadioSetting("quickch1", "Quick CH 1 (1-999)", rs)
basic.append(rset)
rs = RadioSettingValueInteger(1, 999, _settings.quickch2 + 1)
rset = RadioSetting("quickch2", "Quick CH 2 (1-999)", rs)
basic.append(rset)
rs = RadioSettingValueInteger(1, 999, _settings.quickch3 + 1)
rset = RadioSetting("quickch3", "Quick CH 3 (1-999)", rs)
basic.append(rset)
rs = RadioSettingValueInteger(1, 999, _settings.quickch4 + 1)
rset = RadioSetting("quickch4", "Quick CH 4 (1-999)", rs)
basic.append(rset)
rs = RadioSettingValueInteger(0, 65535, _settings.bordercolor)
rset = RadioSetting("bordercolor", "Border Color (0-65535)", rs)
basic.append(rset)
# DTMF Settings
# Menu 40 - DTMF Delay
val = min(_dtmf.senddelay, 0x14)
rs = RadioSettingValueList(LIST_DELAY, current_index=val)
rset = RadioSetting("dtmf.senddelay", "Send Delay", rs)
dtmf.append(rset)
# Menu 41 - DTMF Interval
val = min(_dtmf.sendinterval, 0x11)
rs = RadioSettingValueList(LIST_INTERVAL, current_index=val)
rset = RadioSetting("dtmf.sendinterval", "Send Interval", rs)
dtmf.append(rset)
# Menu 42 - DTMF Mode
rs = RadioSettingValueList(LIST_SENDM,
current_index=_dtmf.sendmode)
rset = RadioSetting("dtmf.sendmode", "Send Mode", rs)
dtmf.append(rset)
# Menu 43 - DTMF Select
rs = RadioSettingValueList(LIST_SENDS,
current_index=_dtmf.sendselect)
rset = RadioSetting("dtmf.sendselect", "Send Select", rs)
dtmf.append(rset)
# Menu 44 - DTMF Display
rs = RadioSettingValueBoolean(_dtmf.recvdisplay)
rset = RadioSetting("dtmf.recvdisplay", "Receive Display", rs)
dtmf.append(rset)
val = min(_dtmf.encodegain, 0x7F)
rs = RadioSettingValueInteger(0, 127, val)
rset = RadioSetting("dtmf.encodegain", "Encode Gain (0-127)", rs)
dtmf.append(rset)
val = min(_dtmf.decodeth, 0x3F)
rs = RadioSettingValueInteger(0, 63, val)
rset = RadioSetting("dtmf.decodeth", "Decode TH (0-63)", rs)
dtmf.append(rset)
for i in range(0, 16):
_codeobj = self._memobj.dtmfcode[i].code
_code = str(_codeobj).rstrip('\xFF')
rs = RadioSettingValueString(0, 14, _code, False)
rs.set_charset(DTMF_CHARS)
rset = RadioSetting("dtmfcode/%i.code" % i,
"Code %i" % (i + 1), rs)
def apply_code(setting, obj, length):
code = ""
for char in str(setting.value):
if char in DTMF_CHARS:
code += char
else:
code += ""
obj.code_len = len(str(code))
obj.code = code.ljust(length, chr(255))
rset.set_apply_callback(apply_code, self._memobj.dtmfcode[i], 14)
dtmf.append(rset)
_codeobj = _dtmf2.kill
_code = str(_codeobj).rstrip('\xFF')
rs = RadioSettingValueString(0, 14, _code, False)
rs.set_charset(DTMF_CHARS)
rset = RadioSetting("dtmf2.kill",
"Remotely Kill", rs)
def apply_code(setting, obj, length):
code = ""
for char in str(setting.value):
if char in DTMF_CHARS:
code += char
else:
code += ""
obj.kill_len = len(str(code))
obj.kill = code.ljust(length, chr(255))
rset.set_apply_callback(apply_code, _dtmf2, 14)
dtmf.append(rset)
_codeobj = _dtmf2.stun
_code = str(_codeobj).rstrip('\xFF')
rs = RadioSettingValueString(0, 14, _code, False)
rs.set_charset(DTMF_CHARS)
rset = RadioSetting("dtmf2.stun",
"Remotely Stun", rs)
def apply_code(setting, obj, length):
code = ""
for char in str(setting.value):
if char in DTMF_CHARS:
code += char
else:
code += ""
obj.stun_len = len(str(code))
obj.stun = code.ljust(length, chr(255))
rset.set_apply_callback(apply_code, _dtmf2, 14)
dtmf.append(rset)
_codeobj = _dtmf2.wakeup
_code = str(_codeobj).rstrip('\xFF')
rs = RadioSettingValueString(0, 14, _code, False)
rs.set_charset(DTMF_CHARS)
rset = RadioSetting("dtmf2.wakeup",
"Wake Up", rs)
def apply_code(setting, obj, length):
code = ""
for char in str(setting.value):
if char in DTMF_CHARS:
code += char
else:
code += ""
obj.wakeup_len = len(str(code))
obj.wakeup = code.ljust(length, chr(255))
rset.set_apply_callback(apply_code, _dtmf2, 14)
dtmf.append(rset)
# Startup Settings
_codeobj = _settings.startuplabel
_code = str(_codeobj).rstrip('\x20')
rs = RadioSettingValueString(0, 32, _code, True)
rset = RadioSetting("startuplabel", "Startup Label", rs)
startup.append(rset)
# Menu 04 - Prompt Text
rs = RadioSettingValueBoolean(_settings.displaylabel)
rset = RadioSetting("displaylabel", "Display Startup Label", rs)
startup.append(rset)
# Menu 02 - Voltage
rs = RadioSettingValueBoolean(_settings.displayvoltage)
rset = RadioSetting("displayvoltage", "Display Voltage", rs)
startup.append(rset)
# Menu 01 - Startup Logo
rs = RadioSettingValueBoolean(_settings.displaylogo)
rset = RadioSetting("displaylogo", "Display Startup Logo", rs)
startup.append(rset)
# Menu 03 - Ringtone
rs = RadioSettingValueBoolean(_settings.startupringtone)
rset = RadioSetting("startupringtone", "Startup Ringtone", rs)
startup.append(rset)
val = min(_settings.xposition, 0x9F)
rs = RadioSettingValueInteger(0, 159, val)
rset = RadioSetting("xposition", "X Position (0-159)", rs)
startup.append(rset)
val = min(_settings.yposition, 0x6E)
rs = RadioSettingValueInteger(16, 110, val)
rset = RadioSetting("yposition", "Y Position (16-110)", rs)
startup.append(rset)
# TX Allow Settings
def apply_txallow_listvalue(setting, obj):
LOG.debug("Setting value: " + str(
setting.value) + " from list")
val = str(setting.value)
index = TXALLOW_CHOICES.index(val)
val = TXALLOW_VALUES[index]
obj.set_value(val)
if _settings.range174_240 in TXALLOW_VALUES:
idx = TXALLOW_VALUES.index(_settings.range174_240)
else:
idx = TXALLOW_VALUES.index(0xFF)
rs = RadioSettingValueList(TXALLOW_CHOICES, current_index=idx)
rset = RadioSetting("range174_240", "174-240 MHz", rs)
rset.set_apply_callback(apply_txallow_listvalue,
_settings.range174_240)
txallow.append(rset)
if _settings.range240_320 in TXALLOW_VALUES:
idx = TXALLOW_VALUES.index(_settings.range240_320)
else:
idx = TXALLOW_VALUES.index(0xFF)
rs = RadioSettingValueList(TXALLOW_CHOICES, current_index=idx)
rset = RadioSetting("range240_320", "240-320 MHz", rs)
rset.set_apply_callback(apply_txallow_listvalue,
_settings.range240_320)
txallow.append(rset)
if _settings.range320_400 in TXALLOW_VALUES:
idx = TXALLOW_VALUES.index(_settings.range320_400)
else:
idx = TXALLOW_VALUES.index(0xFF)
rs = RadioSettingValueList(TXALLOW_CHOICES, current_index=idx)
rset = RadioSetting("range320_400", "320-400 MHz", rs)
rset.set_apply_callback(apply_txallow_listvalue,
_settings.range320_400)
txallow.append(rset)
if _settings.range480_560 in TXALLOW_VALUES:
idx = TXALLOW_VALUES.index(_settings.range480_560)
else:
idx = TXALLOW_VALUES.index(0xFF)
rs = RadioSettingValueList(TXALLOW_CHOICES, current_index=idx)
rset = RadioSetting("range480_560", "480-560 MHz", rs)
rset.set_apply_callback(apply_txallow_listvalue,
_settings.range480_560)
txallow.append(rset)
return top
def set_settings(self, 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 = self._memobj.settings
setting = element.get_name()
if element.has_apply_callback():
LOG.debug("Using apply callback")
element.run_apply_callback()
elif setting in ["areaach",
"areabch",
"quickch1",
"quickch2",
"quickch3",
"quickch4"
]:
setattr(obj, setting, int(element.value) - 1)
elif element.value.get_mutable():
LOG.debug("Setting %s = %s" % (setting, element.value))
setattr(obj, setting, element.value)
except Exception as e:
LOG.debug(element.get_name(), e)
raise
@classmethod
def match_model(cls, filedata, filename):
# This radio has always been post-metadata, so never do
# old-school detection
return False
@directory.register
class RuyageUV58PlusRadio(IradioUV5118plus):
"""Ruyage UV58Plus"""
VENDOR = "Ruyage"
MODEL = "UV58Plus"
chirp-20261030/chirp/drivers/kenwood_d7.py 0000644 0001750 0001750 00000323236 15172631605 017246 0 ustar hibby hibby # 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 logging
import math
import os
import threading
from chirp import chirp_common, directory, errors, util
from chirp.settings import RadioSetting, RadioSettingGroup, \
RadioSettingValueInteger, RadioSettingValueBoolean, \
RadioSettingValueString, RadioSettingValueList, RadioSettingValueMap, \
RadioSettings, RadioSettingValueFloat
LOG = logging.getLogger(__name__)
class KenwoodD7Position(RadioSettingGroup):
def __init__(self, name, shortname, radio, *args, **kwargs):
super().__init__(name, shortname, *args, **kwargs)
self._radio = radio
if ' ' in name:
index = name.split(" ", 1)[1]
else:
index = ''
rs = RadioSetting("latd%s" % index, "Latitude degrees",
RadioSettingValueInteger(0, 90, 0))
self.append(rs)
rs = RadioSetting("latm%s" % index, "Latitude minutes",
RadioSettingValueFloat(0, 59.99, 0,
resolution=0.01,
precision=2))
self.append(rs)
rs = RadioSetting("south%s" % index, "South",
RadioSettingValueBoolean(False))
self.append(rs)
rs = RadioSetting("longd%s" % index, "Longitude degrees",
RadioSettingValueInteger(0, 180, 0))
self.append(rs)
rs = RadioSetting("longm%s" % index, "Longitude minutes",
RadioSettingValueFloat(0, 59.99, 0,
resolution=0.01, precision=2))
self.append(rs)
rs = RadioSetting("west%s" % index, "West",
RadioSettingValueBoolean(False))
self.append(rs)
def kenwood_d7_read_value(self):
index = self._get_index()
name = self.get_name()
rawval = self._radio._kenwood_get(name)[1]
curr_value = int(rawval[0:2])
self["latd%s" % index].value.set_value(curr_value)
curr_value = int(rawval[2:6]) / 100
self["latm%s" % index].value.set_value(curr_value)
curr_value = int(rawval[6:8])
self["south%s" % index].value.set_value(curr_value)
curr_value = int(rawval[8:11])
self["longd%s" % index].value.set_value(curr_value)
curr_value = int(rawval[11:15]) / 100
self["longm%s" % index].value.set_value(curr_value)
curr_value = int(rawval[15:17])
self["west%s" % index].value.set_value(curr_value)
self['latd%s' % index].value._has_changed = False
self['latm%s' % index].value._has_changed = False
self['south%s' % index].value._has_changed = False
self['longd%s' % index].value._has_changed = False
self['longm%s' % index].value._has_changed = False
self['west%s' % index].value._has_changed = False
def _get_index(self):
name = self.get_name()
if ' ' in name:
return name.split(" ", 1)[1]
return ''
def changed(self):
index = self._get_index()
if self['latd%s' % index].changed():
return True
if self['latm%s' % index].changed():
return True
if self['south%s' % index].changed():
return True
if self['longd%s' % index].changed():
return True
if self['longm%s' % index].changed():
return True
if self['west%s' % index].changed():
return True
return False
def kenwood_d7_set_value(self):
index = self._get_index()
latd = self['latd%s' % index].value.get_value()
latm = self['latm%s' % index].value.get_value()
latmd = int(round(math.modf(latm)[0] * 100))
latm = int(round(latm))
south = int(self['south%s' % index].value.get_value())
longd = self['longd%s' % index].value.get_value()
longm = self['longm%s' % index].value.get_value()
longmd = int(round(math.modf(longm)[0] * 100))
longm = int(round(longm))
west = int(self['west%s' % index].value.get_value())
args = '%s%02d%02d%02d%02d%03d%02d%02d%02d' % (index,
latd, latm, latmd,
south,
longd, longm, longmd,
west)
if index != '':
index = ' ' + index
self._radio._kenwood_set('MP%s' % index, args)
# TODO: Hacking up _has_changed is bad... but we don't want
# to re-send all the set commands each time either.
self['latd%s' % index].value._has_changed = False
self['latm%s' % index].value._has_changed = False
self['south%s' % index].value._has_changed = False
self['longd%s' % index].value._has_changed = False
self['longm%s' % index].value._has_changed = False
self['west%s' % index].value._has_changed = False
class KenwoodD7DTMFMemory(RadioSettingGroup):
DTMF_CHARSET = "ABCD#*0123456789 "
def __init__(self, name, shortname, radio, *args, **kwargs):
super().__init__(name, shortname, *args, **kwargs)
self._radio = radio
self._name = name
index = self._get_index()
rs = RadioSetting("DMN %s" % index, "Memory Name",
RadioSettingValueString(0, 8, '', False))
self.append(rs)
rs = RadioSetting("DM %s" % index, "Memory Value",
RadioSettingValueString(0, 16, '', False,
self.DTMF_CHARSET))
self.append(rs)
def _get_index(self):
return "%02d" % int(self._name.split(" ", 1)[1])
def kenwood_d7_read_value(self):
index = self._get_index()
value = self._radio._kenwood_get("DM %s" % index)[1]
value = value.replace("E", "*").replace("F", "#")
vname = self._radio._kenwood_get("DMN %s" % index)[1]
self["DMN %s" % index].value.set_value(vname)
self["DMN %s" % index].value._has_changed = False
self["DM %s" % index].value.set_value(value)
self["DM %s" % index].value._has_changed = False
def changed(self):
for element in self:
if element.changed():
return True
return False
def kenwood_d7_set_value(self):
for element in self:
if not element.changed():
continue
newval = element.value.get_value()
newval = newval.replace("*", "E").replace("#", "F")
self._radio._kenwood_set(element.get_name(),
newval)
element.value._has_changed = False
class KenwoodD7ProgrammableVFOs(RadioSettingGroup):
def __init__(self, name, shortname, radio, *args, **kwargs):
self._radio = radio
super().__init__(name, shortname, *args, **kwargs)
for optname, index, minimum, maximum in self._radio._PROGRAMMABLE_VFOS:
group = RadioSettingGroup("PV %d" % index, optname)
self.append(group)
rs = RadioSetting("PV %dL" % index, "Lower Limit",
RadioSettingValueInteger(minimum, maximum,
minimum))
group.append(rs)
rs = RadioSetting("PV %dU" % index, "Upper Limit",
RadioSettingValueInteger(minimum, maximum,
maximum))
group.append(rs)
def _get_index(self, element):
name = element.get_name()
return int(name.split(" ", 1)[1])
def kenwood_d7_read_value(self):
for element in self:
index = self._get_index(element)
rv = self._radio._kenwood_get("PV %d" % index)[1]
lower, upper = rv.split(',', 1)
lower = int(lower)
upper = int(upper)
pvdl = "PV %dL" % index
pvdu = "PV %dU" % index
element[pvdl].value.set_value(lower)
element[pvdl].value._has_changed = False
element[pvdu].value.set_value(upper)
element[pvdu].value._has_changed = False
def changed(self):
for element in self:
for bound in element:
if bound.changed():
return True
return False
# TODO: Custom validator things...
def kenwood_d7_set_value(self):
for element in self:
index = self._get_index(element)
pvdl = "PV %dL" % index
pvdu = "PV %dU" % index
if element[pvdl].changed() or element[pvdu].changed():
lower = element[pvdl].value.get_value()
upper = element[pvdu].value.get_value()
args = "%05d,%05d" % (lower, upper)
self._radio._kenwood_set(element.get_name(), args)
element[pvdl].value._has_changed = False
element[pvdu].value._has_changed = False
class KenwoodD7Family(chirp_common.LiveRadio):
VENDOR = "Kenwood"
MODEL = ""
HARDWARE_FLOW = False
_ARG_DELIMITER = " "
_BAUDS = [9600]
_CMD_DELIMITER = "\r"
_DISABLE_AI_CMD = ('AI', '0')
_DUPLEX = {0: "", 1: "+", 2: "-"}
_HAS_NAME = True
_LOCK = threading.Lock()
_MODES = {0: "FM", 1: "AM"}
_NOCACHE = "CHIRP_NOCACHE" in os.environ
_SPLIT = True
_STEPS = (5.0, 6.25, 10.0, 12.5, 15.0, 20.0, 25.0, 30.0, 50.0, 100.0)
_TONES = chirp_common.OLD_TONES
_LOWER = 0
_UPPER = 199
_BANDS = []
_CALL_CHANS = ("VHF Call", "UHF Call")
_SPECIAL_CHANS = ("L0", "U0",
"L1", "U1",
"L2", "U2",
"L3", "U3",
"L4", "U4",
"L5", "U5",
"L6", "U6",
"L7", "U7",
"L8", "U8",
"L9", "U9",
*_CALL_CHANS)
# These are used more than once in _COMMON_SETTINGS
_BOOL = {'type': 'bool'}
_DATA_BAND = {'type': 'list',
'values': ("A", "B", "A:TX/B:RX", "A:RX/B:TX")}
_DTMF_MEMORY = {'type': KenwoodD7DTMFMemory}
_ON_OFF_DUAL_MAP = {'type': 'map',
'map': (("Off", "0"), ("On", "1,0")),
'set_requires': (('DL', True),),
'get_requires': (('DL', True),)}
_POSITION = {'type': KenwoodD7Position}
_SSTV_CHARSET = "ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789 !?-/"
_SSTV_COLOURS = {'type': 'list',
'values': ("Black", "Blue", "Red", "Magenta", "Green",
"Cyan", "Yellow", "White")}
_STATUS_TEXT = {'type': 'string', 'max_len': 32}
_SQUELCH = {'type': 'list', 'digits': 2,
'values': ("Open", "1", "2", "3", "4", "5")}
# Settings that are shared by at least two radios.
# If a third radio does not have the setting at all, the presence
# here won't matter. If it has it but with different parameters,
# it can be overridden in the subclass _SETTINGS dict.
_COMMON_SETTINGS = {
"AIP": _BOOL,
"ARL": {'type': 'map',
'map': (('Off', '0000'),) + tuple(
[('%i' % x, '%04i' % x)
for x in range(10, 2510, 10)])},
"ARLM": {'type': 'string', 'max_len': 45},
"AMGG": {'type': 'string', 'max_len': 45,
'charset': "ABCDEFGHIJKLMNOPQRSTUVWXYZ*,-0123456789"},
"AMR": _BOOL,
"APO": {'type': 'list',
'values': ("Off", "30min", "60min")},
"ARO": _BOOL,
# ASC x requires that the specified VFO is enabled on TH-D7G
# (ie: dual is on or VFO is current) for both fetch and set
# Easiest to just enable DL before set/read
"ASC 0": _ON_OFF_DUAL_MAP,
"ASC 1": _ON_OFF_DUAL_MAP,
"BAL": {'type': 'list',
'values': ("4:0", "3:1", "2:2", "1:3", "0:4")},
"BC": {'type': 'list',
'values': ("A", "B")},
"BCN": _BOOL,
# BEL x requires that the specified VFO is enabled on TH-D7G
# (ie: dual is on or VFO is current) for both fetch and set
# Easiest to just enable DL before set/read
"BEL 0": _ON_OFF_DUAL_MAP,
"BEL 1": _ON_OFF_DUAL_MAP,
"BEP": {'type': 'list',
'values': ("Off", "Key", "Key+Data", "All")},
"BEPT": {'type': 'list',
'values': ("Off", "Mine", "All New", "All")},
"CH": _BOOL,
"CKEY": {'type': 'list',
'values': ("Call", "1750 Hz")},
"CNT": {'type': 'integer', 'min': 1, 'max': 16},
"DL": _BOOL,
"DS": {'type': 'list',
'values': ("Data Band", "Both Bands")},
"DTB": _DATA_BAND,
"DTBA": _DATA_BAND,
"dtmfmem 00": _DTMF_MEMORY,
"dtmfmem 01": _DTMF_MEMORY,
"dtmfmem 02": _DTMF_MEMORY,
"dtmfmem 03": _DTMF_MEMORY,
"dtmfmem 04": _DTMF_MEMORY,
"dtmfmem 05": _DTMF_MEMORY,
"dtmfmem 06": _DTMF_MEMORY,
"dtmfmem 07": _DTMF_MEMORY,
"dtmfmem 08": _DTMF_MEMORY,
"dtmfmem 09": _DTMF_MEMORY,
"DTX": {'type': 'list',
'values': ("Manual", "PTT", "Auto")},
"ELK": {'type': 'bool'},
"GU": {'type': 'list',
'values': ("Not Used", "NMEA", "NMEA96")},
# This isn't exactly shared, but each radio adds to this list
"ICO": {'type': 'map',
'map': (('Kenwood', '0,0'),
('Runner', '0,1'),
('House ', '0,2'),
('Tent', '0,3'),
('Boat', '0,4'),
('SSTV', '0,5'),
('Plane', '0,6'),
('Speedboat', '0,7'),
('Car', '0,8'),
('Bicycle', '0,9'),
('TRIANGLE(DF station)', '0,A'),
('Jeep', '0,B'),
('Recreational Vehicle', '0,C'),
('Truck ', '0,D'),
('Van', '0,E'))},
"KILO": {'type': 'list',
'values': ("Miles", "Kilometers")},
"LK": _BOOL,
"LMP": _BOOL,
"MAC": _SSTV_COLOURS,
"MES": {'type': 'string', 'max_len': 8},
# NOTE: Requires memory mode to be active
"MNF": {'type': 'list',
'values': ("Name", "Frequency")},
"MP 1": _POSITION,
"MP 2": _POSITION,
"MP 3": _POSITION,
"MYC": {'type': 'string', 'max_len': 9},
"PAMB": {'type': 'list',
'values': ("Off", "1 Digit", "2 Digits", "3 Digits",
"4 Digits")},
"PKSA": {'type': 'list',
'values': ("1200", "9600")},
"POSC": {'type': 'list',
'values': ("Off Duty", "Enroute", "In Service", "Returning",
"Committed", "Special", "Priority", "CUSTOM 0",
"CUSTOM 1", "CUSTOM 2", "CUSTOM 4", "CUSTOM 5",
"CUSTOM 6", "Emergency")},
"PP": {'type': 'string', 'max_len': 32},
"PT": {'type': 'list',
'values': ("100ms", "200ms", "500ms", "750ms",
"1000ms", "1500ms", "2000ms")},
"PV": {'type': KenwoodD7ProgrammableVFOs},
"RSC": _SSTV_COLOURS,
"RSV": {'type': 'string', 'max_len': 10, 'charset': _SSTV_CHARSET},
"SCC": {'type': 'string', 'max_len': 8},
"SCR": {'type': 'list',
'values': ("Time", "Carrier", "Seek")},
"SCT": {'type': 'string', 'max_len': 8},
"SKTN": {'type': 'map',
'map': tuple([(str(x), "%02d" % (ind + 1))
for ind, x in enumerate(_TONES) if x != 69.3])},
"SMC": _SSTV_COLOURS,
"SMSG": {'type': 'string', 'max_len': 9, 'charset': _SSTV_CHARSET},
"SMY": {'type': 'string', 'max_len': 8, 'charset': _SSTV_CHARSET},
"SQ 0": _SQUELCH,
"SQ 1": _SQUELCH,
"STAT 1": _STATUS_TEXT,
"STAT 2": _STATUS_TEXT,
"STAT 3": _STATUS_TEXT,
"STXR": {'type': 'list',
'values': ("Off", "1/1", "1/2", "1/3", "1/4", "1/5", "1/6",
"1/7", "1/8")},
"SV": {'type': 'list',
'values': ("Off", "0.2s", "0.4s", "0.6s", "0.8s", "1.0s",
"2s", "3s", "4s", "5s")},
"TEMP": {'type': 'list',
'values': ("°F", "°C")},
"TH": _BOOL,
"TNC": _BOOL,
"TSP": _BOOL,
"TXD": {'type': 'map',
'map': (("100ms", "1"), ("200ms", "2"), ("300ms", "3"),
("400ms", "4"), ("500ms", "5"), ("750ms", "6"),
("1000ms", "7"))},
"TXH": _BOOL,
"TXI": {'type': 'list',
'values': ("30sec", "1min", "2min", "3min", "4min", "5min",
"10min", "20min", "30min")},
"TXS": _BOOL,
"TZ": {'type': 'list',
'values': ("UTC - 12:00", "UTC - 11:30", "UTC - 11:00",
"UTC - 10:30", "UTC - 10:00", "UTC - 9:30",
"UTC - 9:00", "UTC - 8:30", "UTC - 8:00",
"UTC - 7:30", "UTC - 7:00", "UTC - 6:30",
"UTC - 6:00", "UTC - 5:30", "UTC - 5:00",
"UTC - 4:30", "UTC - 4:00", "UTC - 3:30",
"UTC - 3:00", "UTC - 2:30", "UTC - 2:00",
"UTC - 1:30", "UTC - 1:00", "UTC - 0:30",
"UTC", "UTC + 0:30", "UTC + 1:00", "UTC + 1:30",
"UTC + 2:00", "UTC + 2:30", "UTC + 3:00",
"UTC + 3:30", "UTC + 4:00", "UTC + 4:30",
"UTC + 5:00", "UTC + 5:30", "UTC + 6:00",
"UTC + 6:30", "UTC + 7:00", "UTC + 7:30",
"UTC + 8:00", "UTC + 8:30", "UTC + 9:00",
"UTC + 9:30", "UTC + 10:00", "UTC + 10:30",
"UTC + 11:00", "UTC + 11:30", "UTC + 12:00")},
"UPR": {'type': 'string', 'max_len': 9,
'charset': "ABCDEFGHIJKLMNOPQRSTUVWXYZ-0123456789"},
"VCS": _BOOL,
"WAY": {'type': 'list',
'values': ("Off", "6 digit NMEA", "7 digit NMEA",
"8 digit NMEA", "9 digit NMEA", "6 digit Magellan",
"DGPS")}}
_PROGRAMMABLE_VFOS = (
("Band A, 118 MHz Sub-Band", 1, 118, 135),
("Band A, VHF Sub-Band", 2, 136, 173),
("Band B, VHF Sub-Band", 3, 144, 147),
("Band B, UHF Sub-Band", 6, 400, 479))
def __init__(self, *args, **kwargs):
chirp_common.LiveRadio.__init__(self, *args, **kwargs)
# Clear the caches
self._memcache = {}
self._setcache = {}
self._baud = 9600
self._cur_setting = 0
self._setting_count = 0
self._vfo = 0
if self.pipe:
self.pipe.timeout = 0.5
radio_id = self._get_id()
if hasattr(self, '_ID_STRING'):
id_str = self._ID_STRING
else:
id_str = self.MODEL.split(" ")[0]
if radio_id != id_str:
raise Exception("Radio reports %s (not %s)" % (radio_id,
id_str))
self._command(*self._DISABLE_AI_CMD)
# This returns the index value for CR/CW commands, not the index
# in the memory list grid
def _call_index(self, memid_or_index):
if isinstance(memid_or_index, int):
memid = self._index_to_memid(memid_or_index)
else:
memid = memid_or_index
return self._CALL_CHANS.index(memid)
# This returns the memid from the index value in CR/CW commands, not
# the index in the memory list grid
def _call_index_to_memid(self, index):
return self._CALL_CHANS[index]
def _cmd_get_memory_name(self, memid):
return "MNA", "%i,%s" % (self._vfo, memid)
def _cmd_get_memory_or_split(self, memid, split):
sd = split and 1 or 0
if self._is_call(memid):
return "CR", "%d,%d" % (self._call_index(memid), split)
return "MR", "%i,%d,%s" % (self._vfo, sd, memid)
def _cmd_set_memory_name(self, memid, name):
if self._is_call(memid):
return None
return "MNA", "%i,%s,%s" % (self._vfo, memid, name)
def _cmd_set_memory_or_split(self, memid, memory, split):
if not memory.empty:
spec = "," + ",".join(self._make_mem_spec(memid, memory))
else:
spec = ''
sd = split and 1 or 0
if (self._is_call(memid)):
return "CW", "%d,%d%s" % (self._call_index(memid), sd, spec)
return "MW", "%i,%d,%s%s" % (self._vfo, sd, memid, spec)
def _count_settings(self, entries):
for entry in entries:
if (type(entry[1]) is tuple):
self._count_settings(entry[1])
else:
self._setting_count += 1
def _create_group(self, entries, grp=None):
if grp is None:
grp = RadioSettings()
for entry in entries:
if (type(entry[1]) is tuple):
subgrp = RadioSettingGroup(entry[0].lower(), entry[0])
grp.append(self._create_group(entry[1], subgrp))
else:
setting = self._get_setting_data(entry[0])
if setting['type'] == 'undefined':
continue
if setting['type'] == 'list':
rsvl = RadioSettingValueList(setting['values'])
rs = RadioSetting(entry[0], entry[1], rsvl)
elif setting['type'] == 'bool':
rs = RadioSetting(entry[0], entry[1],
RadioSettingValueBoolean(False))
elif setting['type'] == 'string':
params = {}
if 'charset' in setting:
params['charset'] = setting['charset']
else:
params['charset'] = chirp_common.CHARSET_ASCII
minlen = 'min_len' in setting and setting['min_len'] or 0
dummy = params['charset'][0] * minlen
rsvs = RadioSettingValueString(minlen,
setting['max_len'],
dummy,
False, **params)
rs = RadioSetting(entry[0], entry[1], rsvs)
elif setting['type'] == 'integer':
rs = RadioSetting(entry[0], entry[1],
RadioSettingValueInteger(setting['min'],
setting['max'],
setting['min']))
elif setting['type'] == 'map':
rsvs = RadioSettingValueMap(setting['map'],
setting['map'][0][1])
rs = RadioSetting(entry[0], entry[1], rsvs)
else:
rs = setting['type'](entry[0], entry[1], self)
rs.kenwood_d7_loaded = False
grp.append(rs)
self._setcache[entry[0]] = rs
return grp
def _do_prerequisite(self, cmd, get, do):
if get:
arg = 'get_requires'
else:
arg = 'set_requires'
setting = self._get_setting_data(cmd)
if arg not in setting:
return
# Undo prerequisites in the reverse order they were applied
if not do:
it = reversed(setting[arg])
else:
it = iter(setting[arg])
for prereq, value in it:
entry = self._setcache[prereq]
if not entry.kenwood_d7_loaded:
self._refresh_setting(entry)
if do:
entry.kenwood_d7_old_value = entry.value.get_value()
entry.value.set_value(value)
else:
entry.value.set_value(entry.kenwood_d7_old_value)
sdata = self._get_setting_data(prereq)
self._set_setting(prereq, sdata, entry)
def _drain_input(self):
oldto = self.pipe.timeout
self.pipe.timeout = 0
# Read until a timeout
while len(self.pipe.read(1)):
pass
self.pipe.timeout = oldto
def _empty_memory(self, memid):
mem = chirp_common.Memory()
if self._is_special(memid):
mem.extd_number = memid
mem.number = self._memid_to_index(memid)
mem.empty = True
mem.immutable = self._get_immutable(memid)
return mem
def _get_id(self):
"""Get the ID of the radio attached to @ser"""
saw_response = False
for i in self._BAUDS:
LOG.info("Trying ID at baud %i" % i)
self.pipe.baudrate = i
# Try to force an error
self.pipe.write(self._CMD_DELIMITER.encode('cp1252'))
# Wait for it to be sent
self.pipe.flush()
self._drain_input()
try:
resp = self._command("ID")
except UnicodeDecodeError:
# If we got binary here, we are using the wrong rate
# or not talking to a kenwood live radio.
continue
if resp:
saw_response = True
ret = self._parse_id_response(resp)
if ret is not None:
self._baud = i
return ret
# Radio responded in the right baud rate,
# but threw an error because of all the crap
# we have been hurling at it. Retry the ID at this
# baud rate, which will almost definitely work.
if "?" in resp:
resp = self._command("ID")
ret = self._parse_id_response(resp)
if ret is not None:
self._baud = i
return ret
if saw_response:
raise errors.RadioError("Unexpected response from radio")
raise errors.RadioNoResponse()
def _get_immutable(self, memid_or_index):
if self._is_call(memid_or_index):
return ['name', 'skip']
if self._is_pscan(memid_or_index):
return ['skip']
return []
def _get_setting_data(self, setting):
if setting in self._SETTINGS:
return self._SETTINGS[setting]
if setting in self._COMMON_SETTINGS:
return self._COMMON_SETTINGS[setting]
if (setting.upper() == setting):
LOG.debug("Undefined setting: %s" % setting)
return {'type': 'undefined'}
def _get_setting_digits(self, setting_data):
if 'digits' in setting_data:
return setting_data['digits']
if setting_data['type'] == 'integer':
return len(str(setting_data['max']))
if setting_data['type'] == 'list':
return len(str(len(setting_data['values'])))
raise TypeError('Setting data should not have digits')
def _get_tmode(self, tone, ctcss, dcs):
"""Get the tone mode based on the values of the tone, ctcss, dcs"""
if dcs and int(dcs) == 1:
return "DTCS"
elif int(ctcss):
return "TSQL"
elif int(tone):
return "Tone"
else:
return ""
def _index_to_memid(self, index):
if not isinstance(index, int):
raise errors.RadioError("%s passed as memory index"
% str(type(index)))
if index <= self._UPPER:
return "%03d" % index
index -= self._UPPER
index -= 1
return self._SPECIAL_CHANS[index]
def _is_call(self, memid_or_index):
if isinstance(memid_or_index, int):
memid = self._index_to_memid(memid_or_index)
else:
memid = memid_or_index
return memid in self._CALL_CHANS
def _is_pscan(self, memid_or_index):
if isinstance(memid_or_index, int):
memid = self._index_to_memid(memid_or_index)
else:
memid = memid_or_index
if memid[0] == 'L' or memid[0] == 'U':
if memid[1:].isdigit():
return True
return False
def _is_special(self, memid_or_index):
if isinstance(memid_or_index, str):
index = self._memid_to_index(memid_or_index)
else:
index = memid_or_index
return index > self._UPPER
def _iserr(self, result):
"""Returns True if the @result from a radio is an error"""
return result in ["N", "?"]
def _keep_reading(self, result):
return False
def _kenwood_get(self, cmd):
self._do_prerequisite(cmd, True, True)
ret = None
if " " in cmd:
suffix = cmd.split(" ", 1)[1]
resp = self._kenwood_simple_get(cmd)
if resp[1][0:len(suffix)+1] == suffix + ',':
resp = (cmd, resp[1][len(suffix)+1:])
ret = resp
else:
raise errors.RadioError("Command %s response value '%s' "
"unusable" % (cmd, resp[1]))
else:
ret = self._kenwood_simple_get(cmd)
self._do_prerequisite(cmd, True, False)
return ret
def _kenwood_get_bool(self, cmd):
_cmd, result = self._kenwood_get(cmd)
return result == "1"
def _kenwood_get_int(self, cmd):
_cmd, result = self._kenwood_get(cmd)
return int(result)
def _kenwood_set_success(self, cmd, modcmd, value, response):
if response[:len(modcmd)] == modcmd:
return True
return False
def _kenwood_set(self, cmd, value):
self._do_prerequisite(cmd, False, True)
if " " in cmd:
resp = self._command(cmd + "," + value)
modcmd = cmd.split(" ", 1)[0]
else:
resp = self._command(cmd, value)
modcmd = cmd
self._do_prerequisite(cmd, False, False)
if self._kenwood_set_success(cmd, modcmd, value, resp):
return
raise errors.RadioError("Radio refused to set %s" % cmd)
def _kenwood_set_bool(self, cmd, value):
return self._kenwood_set(cmd, str(int(value)))
def _kenwood_set_int(self, cmd, value, digits=1):
return self._kenwood_set(cmd, ("%%0%ii" % digits) % value)
def _kenwood_simple_get(self, cmd):
resp = self._command(cmd)
if " " in resp:
return resp.split(" ", 1)
else:
if resp == cmd:
return [resp, ""]
else:
raise errors.RadioError("Radio refused to return %s" % cmd)
def _mem_spec_fixup(self, spec, memid, mem):
pass
def _make_mem_spec(self, memid, mem):
if mem.duplex in " -+":
duplex = util.get_dict_rev(self._DUPLEX, mem.duplex)
offset = mem.offset
else:
duplex = 0
offset = 0
spec = [
"%011i" % mem.freq,
"%X" % self._STEPS.index(mem.tuning_step),
"%i" % duplex,
"0",
"%i" % (mem.tmode == "Tone"),
"%i" % (mem.tmode == "TSQL"),
"", # DCS Flag
"%02i" % (self._TONES.index(mem.rtone) + 1),
"", # DCS Code
"%02i" % (self._TONES.index(mem.ctone) + 1),
"%09i" % offset,
"%i" % util.get_dict_rev(self._MODES, mem.mode)]
if not self._is_call(memid):
spec.append("%i" % ((mem.skip == "S") and 1 or 0))
self._mem_spec_fixup(spec, memid, mem)
return tuple(spec)
def _make_split_spec(self, mem):
return ("%011i" % mem.offset, "0")
def _memid_to_index(self, memid):
if not isinstance(memid, str):
raise errors.RadioError("%s passed as memory id"
% str(type(memid)))
if memid in self._SPECIAL_CHANS:
return self._SPECIAL_CHANS.index(memid) + self._UPPER + 1
return int(memid)
def _parse_id_response(self, resp):
# most kenwood VHF+ radios
if " " in resp:
return resp.split(" ")[1]
return None
def _parse_mem_fixup(self, mem, spec):
pass
def _parse_mem(self, result):
mem = chirp_common.Memory()
value = result.split(" ")[1]
spec = value.split(",")
if len(spec) == 14: # This is a Call memory
spec.insert(1, 0) # Add "bank"
spec.insert(15, 0) # Add "skip"
memid = self._call_index_to_memid(int(spec[0]))
mem.extd_number = memid
mem.number = self._memid_to_index(mem.extd_number)
elif spec[2].isdigit(): # Normal memory
mem.number = int(spec[2])
memid = self._index_to_memid(mem.number)
else: # Program Scan memory
memid = spec[2]
mem.extd_number = memid
mem.number = self._memid_to_index(mem.extd_number)
mem.immutable = self._get_immutable(memid)
mem.freq = int(spec[3], 10)
mem.tuning_step = self._STEPS[int(spec[4], 16)]
mem.duplex = self._DUPLEX[int(spec[5])]
mem.tmode = self._get_tmode(spec[7], spec[8], spec[9])
mem.rtone = self._TONES[int(spec[10]) - 1]
mem.ctone = self._TONES[int(spec[12]) - 1]
if spec[13]:
mem.offset = int(spec[13])
else:
mem.offset = 0
mem.mode = self._MODES[int(spec[14])]
if 'skip' not in mem.immutable:
mem.skip = int(spec[15]) and "S" or ""
self._parse_mem_fixup(mem, spec)
return mem
def _parse_split(self, mem, result):
value = result.split(" ")[1]
spec = value.split(",")
mem.duplex = "split"
# TODO: There's another parameter here... not sure what it is
if len(spec) == 14: # Call memory
mem.offset = int(spec[2])
else:
mem.offset = int(spec[3])
def _refresh_setting(self, entry):
name = entry.get_name()
setting = self._get_setting_data(name)
# TODO: It would be nice to bump_wait_dialog here...
# Also, this is really only useful for the cli. :(
if self.status_fn:
status = chirp_common.Status()
status.cur = self._cur_setting
status.max = self._setting_count
status.msg = "Fetching %-30s" % entry.get_shortname()
# self.bump_wait_dialog(int(status.cur * 100 / status.max),
# "Fetching %s" % entry[1])
self.status_fn(status)
self._cur_setting += 1
if setting['type'] == 'list':
value = self._kenwood_get_int(name)
entry.value.set_index(value)
elif setting['type'] == 'bool':
value = self._kenwood_get_bool(name)
entry.value.set_value(value)
elif setting['type'] == 'string':
value = self._kenwood_get(name)[1]
entry.value.set_value(value)
elif setting['type'] == 'integer':
value = self._kenwood_get_int(name)
entry.value.set_value(value)
elif setting['type'] == 'map':
value = self._kenwood_get(name)[1]
entry.value.set_mem_val(value)
else:
entry.kenwood_d7_read_value()
if hasattr(entry, 'value'):
if hasattr(entry.value, '_has_changed'):
entry.value._has_changed = False
entry.kenwood_d7_loaded = True
def _refresh_settings(self):
for entry in self._setcache.values():
if not entry.kenwood_d7_loaded:
try:
self._refresh_setting(entry)
except errors.RadioError as e:
# Don't fail if we can't read a setting. Some radios
# refuse to return MCL (perhaps v1 radios?)
LOG.error("Error refreshing setting %s: %s"
% (entry.get_name(), str(e)))
def _validate_memid(self, memid_or_index):
if isinstance(memid_or_index, int):
index = memid_or_index
memid = self._index_to_memid(memid_or_index)
else:
memid = memid_or_index
index = self._memid_to_index(memid_or_index)
if memid in self._SPECIAL_CHANS:
pass
elif index < self._LOWER or index > self._UPPER:
raise errors.InvalidMemoryLocation(
"Number must be between %i and %i" % (self._LOWER,
self._UPPER))
return index, memid
def _validate_memory(self, memory):
if memory.number is None:
return self._validate_memid(memory.extd_number)
return self._validate_memid(memory.number)
def _set_setting(self, name, sdata, element):
if sdata['type'] == 'bool':
value = element.value.get_value()
self._kenwood_set_bool(name, value)
element.value._has_changed = False
elif sdata['type'] == 'integer':
value = element.value.get_value()
digits = self._get_setting_digits(sdata)
self._kenwood_set_int(name, value, digits)
element.value._has_changed = False
elif sdata['type'] == 'string':
value = element.value.get_value()
self._kenwood_set(name, value)
element.value._has_changed = False
elif sdata['type'] == 'list':
value = element.value.get_value()
digits = self._get_setting_digits(sdata)
self._kenwood_set_int(name, sdata['values'].index(value),
digits)
element.value._has_changed = False
elif sdata['type'] == 'map':
self._kenwood_set(name, element.value.get_mem_val())
element.value._has_changed = False
# TODO: I would like to have tried this first then fetched
# value, but we can't use hasattr() on settings due to the
# Magic foo.value attribute in chirp/settings.py. Instead,
# I need to get_value() each time. :(
elif hasattr(element, 'kenwood_d7_set_value'):
element.kenwood_d7_set_value()
else:
raise TypeError('No way to set %s value' % name)
def _command(self, cmd, *args):
"""Send @cmd to radio via @ser"""
# This lock is needed to allow clicking the settings tab while
# the memories are still loading. Most important with the TH-D7A
# and TH-D7A(G) with the 9600bps maximum.
with self._LOCK:
if args:
cmd += self._ARG_DELIMITER + self._ARG_DELIMITER.join(args)
cmd += self._CMD_DELIMITER
self._drain_input()
LOG.debug("PC->RADIO: %s" % cmd.strip())
self.pipe.write(cmd.encode('cp1252'))
cd = self._CMD_DELIMITER.encode('cp1252')
keep_reading = True
while keep_reading:
result = self.pipe.read_until(cd).decode('cp1252')
if result.endswith(self._CMD_DELIMITER):
keep_reading = self._keep_reading(result)
LOG.debug("RADIO->PC: %r" % result.strip())
result = result[:-1]
else:
keep_reading = False
LOG.error("Timeout waiting for data")
return result.strip()
def erase_memory(self, memid_or_index):
index, memid = self._validate_memid(memid_or_index)
if memid not in self._memcache:
return
# TODO: Can't disable the menu item?
if self._is_call(memid):
# Raising an error just makes the memory errored
return
cmd = self._cmd_set_memory_or_split(memid, self._empty_memory(memid),
False)
resp = self._command(*cmd)
if self._iserr(resp):
raise errors.RadioError("Radio refused delete of %s" % memid)
del self._memcache[memid]
def get_features(self, *args, **kwargs):
rf = chirp_common.RadioFeatures(*args, **kwargs)
rf.valid_tones = tuple([x for x in self._TONES if x != 69.3])
rf.has_settings = True
rf.has_dtcs = False
rf.has_dtcs_polarity = False
rf.has_bank = False
rf.has_mode = True
rf.has_tuning_step = True
rf.can_odd_split = True
rf.valid_name_length = 8
rf.valid_duplexes = ["", "-", "+", "split"]
rf.valid_modes = list(self._MODES.values())
rf.valid_tmodes = ["", "Tone", "TSQL"]
rf.valid_characters = \
chirp_common.CHARSET_ALPHANUMERIC + "/.-+*)('&%$#! ~}|{"
rf.valid_tuning_steps = self._STEPS
rf.memory_bounds = (self._LOWER, self._UPPER)
rf.valid_special_chans = self._SPECIAL_CHANS
rf.valid_bands = self._BANDS
return rf
def get_memory(self, memid_or_index):
index, memid = self._validate_memid(memid_or_index)
if memid in self._memcache and not self._NOCACHE:
return self._memcache[memid]
result = self._command(*self._cmd_get_memory_or_split(memid,
False))
if result == "N":
mem = self._empty_memory(memid)
self._memcache[memid] = mem
return mem
elif self._ARG_DELIMITER not in result:
LOG.error("Not sure what to do with this: `%s'" % result)
raise errors.RadioError("Unexpected result returned from radio")
mem = self._parse_mem(result)
self._memcache[memid] = mem
if self._HAS_NAME:
cmd = self._cmd_get_memory_name(memid)
if cmd is not None:
result = self._command(*cmd)
if " " in result:
value = result.split(" ", 1)[1]
mem.name = value.split(",", 2)[2]
if mem.duplex == "" and self._SPLIT:
cmd = self._cmd_get_memory_or_split(memid, True)
if cmd is not None:
result = self._command(*cmd)
if " " in result:
self._parse_split(mem, result)
return mem
def get_settings(self):
if self._setting_count == 0:
self._count_settings(self._SETTINGS_MENUS)
ret = self._create_group(self._SETTINGS_MENUS)
self._cur_setting = 0
self._refresh_settings()
status = chirp_common.Status()
status.cur = self._setting_count
status.max = self._setting_count
status.msg = "%-40s" % "Done"
self.status_fn(status)
return ret
def set_memory(self, memory):
index, memid = self._validate_memory(memory)
if memory.empty:
self.erase_memory(memid)
LOG.debug("set_memory passed an empty memory, perhaps "
"you wanted erase_memory()?")
return
r1 = self._command(*self._cmd_set_memory_or_split(memid, memory,
False))
if self._iserr(r1):
raise errors.InvalidDataError("Radio refused %s" % memid)
self._memcache[memid] = memory.dupe()
if self._HAS_NAME:
r2cmd = self._cmd_set_memory_name(memid, memory.name)
if r2cmd is not None:
r2 = self._command(*r2cmd)
if self._iserr(r2):
raise errors.InvalidDataError("Radio refused name %s: %s" %
(memid, repr(memory.name)))
if memory.duplex == "split" and self._SPLIT:
result = self._command(*self._cmd_set_memory_or_split(memid,
memory,
True))
if self._iserr(result):
raise errors.InvalidDataError("Radio refused %s" %
memid)
def set_settings(self, settings):
for element in settings:
name = element.get_name()
sdata = self._get_setting_data(name)
if sdata['type'] == 'undefined':
if isinstance(element, RadioSettingGroup):
self.set_settings(element)
continue
if not element.changed():
continue
self._set_setting(name, sdata, element)
@directory.register
class THD7Radio(KenwoodD7Family):
"""Kenwood TH-D7"""
MODEL = "TH-D7"
_BANDS = [(118000000, 174000000), (400000000, 470000000)]
_SETTINGS = {
"BEPT": {'type': 'list',
'values': ("Off", "Mine", "All New")},
"DTB": {'type': 'list',
'values': ("A", "B")},
"DTBA": {'type': 'list',
'values': ("A", "B", "A:TX/B:RX", "A:RX/B:TX")},
"GU": {'type': 'list',
'values': ("Not Used", "NMEA")},
"MP": {'type': 'position'},
"STAT": {'type': 'string', 'max_len': 20},
"TEMP": {'type': 'list',
'values': ("Miles / °F", "Kilometers / °C")}}
_SETTINGS_MENUS = (
('Main',
(("ASC 0", "Automatic Simplex Check Band A"),
("ASC 1", "Automatic Simplex Check Band B"),
("BAL", "Balance"),
("BC", "Band"),
("BEL 0", "Tone Alert Band A"),
("BEL 1", "Tone Alert Band B"),
("CH", "Channel Mode Display"),
("DL", "Dual"),
("LMP", "Lamp"),
("LK", "Lock"),
# MNF requires that the *current* VFO be in MR mode
# Also, it has a direct key
# ("MNF", "Memory Display Mode"),
("SQ 0", "Band A Squelch"),
("SQ 1", "Band B Squelch"),
# We likely don't want to enable this from Chirp...
# ("TNC", "Packet Mode"))),
)),
('Radio',
(('Display',
(("MES", "Power-ON Message"),
("CNT", "Contrast"))),
('Save',
(("SV", "Battery Saver Interval"),
("APO", "Automatic Power Off (APO)"))),
('DTMF',
(("dtmfmem 00", "Number Store #0"),
("dtmfmem 01", "Number Store #1"),
("dtmfmem 02", "Number Store #2"),
("dtmfmem 03", "Number Store #3"),
("dtmfmem 04", "Number Store #4"),
("dtmfmem 05", "Number Store #5"),
("dtmfmem 06", "Number Store #6"),
("dtmfmem 07", "Number Store #7"),
("dtmfmem 08", "Number Store #8"),
("dtmfmem 09", "Number Store #9"),
("TSP", "TX Speed"),
("TXH", "TX Hold"),
("PT", "Pause"))),
('TNC',
(("DTB", "Data band select"),
("DS", "DCD sense"))),
('AUX',
(("ARO", "Automatic Repeater Offset"),
("SCR", "Scan Resume"),
("BEP", "Key Beep"),
("ELK", "Tuning Enable"),
("TXS", "TX Inhibit"),
("AIP", "Advanced Intercept Point"),
("TH", "TX Hold, 1750 Hz"),
("XXX", "VHF band narrow TX deviation"))))),
('APRS',
(("MYC", "My call sign"),
("GU", "GPS receiver"),
# Untested... D7G and D700 take an index, but since D7A only has
# one position and is older, it may not.
# ("MP", "Latitude / longitude data"),
("POSC", "Position comment"),
("ICO", "Station icon"),
("STAT", "Status text"),
("TXI", "Beacon transmit interval"),
("PP", "Packet path"),
("DTX", "Beacon transmit method"),
("UPR", "Group code"),
("ARL", "Reception restriction distance"),
("TEMP", "Unit for temperature"))),
('SSTV',
(("SMY", "My call sign"),
("MAC", "Color for call sign"),
("SMSG", "Message"),
("SMC", "Color for message"),
("RSV", "RSV report"),
("RSC", "Color for RSV report"),
("VCS", "VC-H1 Control"))),
('SkyCommand',
(("SCC", "Commander call sign"),
("SCT", "Transporter call sign"),
("SKTN", "Tone frequency select"))))
@directory.register
class THD7GRadio(KenwoodD7Family):
"""Kenwood TH-D7G"""
MODEL = "TH-D7G"
_BANDS = [(118000000, 174000000), (400000000, 470000000)]
_APRS_EXTRA = (('WEATHER Station (blue)', '1,/#'),
('House QTH (VHF)', '1,/-'),
('Boy Scouts', '1,/,'),
('Campground (Portable ops)', '1,/;'),
('FIRE', '1,/:'),
('Police, Sheriff', '1,/!'),
('SERVER for Files', '1,/?'),
('X', '1,/.'),
('Small AIRCRAFT (SSID-11)', "1,/'"),
('reserved (was rain)', '1,/"'),
('Mobile Satellite Station', '1,/('),
('Wheelchair (handicapped)', '1,/)'),
('Human/Person (SSID-7)', '1,/['),
('MAIL/PostOffice(was PBBS)', '1,/]'),
('/{ (J1)', '1,/{'),
('/} (J3)', '1,/}'),
('HC FUTURE predict (dot)', '1,/@'),
('SnowMobile', '1,/*'),
('Red Dot', '1,//'),
('TRIANGLE(DF station)', '1,/\\'),
('HF GATEway', '1,/&'),
('DIGI (white center)', '1,/#'),
('DX CLUSTER', '1,/%'),
('Dish Antenna', '1,/`'),
('LARGE AIRCRAFT', '1,/^'),
('Red Cross', '1,/+'),
('Motorcycle (SSID-10)', '1,/<'),
('RAILROAD ENGINE', '1,/='),
('CAR (SSID-9)', '1,/>'),
('TNC Stream Switch', '1,/|'),
('TNC Stream Switch', '1,/~'),
('PHONE', '1,/$'),
('# circle (obsolete)', '1,/0'),
('TBD (these were numbered)', '1,/1'),
('TBD (circles like pool)', '1,/2'),
('TBD (balls. But with)', '1,/3'),
('TBD (overlays, we can)', '1,/4'),
("TBD (put all #'s on one)", '1,/5'),
('TBD (So 1-9 are available)', '1,/6'),
('TBD (for new uses?)', '1,/7'),
('TBD (They are often used)', '1,/8'),
('TBD (as mobiles at events)', '1,/9'),
('Aid Station', '1,/A'),
('AMBULANCE (SSID-1)', '1,/a'),
('BBS or PBBS', '1,/B'),
('BIKE (SSID-4)', '1,/b'),
('Canoe', '1,/C'),
('Incident Command Post', '1,/c'),
('/D (PD)', '1,/D'),
('Fire dept', '1,/d'),
('EYEBALL (Events, etc!)', '1,/E'),
('HORSE (equestrian)', '1,/e'),
('Farm Vehicle (tractor)', '1,/F'),
('FIRE TRUCK (SSID-3)', '1,/f'),
('Grid Square (6 digit)', '1,/G'),
('Glider', '1,/g'),
('HOTEL (blue bed symbol)', '1,/H'),
('HOSPITAL', '1,/h'),
('TcpIp on air network stn', '1,/I'),
('IOTA (islands on the air)', '1,/i'),
('/J (PJ)', '1,/J'),
('JEEP (SSID-12)', '1,/j'),
('School', '1,/K'),
('TRUCK (SSID-14)', '1,/k'),
('PC user (Jan 03)', '1,/L'),
('Laptop (Jan 03) (Feb 07)', '1,/l'),
('MacAPRS', '1,/M'),
('Mic-E Repeater', '1,/m'),
('NTS Station', '1,/N'),
('Node (black bulls-eye)', '1,/n'),
('BALLOON (SSID-11)', '1,/O'),
('EOC', '1,/o'),
('Police', '1,/P'),
('ROVER (puppy, or dog)', '1,/p'),
('TBD', '1,/Q'),
('GRID SQ shown above 128 m', '1,/q'),
('REC. VEHICLE (SSID-13)', '1,/R'),
('Repeater (Feb 07)', '1,/r'),
('SHUTTLE', '1,/S'),
('SHIP (pwr boat) (SSID-8)', '1,/s'),
('SSTV', '1,/T'),
('TRUCK STOP', '1,/t'),
('BUS (SSID-2)', '1,/U'),
('TRUCK (18 wheeler)', '1,/u'),
('ATV', '1,/V'),
('VAN (SSID-15)', '1,/v'),
('National WX Service Site', '1,/W'),
('WATER station', '1,/w'),
('HELO (SSID-6)', '1,/X'),
('xAPRS (Unix)', '1,/x'),
('YACHT (sail) (SSID-5)', '1,/Y'),
('YAGI @ QTH', '1,/y'),
('WinAPRS', '1,/Z'),
('TBD', '1,/z'),
('# WX site (green digi)', '1,\\_'),
('# WX site (green digi) with Zero overlaid', '1,0_'),
('# WX site (green digi) with One overlaid', '1,1_'),
('# WX site (green digi) with Two overlaid', '1,2_'),
('# WX site (green digi) with Three overlaid', '1,3_'),
('# WX site (green digi) with Four overlaid', '1,4_'),
('# WX site (green digi) with Five overlaid', '1,5_'),
('# WX site (green digi) with Six overlaid', '1,6_'),
('# WX site (green digi) with Seven overlaid', '1,7_'),
('# WX site (green digi) with Eight overlaid', '1,8_'),
('# WX site (green digi) with Nine overlaid', '1,9_'),
('# WX site (green digi) with Letter A overlaid', '1,A_'),
('# WX site (green digi) with Letter B overlaid', '1,B_'),
('# WX site (green digi) with Letter C overlaid', '1,C_'),
('# WX site (green digi) with Letter D overlaid', '1,D_'),
('# WX site (green digi) with Letter E overlaid', '1,E_'),
('# WX site (green digi) with Letter F overlaid', '1,F_'),
('# WX site (green digi) with Letter G overlaid', '1,G_'),
('# WX site (green digi) with Letter H overlaid', '1,H_'),
('# WX site (green digi) with Letter I overlaid', '1,I_'),
('# WX site (green digi) with Letter J overlaid', '1,J_'),
('# WX site (green digi) with Letter K overlaid', '1,K_'),
('# WX site (green digi) with Letter L overlaid', '1,L_'),
('# WX site (green digi) with Letter M overlaid', '1,M_'),
('# WX site (green digi) with Letter N overlaid', '1,N_'),
('# WX site (green digi) with Letter O overlaid', '1,O_'),
('# WX site (green digi) with Letter P overlaid', '1,P_'),
('# WX site (green digi) with Letter Q overlaid', '1,Q_'),
('# WX site (green digi) with Letter R overlaid', '1,R_'),
('# WX site (green digi) with Letter S overlaid', '1,S_'),
('# WX site (green digi) with Letter T overlaid', '1,T_'),
('# WX site (green digi) with Letter U overlaid', '1,U_'),
('# WX site (green digi) with Letter V overlaid', '1,V_'),
('# WX site (green digi) with Letter W overlaid', '1,W_'),
('# WX site (green digi) with Letter X overlaid', '1,X_'),
('# WX site (green digi) with Letter Y overlaid', '1,Y_'),
('# WX site (green digi) with Letter Z overlaid', '1,Z_'),
('House (H=HF) (O = Op Present)', '1,\\-'),
('Girl Scouts', '1,\\,'),
('Park/Picnic + overlay events', '1,\\;'),
('AVAIL (Hail ==> ` ovly H)', '1,\\:'),
('EMERGENCY (and overlays)', '1,\\!'),
('INFO Kiosk (Blue box with ?)', '1,\\?'),
('Ambiguous (Big Question mark)', '1,\\.'),
('Crash (& now Incident sites)', "1,\\'"),
('reserved', '1,\\"'),
('CLOUDY (other clouds w ovrly)', '1,\\('),
('Firenet MEO, MODIS Earth Obs.', '1,\\)'),
('W.Cloud (& humans w Ovrly)', '1,\\['),
('AVAIL', '1,\\]'),
('AVAIL? (Fog ==> E ovly F)', '1,\\{'),
('AVAIL? (maybe)', '1,\\}'),
('HURICANE/Trop-Storm', '1,\\@'),
('AVAIL (SNOW moved to ` ovly S)', '1,\\*'),
('Waypoint Destination See APRSdos MOBILE.txt', '1,\\/'),
('New overlayable GPS symbol', '1,\\\\'),
('I=Igte R=RX T=1hopTX 2=2hopTX', '1,\\&'),
('I=Igte R=RX T=1hopTX 2=2hopTX with Zero overlaid',
'1,0&'),
('I=Igte R=RX T=1hopTX 2=2hopTX with One overlaid', '1,1&'),
('TX igate with path set to 2 hops', '1,2&'),
('I=Igte R=RX T=1hopTX 2=2hopTX with Three overlaid',
'1,3&'),
('I=Igte R=RX T=1hopTX 2=2hopTX with Four overlaid',
'1,4&'),
('I=Igte R=RX T=1hopTX 2=2hopTX with Five overlaid',
'1,5&'),
('I=Igte R=RX T=1hopTX 2=2hopTX with Six overlaid', '1,6&'),
('I=Igte R=RX T=1hopTX 2=2hopTX with Seven overlaid',
'1,7&'),
('I=Igte R=RX T=1hopTX 2=2hopTX with Eight overlaid',
'1,8&'),
('I=Igte R=RX T=1hopTX 2=2hopTX with Nine overlaid',
'1,9&'),
('I=Igte R=RX T=1hopTX 2=2hopTX with Letter A overlaid',
'1,A&'),
('I=Igte R=RX T=1hopTX 2=2hopTX with Letter B overlaid',
'1,B&'),
('I=Igte R=RX T=1hopTX 2=2hopTX with Letter C overlaid',
'1,C&'),
('I=Igte R=RX T=1hopTX 2=2hopTX with Letter D overlaid',
'1,D&'),
('I=Igte R=RX T=1hopTX 2=2hopTX with Letter E overlaid',
'1,E&'),
('I=Igte R=RX T=1hopTX 2=2hopTX with Letter F overlaid',
'1,F&'),
('I=Igte R=RX T=1hopTX 2=2hopTX with Letter G overlaid',
'1,G&'),
('I=Igte R=RX T=1hopTX 2=2hopTX with Letter H overlaid',
'1,H&'),
('Igate Generic', '1,I&'),
('I=Igte R=RX T=1hopTX 2=2hopTX with Letter J overlaid',
'1,J&'),
('I=Igte R=RX T=1hopTX 2=2hopTX with Letter K overlaid',
'1,K&'),
('Lora Igate', '1,L&'),
('I=Igte R=RX T=1hopTX 2=2hopTX with Letter M overlaid',
'1,M&'),
('I=Igte R=RX T=1hopTX 2=2hopTX with Letter N overlaid',
'1,N&'),
('I=Igte R=RX T=1hopTX 2=2hopTX with Letter O overlaid',
'1,O&'),
('PSKmail node', '1,P&'),
('I=Igte R=RX T=1hopTX 2=2hopTX with Letter Q overlaid',
'1,Q&'),
('Receive only Igate', '1,R&'),
('I=Igte R=RX T=1hopTX 2=2hopTX with Letter S overlaid',
'1,S&'),
('TX igate with path set to 1 hop only', '1,T&'),
('I=Igte R=RX T=1hopTX 2=2hopTX with Letter U overlaid',
'1,U&'),
('I=Igte R=RX T=1hopTX 2=2hopTX with Letter V overlaid',
'1,V&'),
('WIRES-X', '1,W&'),
('I=Igte R=RX T=1hopTX 2=2hopTX with Letter X overlaid',
'1,X&'),
('I=Igte R=RX T=1hopTX 2=2hopTX with Letter Y overlaid',
'1,Y&'),
('I=Igte R=RX T=1hopTX 2=2hopTX with Letter Z overlaid',
'1,Z&'),
('OVERLAY DIGI (green star)', '1,\\#'),
('OVERLAY DIGI (green star) with Zero overlaid', '1,0#'),
('OVERLAY DIGI (green star) with One overlaid', '1,1#'),
('OVERLAY DIGI (green star) with Two overlaid', '1,2#'),
('OVERLAY DIGI (green star) with Three overlaid', '1,3#'),
('OVERLAY DIGI (green star) with Four overlaid', '1,4#'),
('OVERLAY DIGI (green star) with Five overlaid', '1,5#'),
('OVERLAY DIGI (green star) with Six overlaid', '1,6#'),
('OVERLAY DIGI (green star) with Seven overlaid', '1,7#'),
('OVERLAY DIGI (green star) with Eight overlaid', '1,8#'),
('OVERLAY DIGI (green star) with Nine overlaid', '1,9#'),
('OVERLAY DIGI (green star) with Letter A overlaid',
'1,A#'),
('OVERLAY DIGI (green star) with Letter B overlaid',
'1,B#'),
('OVERLAY DIGI (green star) with Letter C overlaid',
'1,C#'),
('OVERLAY DIGI (green star) with Letter D overlaid',
'1,D#'),
('OVERLAY DIGI (green star) with Letter E overlaid',
'1,E#'),
('OVERLAY DIGI (green star) with Letter F overlaid',
'1,F#'),
('OVERLAY DIGI (green star) with Letter G overlaid',
'1,G#'),
('OVERLAY DIGI (green star) with Letter H overlaid',
'1,H#'),
('OVERLAY DIGI (green star) with Letter I overlaid',
'1,I#'),
('OVERLAY DIGI (green star) with Letter J overlaid',
'1,J#'),
('OVERLAY DIGI (green star) with Letter K overlaid',
'1,K#'),
('OVERLAY DIGI (green star) with Letter L overlaid',
'1,L#'),
('OVERLAY DIGI (green star) with Letter M overlaid',
'1,M#'),
('OVERLAY DIGI (green star) with Letter N overlaid',
'1,N#'),
('OVERLAY DIGI (green star) with Letter O overlaid',
'1,O#'),
('OVERLAY DIGI (green star) with Letter P overlaid',
'1,P#'),
('OVERLAY DIGI (green star) with Letter Q overlaid',
'1,Q#'),
('OVERLAY DIGI (green star) with Letter R overlaid',
'1,R#'),
('OVERLAY DIGI (green star) with Letter S overlaid',
'1,S#'),
('OVERLAY DIGI (green star) with Letter T overlaid',
'1,T#'),
('OVERLAY DIGI (green star) with Letter U overlaid',
'1,U#'),
('OVERLAY DIGI (green star) with Letter V overlaid',
'1,V#'),
('OVERLAY DIGI (green star) with Letter W overlaid',
'1,W#'),
('OVERLAY DIGI (green star) with Letter X overlaid',
'1,X#'),
('OVERLAY DIGI (green star) with Letter Y overlaid',
'1,Y#'),
('OVERLAY DIGI (green star) with Letter Z overlaid',
'1,Z#'),
('Power Plant with overlay', '1,\\%'),
('Rain (all types w ovrly)', '1,\\`'),
('other Aircraft ovrlys (2014)', '1,\\^'),
('other Aircraft ovrlys (2014) with Zero overlaid', '1,0^'),
('other Aircraft ovrlys (2014) with One overlaid', '1,1^'),
('other Aircraft ovrlys (2014) with Two overlaid', '1,2^'),
('other Aircraft ovrlys (2014) with Three overlaid',
'1,3^'),
('other Aircraft ovrlys (2014) with Four overlaid', '1,4^'),
('other Aircraft ovrlys (2014) with Five overlaid', '1,5^'),
('other Aircraft ovrlys (2014) with Six overlaid', '1,6^'),
('other Aircraft ovrlys (2014) with Seven overlaid',
'1,7^'),
('other Aircraft ovrlys (2014) with Eight overlaid',
'1,8^'),
('other Aircraft ovrlys (2014) with Nine overlaid', '1,9^'),
('Autonomous', '1,A^'),
('other Aircraft ovrlys (2014) with Letter B overlaid',
'1,B^'),
('other Aircraft ovrlys (2014) with Letter C overlaid',
'1,C^'),
('Drone', '1,D^'),
('Electric aircraft', '1,E^'),
('other Aircraft ovrlys (2014) with Letter F overlaid',
'1,F^'),
('other Aircraft ovrlys (2014) with Letter G overlaid',
'1,G^'),
('Hovercraft', '1,H^'),
('other Aircraft ovrlys (2014) with Letter I overlaid',
'1,I^'),
('JET', '1,J^'),
('other Aircraft ovrlys (2014) with Letter K overlaid',
'1,K^'),
('other Aircraft ovrlys (2014) with Letter L overlaid',
'1,L^'),
('Missle', '1,M^'),
('other Aircraft ovrlys (2014) with Letter N overlaid',
'1,N^'),
('other Aircraft ovrlys (2014) with Letter O overlaid',
'1,O^'),
('Prop', '1,P^'),
('other Aircraft ovrlys (2014) with Letter Q overlaid',
'1,Q^'),
('Remotely Piloted', '1,R^'),
('Solar Powered', '1,S^'),
('other Aircraft ovrlys (2014) with Letter T overlaid',
'1,T^'),
('other Aircraft ovrlys (2014) with Letter U overlaid',
'1,U^'),
('Vertical takeoff', '1,V^'),
('other Aircraft ovrlys (2014) with Letter W overlaid',
'1,W^'),
('Experimental', '1,X^'),
('other Aircraft ovrlys (2014) with Letter Y overlaid',
'1,Y^'),
('other Aircraft ovrlys (2014) with Letter Z overlaid',
'1,Z^'),
('Church', '1,\\+'),
('ADVISORY (one WX flag)', '1,\\<'),
('avail. symbol overlay group', '1,\\='),
('OVERLAYED CARs & Vehicles', '1,\\>'),
('OVERLAYED CARs & Vehicles with Zero overlaid', '1,0>'),
('OVERLAYED CARs & Vehicles with One overlaid', '1,1>'),
('OVERLAYED CARs & Vehicles with Two overlaid', '1,2>'),
('Model 3 (Tesla)', '1,3>'),
('OVERLAYED CARs & Vehicles with Four overlaid', '1,4>'),
('OVERLAYED CARs & Vehicles with Five overlaid', '1,5>'),
('OVERLAYED CARs & Vehicles with Six overlaid', '1,6>'),
('OVERLAYED CARs & Vehicles with Seven overlaid', '1,7>'),
('OVERLAYED CARs & Vehicles with Eight overlaid', '1,8>'),
('OVERLAYED CARs & Vehicles with Nine overlaid', '1,9>'),
('OVERLAYED CARs & Vehicles with Letter A overlaid',
'1,A>'),
('BEV - Battery EV', '1,B>'),
('OVERLAYED CARs & Vehicles with Letter C overlaid',
'1,C>'),
('DIY - Do it yourself ', '1,D>'),
('Ethanol (was electric)', '1,E>'),
('Fuelcell or hydrogen', '1,F>'),
('OVERLAYED CARs & Vehicles with Letter G overlaid',
'1,G>'),
('Hybrid', '1,H>'),
('OVERLAYED CARs & Vehicles with Letter I overlaid',
'1,I>'),
('OVERLAYED CARs & Vehicles with Letter J overlaid',
'1,J>'),
('OVERLAYED CARs & Vehicles with Letter K overlaid',
'1,K>'),
('Leaf', '1,L>'),
('OVERLAYED CARs & Vehicles with Letter M overlaid',
'1,M>'),
('OVERLAYED CARs & Vehicles with Letter N overlaid',
'1,N>'),
('OVERLAYED CARs & Vehicles with Letter O overlaid',
'1,O>'),
('PHEV - Plugin-hybrid', '1,P>'),
('OVERLAYED CARs & Vehicles with Letter Q overlaid',
'1,Q>'),
('OVERLAYED CARs & Vehicles with Letter R overlaid',
'1,R>'),
('Solar powered', '1,S>'),
('Tesla (temporary)', '1,T>'),
('OVERLAYED CARs & Vehicles with Letter U overlaid',
'1,U>'),
('Volt (temporary)', '1,V>'),
('OVERLAYED CARs & Vehicles with Letter W overlaid',
'1,W>'),
('Model X', '1,X>'),
('OVERLAYED CARs & Vehicles with Letter Y overlaid',
'1,Y>'),
('OVERLAYED CARs & Vehicles with Letter Z overlaid',
'1,Z>'),
('TNC Stream Switch', '1,\\|'),
('TNC Stream Switch', '1,\\~'),
('Bank or ATM (green box)', '1,\\$'),
('CIRCLE (IRLP/Echolink/WIRES)', '1,\\0'),
('CIRCLE (IRLP/Echolink/WIRES) with Zero overlaid', '1,00'),
('CIRCLE (IRLP/Echolink/WIRES) with One overlaid', '1,10'),
('CIRCLE (IRLP/Echolink/WIRES) with Two overlaid', '1,20'),
('CIRCLE (IRLP/Echolink/WIRES) with Three overlaid',
'1,30'),
('CIRCLE (IRLP/Echolink/WIRES) with Four overlaid', '1,40'),
('CIRCLE (IRLP/Echolink/WIRES) with Five overlaid', '1,50'),
('CIRCLE (IRLP/Echolink/WIRES) with Six overlaid', '1,60'),
('CIRCLE (IRLP/Echolink/WIRES) with Seven overlaid',
'1,70'),
('CIRCLE (IRLP/Echolink/WIRES) with Eight overlaid',
'1,80'),
('CIRCLE (IRLP/Echolink/WIRES) with Nine overlaid', '1,90'),
('Allstar Node', '1,A0'),
('CIRCLE (IRLP/Echolink/WIRES) with Letter B overlaid',
'1,B0'),
('CIRCLE (IRLP/Echolink/WIRES) with Letter C overlaid',
'1,C0'),
('CIRCLE (IRLP/Echolink/WIRES) with Letter D overlaid',
'1,D0'),
('Echolink Node', '1,E0'),
('CIRCLE (IRLP/Echolink/WIRES) with Letter F overlaid',
'1,F0'),
('CIRCLE (IRLP/Echolink/WIRES) with Letter G overlaid',
'1,G0'),
('CIRCLE (IRLP/Echolink/WIRES) with Letter H overlaid',
'1,H0'),
('IRLP repeater', '1,I0'),
('CIRCLE (IRLP/Echolink/WIRES) with Letter J overlaid',
'1,J0'),
('CIRCLE (IRLP/Echolink/WIRES) with Letter K overlaid',
'1,K0'),
('CIRCLE (IRLP/Echolink/WIRES) with Letter L overlaid',
'1,L0'),
('CIRCLE (IRLP/Echolink/WIRES) with Letter M overlaid',
'1,M0'),
('CIRCLE (IRLP/Echolink/WIRES) with Letter N overlaid',
'1,N0'),
('CIRCLE (IRLP/Echolink/WIRES) with Letter O overlaid',
'1,O0'),
('CIRCLE (IRLP/Echolink/WIRES) with Letter P overlaid',
'1,P0'),
('CIRCLE (IRLP/Echolink/WIRES) with Letter Q overlaid',
'1,Q0'),
('CIRCLE (IRLP/Echolink/WIRES) with Letter R overlaid',
'1,R0'),
('Staging Area', '1,S0'),
('CIRCLE (IRLP/Echolink/WIRES) with Letter T overlaid',
'1,T0'),
('CIRCLE (IRLP/Echolink/WIRES) with Letter U overlaid',
'1,U0'),
('Echolink and IRLP', '1,V0'),
('WIRES (Yaesu VOIP)', '1,W0'),
('CIRCLE (IRLP/Echolink/WIRES) with Letter X overlaid',
'1,X0'),
('CIRCLE (IRLP/Echolink/WIRES) with Letter Y overlaid',
'1,Y0'),
('CIRCLE (IRLP/Echolink/WIRES) with Letter Z overlaid',
'1,Z0'),
('AVAIL', '1,\\1'),
('AVAIL', '1,\\2'),
('AVAIL', '1,\\3'),
('AVAIL', '1,\\4'),
('AVAIL', '1,\\5'),
('AVAIL', '1,\\6'),
('AVAIL', '1,\\7'),
('802.11 or other network node', '1,\\8'),
('Gas Station (blue pump)', '1,\\9'),
('overlayBOX DTMF & RFID & XO', '1,\\A'),
('overlayBOX DTMF & RFID & XO with Zero overlaid', '1,0A'),
('overlayBOX DTMF & RFID & XO with One overlaid', '1,1A'),
('overlayBOX DTMF & RFID & XO with Two overlaid', '1,2A'),
('overlayBOX DTMF & RFID & XO with Three overlaid', '1,3A'),
('overlayBOX DTMF & RFID & XO with Four overlaid', '1,4A'),
('overlayBOX DTMF & RFID & XO with Five overlaid', '1,5A'),
('overlayBOX DTMF & RFID & XO with Six overlaid', '1,6A'),
('HT DTMF user', '1,7A'),
('overlayBOX DTMF & RFID & XO with Eight overlaid', '1,8A'),
('Mobile DTMF user', '1,9A'),
('AllStar DTMF report', '1,AA'),
('overlayBOX DTMF & RFID & XO with Letter B overlaid',
'1,BA'),
('overlayBOX DTMF & RFID & XO with Letter C overlaid',
'1,CA'),
('D-Star report', '1,DA'),
('Echolink DTMF report', '1,EA'),
('overlayBOX DTMF & RFID & XO with Letter F overlaid',
'1,FA'),
('overlayBOX DTMF & RFID & XO with Letter G overlaid',
'1,GA'),
('House DTMF user', '1,HA'),
('IRLP DTMF report', '1,IA'),
('overlayBOX DTMF & RFID & XO with Letter J overlaid',
'1,JA'),
('overlayBOX DTMF & RFID & XO with Letter K overlaid',
'1,KA'),
('overlayBOX DTMF & RFID & XO with Letter L overlaid',
'1,LA'),
('overlayBOX DTMF & RFID & XO with Letter M overlaid',
'1,MA'),
('overlayBOX DTMF & RFID & XO with Letter N overlaid',
'1,NA'),
('overlayBOX DTMF & RFID & XO with Letter O overlaid',
'1,OA'),
('overlayBOX DTMF & RFID & XO with Letter P overlaid',
'1,PA'),
('overlayBOX DTMF & RFID & XO with Letter Q overlaid',
'1,QA'),
('RFID report', '1,RA'),
('overlayBOX DTMF & RFID & XO with Letter S overlaid',
'1,SA'),
('overlayBOX DTMF & RFID & XO with Letter T overlaid',
'1,TA'),
('overlayBOX DTMF & RFID & XO with Letter U overlaid',
'1,UA'),
('overlayBOX DTMF & RFID & XO with Letter V overlaid',
'1,VA'),
('overlayBOX DTMF & RFID & XO with Letter W overlaid',
'1,WA'),
('OLPC Laptop XO', '1,XA'),
('overlayBOX DTMF & RFID & XO with Letter Y overlaid',
'1,YA'),
('overlayBOX DTMF & RFID & XO with Letter Z overlaid',
'1,ZA'),
('ARRL,ARES,WinLINK,Dstar, etc', '1,\\a'),
('AVAIL (BlwngSnow ==> E ovly B', '1,\\B'),
('AVAIL (Blwng Dst/Snd => E ovly)', '1,\\b'),
('Coast Guard', '1,\\C'),
('CD triangle RACES/SATERN/etc', '1,\\c'),
('DX spot by callsign', '1,\\d'),
("DEPOTS (Drizzle ==> ' ovly D)", '1,\\D A'),
('Smoke (& other vis codes)', '1,\\E'),
('Sleet (& future ovrly codes)', '1,\\e'),
('AVAIL (FrzngRain ==> `F)', '1,\\F'),
('Funnel Cloud', '1,\\f'),
('AVAIL (Snow Shwr ==> I ovly S)', '1,\\G'),
('Gale Flags', '1,\\g'),
('\\Haze (& Overlay Hazards)', '1,\\H'),
('Store. or HAMFST Hh=HAM store', '1,\\h'),
('Rain Shower', '1,\\I'),
('BOX or points of Interest', '1,\\i'),
('AVAIL (Lightning ==> I ovly L)', '1,\\J'),
('WorkZone (Steam Shovel)', '1,\\j'),
('Kenwood HT (W)', '1,\\K'),
('Special Vehicle SUV,ATV,4x4', '1,\\k'),
('Lighthouse', '1,\\L'),
('Areas (box,circles,etc)', '1,\\l'),
('MARS (A=Army,N=Navy,F=AF)', '1,\\M'),
('Value Sign (3 digit display)', '1,\\m'),
('Navigation Buoy', '1,\\N'),
('OVERLAY TRIANGLE', '1,\\n'),
('OVERLAY TRIANGLE with Zero overlaid', '1,0n'),
('OVERLAY TRIANGLE with One overlaid', '1,1n'),
('OVERLAY TRIANGLE with Two overlaid', '1,2n'),
('OVERLAY TRIANGLE with Three overlaid', '1,3n'),
('OVERLAY TRIANGLE with Four overlaid', '1,4n'),
('OVERLAY TRIANGLE with Five overlaid', '1,5n'),
('OVERLAY TRIANGLE with Six overlaid', '1,6n'),
('OVERLAY TRIANGLE with Seven overlaid', '1,7n'),
('OVERLAY TRIANGLE with Eight overlaid', '1,8n'),
('OVERLAY TRIANGLE with Nine overlaid', '1,9n'),
('OVERLAY TRIANGLE with Letter A overlaid', '1,An'),
('OVERLAY TRIANGLE with Letter B overlaid', '1,Bn'),
('OVERLAY TRIANGLE with Letter C overlaid', '1,Cn'),
('OVERLAY TRIANGLE with Letter D overlaid', '1,Dn'),
('OVERLAY TRIANGLE with Letter E overlaid', '1,En'),
('OVERLAY TRIANGLE with Letter F overlaid', '1,Fn'),
('OVERLAY TRIANGLE with Letter G overlaid', '1,Gn'),
('OVERLAY TRIANGLE with Letter H overlaid', '1,Hn'),
('OVERLAY TRIANGLE with Letter I overlaid', '1,In'),
('OVERLAY TRIANGLE with Letter J overlaid', '1,Jn'),
('OVERLAY TRIANGLE with Letter K overlaid', '1,Kn'),
('OVERLAY TRIANGLE with Letter L overlaid', '1,Ln'),
('OVERLAY TRIANGLE with Letter M overlaid', '1,Mn'),
('OVERLAY TRIANGLE with Letter N overlaid', '1,Nn'),
('OVERLAY TRIANGLE with Letter O overlaid', '1,On'),
('OVERLAY TRIANGLE with Letter P overlaid', '1,Pn'),
('OVERLAY TRIANGLE with Letter Q overlaid', '1,Qn'),
('OVERLAY TRIANGLE with Letter R overlaid', '1,Rn'),
('OVERLAY TRIANGLE with Letter S overlaid', '1,Sn'),
('OVERLAY TRIANGLE with Letter T overlaid', '1,Tn'),
('OVERLAY TRIANGLE with Letter U overlaid', '1,Un'),
('OVERLAY TRIANGLE with Letter V overlaid', '1,Vn'),
('OVERLAY TRIANGLE with Letter W overlaid', '1,Wn'),
('OVERLAY TRIANGLE with Letter X overlaid', '1,Xn'),
('OVERLAY TRIANGLE with Letter Y overlaid', '1,Yn'),
('OVERLAY TRIANGLE with Letter Z overlaid', '1,Zn'),
('Overlay Balloon (Rocket = \\O)', '1,\\O'),
('small circle', '1,\\o'),
('Parking', '1,\\P'),
('AVAIL (PrtlyCldy => ( ovly P', '1,\\p'),
('QUAKE', '1,\\Q'),
('AVAIL', '1,\\q'),
('Restaurant', '1,\\R'),
('Restrooms', '1,\\r'),
('Satellite/Pacsat', '1,\\S'),
('OVERLAY SHIP/boats', '1,\\s'),
('OVERLAY SHIP/boats with Zero overlaid', '1,0s'),
('OVERLAY SHIP/boats with One overlaid', '1,1s'),
('OVERLAY SHIP/boats with Two overlaid', '1,2s'),
('OVERLAY SHIP/boats with Three overlaid', '1,3s'),
('OVERLAY SHIP/boats with Four overlaid', '1,4s'),
('OVERLAY SHIP/boats with Five overlaid', '1,5s'),
('Shipwreck ("deep6")', '1,6s'),
('OVERLAY SHIP/boats with Seven overlaid', '1,7s'),
('OVERLAY SHIP/boats with Eight overlaid', '1,8s'),
('OVERLAY SHIP/boats with Nine overlaid', '1,9s'),
('OVERLAY SHIP/boats with Letter A overlaid', '1,As'),
('Pleasure Boat', '1,Bs'),
('Cargo', '1,Cs'),
('Diving', '1,Ds'),
('Emergency or Medical transport', '1,Es'),
('Fishing', '1,Fs'),
('OVERLAY SHIP/boats with Letter G overlaid', '1,Gs'),
('High-speed Craft', '1,Hs'),
('OVERLAY SHIP/boats with Letter I overlaid', '1,Is'),
('Jet Ski', '1,Js'),
('OVERLAY SHIP/boats with Letter K overlaid', '1,Ks'),
('Law enforcement', '1,Ls'),
('Miltary', '1,Ms'),
('OVERLAY SHIP/boats with Letter N overlaid', '1,Ns'),
('Oil Rig', '1,Os'),
('Pilot Boat', '1,Ps'),
('Torpedo', '1,Qs'),
('OVERLAY SHIP/boats with Letter R overlaid', '1,Rs'),
('Search and Rescue', '1,Ss'),
('Tug', '1,Ts'),
('Underwater ops or submarine', '1,Us'),
('OVERLAY SHIP/boats with Letter V overlaid', '1,Vs'),
('Wing-in-Ground effect (or Hovercraft)', '1,Ws'),
('Passenger (paX)(ferry)', '1,Xs'),
('Sailing (large ship)', '1,Ys'),
('OVERLAY SHIP/boats with Letter Z overlaid', '1,Zs'),
('Thunderstorm', '1,\\T'),
('Tornado', '1,\\t'),
('SUNNY', '1,\\U'),
('OVERLAYED TRUCK', '1,\\u'),
('OVERLAYED TRUCK with Zero overlaid', '1,0u'),
('OVERLAYED TRUCK with One overlaid', '1,1u'),
('OVERLAYED TRUCK with Two overlaid', '1,2u'),
('OVERLAYED TRUCK with Three overlaid', '1,3u'),
('OVERLAYED TRUCK with Four overlaid', '1,4u'),
('OVERLAYED TRUCK with Five overlaid', '1,5u'),
('OVERLAYED TRUCK with Six overlaid', '1,6u'),
('OVERLAYED TRUCK with Seven overlaid', '1,7u'),
('OVERLAYED TRUCK with Eight overlaid', '1,8u'),
('OVERLAYED TRUCK with Nine overlaid', '1,9u'),
('OVERLAYED TRUCK with Letter A overlaid', '1,Au'),
('Buldozer/construction/Backhoe', '1,Bu'),
('Chlorine Tanker', '1,Cu'),
('OVERLAYED TRUCK with Letter D overlaid', '1,Du'),
('OVERLAYED TRUCK with Letter E overlaid', '1,Eu'),
('OVERLAYED TRUCK with Letter F overlaid', '1,Fu'),
('Gas', '1,Gu'),
('Hazardous', '1,Hu'),
('OVERLAYED TRUCK with Letter I overlaid', '1,Iu'),
('OVERLAYED TRUCK with Letter J overlaid', '1,Ju'),
('OVERLAYED TRUCK with Letter K overlaid', '1,Ku'),
('OVERLAYED TRUCK with Letter L overlaid', '1,Lu'),
('OVERLAYED TRUCK with Letter M overlaid', '1,Mu'),
('OVERLAYED TRUCK with Letter N overlaid', '1,Nu'),
('OVERLAYED TRUCK with Letter O overlaid', '1,Ou'),
('Plow or SnowPlow', '1,Pu'),
('OVERLAYED TRUCK with Letter Q overlaid', '1,Qu'),
('OVERLAYED TRUCK with Letter R overlaid', '1,Ru'),
('OVERLAYED TRUCK with Letter S overlaid', '1,Su'),
('Tanker', '1,Tu'),
('OVERLAYED TRUCK with Letter U overlaid', '1,Uu'),
('OVERLAYED TRUCK with Letter V overlaid', '1,Vu'),
('OVERLAYED TRUCK with Letter W overlaid', '1,Wu'),
('OVERLAYED TRUCK with Letter X overlaid', '1,Xu'),
('OVERLAYED TRUCK with Letter Y overlaid', '1,Yu'),
('OVERLAYED TRUCK with Letter Z overlaid', '1,Zu'),
('VORTAC Nav Aid', '1,\\V'),
('OVERLAYED Van', '1,\\v'),
('OVERLAYED Van with Zero overlaid', '1,0v'),
('OVERLAYED Van with One overlaid', '1,1v'),
('OVERLAYED Van with Two overlaid', '1,2v'),
('OVERLAYED Van with Three overlaid', '1,3v'),
('OVERLAYED Van with Four overlaid', '1,4v'),
('OVERLAYED Van with Five overlaid', '1,5v'),
('OVERLAYED Van with Six overlaid', '1,6v'),
('OVERLAYED Van with Seven overlaid', '1,7v'),
('OVERLAYED Van with Eight overlaid', '1,8v'),
('OVERLAYED Van with Nine overlaid', '1,9v'),
('OVERLAYED Van with Letter A overlaid', '1,Av'),
('OVERLAYED Van with Letter B overlaid', '1,Bv'),
('OVERLAYED Van with Letter C overlaid', '1,Cv'),
('OVERLAYED Van with Letter D overlaid', '1,Dv'),
('OVERLAYED Van with Letter E overlaid', '1,Ev'),
('OVERLAYED Van with Letter F overlaid', '1,Fv'),
('OVERLAYED Van with Letter G overlaid', '1,Gv'),
('OVERLAYED Van with Letter H overlaid', '1,Hv'),
('OVERLAYED Van with Letter I overlaid', '1,Iv'),
('OVERLAYED Van with Letter J overlaid', '1,Jv'),
('OVERLAYED Van with Letter K overlaid', '1,Kv'),
('OVERLAYED Van with Letter L overlaid', '1,Lv'),
('OVERLAYED Van with Letter M overlaid', '1,Mv'),
('OVERLAYED Van with Letter N overlaid', '1,Nv'),
('OVERLAYED Van with Letter O overlaid', '1,Ov'),
('OVERLAYED Van with Letter P overlaid', '1,Pv'),
('OVERLAYED Van with Letter Q overlaid', '1,Qv'),
('OVERLAYED Van with Letter R overlaid', '1,Rv'),
('OVERLAYED Van with Letter S overlaid', '1,Sv'),
('OVERLAYED Van with Letter T overlaid', '1,Tv'),
('OVERLAYED Van with Letter U overlaid', '1,Uv'),
('OVERLAYED Van with Letter V overlaid', '1,Vv'),
('OVERLAYED Van with Letter W overlaid', '1,Wv'),
('OVERLAYED Van with Letter X overlaid', '1,Xv'),
('OVERLAYED Van with Letter Y overlaid', '1,Yv'),
('OVERLAYED Van with Letter Z overlaid', '1,Zv'),
('# NWS site (NWS options)', '1,\\W'),
('# NWS site (NWS options) with Zero overlaid', '1,0W'),
('# NWS site (NWS options) with One overlaid', '1,1W'),
('# NWS site (NWS options) with Two overlaid', '1,2W'),
('# NWS site (NWS options) with Three overlaid', '1,3W'),
('# NWS site (NWS options) with Four overlaid', '1,4W'),
('# NWS site (NWS options) with Five overlaid', '1,5W'),
('# NWS site (NWS options) with Six overlaid', '1,6W'),
('# NWS site (NWS options) with Seven overlaid', '1,7W'),
('# NWS site (NWS options) with Eight overlaid', '1,8W'),
('# NWS site (NWS options) with Nine overlaid', '1,9W'),
('# NWS site (NWS options) with Letter A overlaid', '1,AW'),
('# NWS site (NWS options) with Letter B overlaid', '1,BW'),
('# NWS site (NWS options) with Letter C overlaid', '1,CW'),
('# NWS site (NWS options) with Letter D overlaid', '1,DW'),
('# NWS site (NWS options) with Letter E overlaid', '1,EW'),
('# NWS site (NWS options) with Letter F overlaid', '1,FW'),
('# NWS site (NWS options) with Letter G overlaid', '1,GW'),
('# NWS site (NWS options) with Letter H overlaid', '1,HW'),
('# NWS site (NWS options) with Letter I overlaid', '1,IW'),
('# NWS site (NWS options) with Letter J overlaid', '1,JW'),
('# NWS site (NWS options) with Letter K overlaid', '1,KW'),
('# NWS site (NWS options) with Letter L overlaid', '1,LW'),
('# NWS site (NWS options) with Letter M overlaid', '1,MW'),
('# NWS site (NWS options) with Letter N overlaid', '1,NW'),
('# NWS site (NWS options) with Letter O overlaid', '1,OW'),
('# NWS site (NWS options) with Letter P overlaid', '1,PW'),
('# NWS site (NWS options) with Letter Q overlaid', '1,QW'),
('# NWS site (NWS options) with Letter R overlaid', '1,RW'),
('# NWS site (NWS options) with Letter S overlaid', '1,SW'),
('# NWS site (NWS options) with Letter T overlaid', '1,TW'),
('# NWS site (NWS options) with Letter U overlaid', '1,UW'),
('# NWS site (NWS options) with Letter V overlaid', '1,VW'),
('# NWS site (NWS options) with Letter W overlaid', '1,WW'),
('# NWS site (NWS options) with Letter X overlaid', '1,XW'),
('# NWS site (NWS options) with Letter Y overlaid', '1,YW'),
('# NWS site (NWS options) with Letter Z overlaid', '1,ZW'),
('Flooding (Avalanches/Slides)', '1,\\w'),
('Pharmacy Rx (Apothecary)', '1,\\X'),
('Wreck or Obstruction ->X<-', '1,\\x'),
('Radios and devices', '1,\\Y'),
('Skywarn', '1,\\y'),
('AVAIL', '1,\\Z'),
('OVERLAYED Shelter', '1,\\z'))
_SETTINGS = {
"DS": {'type': 'list',
'values': ("Data Band", "Both Bands", "Ignore DCD")},
"DSPA": {'type': 'list',
'values': ("Entire Display", "One Line")},
"ICO": {'type': 'map',
'map': (
KenwoodD7Family.
_COMMON_SETTINGS["ICO"]["map"] + # type: ignore
_APRS_EXTRA)}}
_SETTINGS_MENUS = (
('Main',
(("ASC 0", "Automatic Simplex Check Band A"),
("ASC 1", "Automatic Simplex Check Band B"),
("BAL", "Balance"),
("BC", "Band"),
("BEL 0", "Tone Alert Band A"),
("BEL 1", "Tone Alert Band B"),
("CH", "Channel Mode Display"),
("DL", "Dual"),
("LK", "Lock"),
("LMP", "Lamp"),
# MNF requires that the *current* VFO be in MR mode
# Also, it has a direct key
# ("MNF", "Memory Display Mode"),
("PV", "Programmable VFOs"),
("SQ 0", "Band A Squelch"),
("SQ 1", "Band B Squelch"),
# We likely don't want to enable this from Chirp...
# ("TNC", "Packet Mode"))),
)),
('Radio',
(('Display',
(("MES", "Power-ON Message"),
("CNT", "Contrast"))),
('Save',
(("SV", "Battery Saver Interval"),
("APO", "Automatic Power Off (APO)"))),
('DTMF',
(("dtmfmem 00", "Number Store #0"),
("dtmfmem 01", "Number Store #1"),
("dtmfmem 02", "Number Store #2"),
("dtmfmem 03", "Number Store #3"),
("dtmfmem 04", "Number Store #4"),
("dtmfmem 05", "Number Store #5"),
("dtmfmem 06", "Number Store #6"),
("dtmfmem 07", "Number Store #7"),
("dtmfmem 08", "Number Store #8"),
("dtmfmem 09", "Number Store #9"),
("TSP", "TX Speed"),
("TXH", "TX Hold"),
("PT", "Pause"))),
('TNC',
(("DTB", "Data band select"),
("DS", "DCD sense"))),
('AUX',
(("ARO", "Automatic Repeater Offset"),
("SCR", "Scan Resume"),
("BEP", "Key Beep"),
("ELK", "Tuning Enable"),
("TXS", "TX Inhibit"),
("AIP", "Advanced Intercept Point"),
("CKEY", "Call Key function"),
("TH", "TX Hold, 1750 Hz"))))),
('APRS',
(("MYC", "My call sign"),
("GU", "GPS receiver"),
("WAY", "Waypoint"),
("MP 1", "My position #1"),
("MP 2", "My position #2"),
("MP 3", "My position #3"),
("PAMB", "Position Ambiguity"),
("POSC", "Position comment"),
("ARL", "Reception restriction distance"),
("ICO", "Station icon"),
("STAT 1", "Status text #1"),
("STAT 2", "Status text #2"),
("STAT 3", "Status text #3"),
("STXR", "Status text transmit rate"),
("PP", "Packet path"),
("DTX", "Packet transmit method"),
("TXI", "Packet transmit interval"),
("UPR", "Group code"),
("BEPT", "Beep"),
("DSPA", "Display area"),
("KILO", "Unit for distance"),
("TEMP", "Unit for temperature"),
("AMR", "Auto Answer Reply"),
("ARLM", "Reply message"),
("AMGG", "Message group"),
("DTBA", "Data band"),
("PKSA", "Packet transfer rate"),
("TZ", "Time Zone"),
("TXD", "Packet transmit delay"))),
('SSTV',
(("SMY", "My call sign"),
("MAC", "Color for call sign"),
("SMSG", "Message"),
("SMC", "Color for message"),
("RSV", "RSV report"),
("RSC", "Color for RSV report"),
("VCS", "VC-H1 Control"))),
('SkyCommand',
(("SCC", "Commander call sign"),
("SCT", "Transporter call sign"),
("SKTN", "Tone frequency select"))))
@directory.register
class TMD700Radio(KenwoodD7Family):
"""Kenwood TH-D700"""
MODEL = "TM-D700"
HARDWARE_FLOW = True
# Details taken from https://www.qsl.net/k7jar/pages/D700Cmds.html
# And https://www.qsl.net/k/kb9rku/aprs/ (continued on next line)
# PC%20Command%20Specifications%20for%20TM-D700_Rev1.pdf
_BAUDS = [57600, 38400, 19200, 9600]
_LOWER = 1
_UPPER = 200
_BANDS = [(118000000, 524000000), (800000000, 1300000000)]
_BOOL = {'type': 'bool'}
_PKEY_FUNCTION = {'type': 'list',
'values': ('Power', 'A/B', 'Monitor', 'Enter', 'Voice',
'1750', 'PM', 'Menu', 'VFO', 'MR', 'CALL',
'MHz', 'Tone', 'Rev', 'Low', 'Mute', 'Ctrl',
'PM.In', 'A.B.C', 'M>V', 'M.In', 'C.In',
'Lock', 'T.Sel', 'Shift', 'Step', 'Visual',
'Dim', 'Sub-Band Select', 'DX', 'TNC', 'List',
'P.Mon', 'BCon', 'Msg', 'Pos')}
_VOLUME = {'type': 'list',
'values': ('Off', '1', '2', '3', '4', '5', '6', 'Maximum')}
_SETTINGS = {
"ABLG": {'type': 'string', 'max_len': 29},
"AD": _BOOL,
"BEP": {'type': 'list',
'values': ("Off", "Key")},
"BVOL": _VOLUME,
"CP": {'type': 'list',
'values': ('9600bps', '19200bps', '38400bps', '57600bps')},
"DATP": {'type': 'list',
'values': ('1200bps', '9600bps')},
"DIG": _BOOL,
"DTM": _BOOL,
"FUNC": {'type': 'map',
'map': (("Mode 1", "1"), ("Mode 2", "2"), ("Mode 3", "3"))},
"ICO": {'type': 'map',
'map': (KenwoodD7Family.
_COMMON_SETTINGS["ICO"]["map"] + # type: ignore
(('Digipeater', '0,F'),) +
THD7GRadio._APRS_EXTRA)},
"MCL 0": _BOOL,
"MCL 1": _BOOL,
"MCNT": _BOOL,
"MD": {'type': 'list', 'values': ('FM', 'AM')},
"NP": _BOOL,
"OS": {'type': 'integer', 'min': -99999999, 'max': 999999999,
'digits': 9},
"PF 1": _PKEY_FUNCTION,
"PF 2": _PKEY_FUNCTION,
"PF 3": _PKEY_FUNCTION,
"PF 4": _PKEY_FUNCTION,
"PMM": _BOOL,
"RCA": _BOOL,
"RC": _BOOL,
"RCC": {'type': 'integer', 'min': 0, 'max': 999},
"REP": {'type': 'list',
'values': ('Off', 'Locked-Band', 'Cross-Band')},
"REPL": _BOOL,
"SHT": {'type': 'list',
'values': ('Off', '125ms', '250ms', '500ms')},
"SSEL": {'type': 'map',
'map': (("MODE1", "1"), ("MODE2", "2"))},
"SSQ 0": _BOOL,
"SSQ 1": _BOOL,
"TOT": {'type': 'list',
'values': ('3 minutes', '5 minutes', '10 minutes')},
"UDIG": {'type': 'string', 'max_len': 39, 'charset': '0123456789'
'ABCDEFGHIJKLMNOPQRSTUVWXYZ,-'},
"VOM": {'type': 'list',
'values': ('Off', 'English', 'APRS only', 'Japanese')},
"VS3": _VOLUME,
"VSM": {'type': 'map',
'map': (('31 Channels', '1'), ('61 Channels', '2'),
('91 Channels', '3'), ('181 Channels', '4'))}}
_PROGRAMMABLE_VFOS = (
("Band A 118 MHz Sub-Band", 1, 118, 135),
("Band A VHF Sub-Band", 2, 136, 199),
("Band B VHF Sub-Band", 3, 136, 174),
("Band A 220 MHz Sub-Band", 4, 200, 299),
("Band B 300 MHz Sub-Band", 5, 300, 399),
("Band A 300 MHz Sub-Band", 6, 300, 399),
("Band B UHF Sub-Band", 7, 400, 523),
("Band A UHF Sub-Band", 8, 400, 469),
("Band B 1.2 GHz Sub-Band", 9, 800, 1299))
_SETTINGS_MENUS = (
('Main',
(("ASC 0", "Automatic Simplex Check Band A"),
("ASC 1", "Automatic Simplex Check Band B"),
("DL", "Dual"),
("LK", "Lock"),
# MNF requires that the *current* VFO be in MR mode
# ("MNF", "Memory Display Mode"),
# We likely don't want to enable this from Chirp...
# ("TNC", "Packet Mode"))),
)),
('Radio',
(('Display',
(("MES", "Power-ON Message"),
("CNT", "Contrast"),
("NP", "Reverse mode"),
("AD", "Auto Dimmer Change"),
("FUNC", "Multi-function button"))),
('Audio',
(("BVOL", "Beep volume"),
("BEP", "Key Beep"),
("SSEL", "Speaker configuration"),
("VOM", "Voice Synthesizer"),
("VS3", "Voice volume"))),
('TX/RX',
(("PV", "Programmable VFO"),
("SSQ 0", "Band A S-meter Squelch"),
("SSQ 1", "Band B S-meter Squelch"),
("SHT", "Squelch hang time"),
("MD", "FM / AM mode"),
("AIP", "Advanced Intercept Point"),
("XXX", "TX/ RX deviation"))),
('Memory',
(("PMM", "Auto PM Channel Store"),
("CH", "Channel Display"),
("MCL 0", "Band A Memory Channel Lockout"),
("MCL 1", "Band B Memory Channel Lockout"),
("XXX", "Memory channel name"))),
('DTMF',
(("dtmfmem 00", "Number Store #0"),
("dtmfmem 01", "Number Store #1"),
("dtmfmem 02", "Number Store #2"),
("dtmfmem 03", "Number Store #3"),
("dtmfmem 04", "Number Store #4"),
("dtmfmem 05", "Number Store #5"),
("dtmfmem 06", "Number Store #6"),
("dtmfmem 07", "Number Store #7"),
("dtmfmem 08", "Number Store #8"),
("dtmfmem 09", "Number Store #9"),
("TSP", "TX Speed"),
("PT", "Pause"))),
('TNC',
(("DTB", "Data band"),
("DS", "DCD sense"),
("XXX", "Time"),
("XXX", "Date"),
("TZ", "Time zone"))),
('Repeater',
(("OS", "Offset frequency"),
("ARO", "Automatic Repeater Offset"),
("CKEY", "Call Button Function"),
("TH", "TX Hold"),
("REPH", "Repeater Hold"),
("REP", "Repeater function"))),
('Mic',
(("PF 1", "Mic PF Key"),
("PF 2", "Mic MR Key"),
("PF 3", "Mic VFO Key"),
("PF 4", "Mic CALL Key"),
("MCNT", "Microphone Control"),
("DTM", "DTMF Monitor"))),
('Aux',
(("SCR", "Scan Resume"),
("VSM", "Number of Channels for Visual Scan"),
("APO", "Automatic Power Off (APO)"),
("TOT", "Time-Out Timer (TOT)"),
("CP", "COM port"),
("DATP", "Data port"))),
('Remote Control',
(("RCC", "Secret code"),
("RCA", "Acknowledgement"),
("RC", "Remote Control"))))),
('SSTV',
(("SMY", "My call sign"),
("MAC", "Color for call sign"),
("SMSG", "Message"),
("SMC", "Color for message"),
("RSV", "RSV report"),
("RSC", "Color for RSV report"),
("VCS", "VC-H1 Control"))),
('APRS',
(("MYC", "My call sign"),
("GU", "GPS receiver"),
("WAY", "Waypoint"),
("MP 1", "My position #1"),
("MP 2", "My position #2"),
("MP 3", "My position #3"),
("PAMB", "Position Ambiguity"),
("POSC", "Position comment"),
("ARL", "Reception restriction distance"),
("ICO", "Station icon"),
("STAT 1", "Status text #1"),
("STAT 2", "Status text #2"),
("STAT 3", "Status text #3"),
("STXR", "Status text transmit rate"),
("PP", "Packet path"),
("DTX", "Packet transmit method"),
("TXI", "Packet transmit interval"),
("UPR", "Group code"),
("BEPT", "Beep"),
("KILO", "Unit for distance"),
("TEMP", "Unit for temperature"),
("DTBA", "Data band"),
("PKSA", "Packet transfer rate"),
("DIG", "Digipeater"),
("UDIG", "Digipeating path"),
("AMR", "Auto Answer Reply"),
("ARLM", "Reply message"),
("ABLG", "Bulletin group"))),
('SkyCommand',
(("SCC", "Commander call sign"),
("SCT", "Transporter call sign"),
("SKTN", "Tone frequency select"))))
# Apparently, when sent "CH 1", the radio will return "SM 0,00"
# which is the current value of the Band A S-Meter. We'll ignore that
# and read the next line (which is hopefully "CH 1")
def _keep_reading(self, result):
if result[0:3] == 'SM ':
return True
return False
# It also seems that "CH 1" can return "N" when the current memory
# is invalid (but still enters CH mode)
def _kenwood_set_success(self, cmd, modcmd, value, response):
if cmd == 'CH' and value == "1":
if response == 'N':
return True
return super()._kenwood_set_success(cmd, modcmd, value, response)
def _mem_spec_fixup(self, spec, memid, mem):
spec[6] = "%i" % (mem.tmode == "DTCS")
spec[8] = "%03i0" % (chirp_common.DTCS_CODES.index(mem.dtcs) + 1)
def _parse_mem_fixup(self, mem, spec):
if spec[11] and spec[11].isdigit():
mem.dtcs = chirp_common.DTCS_CODES[int(spec[11][:-1]) - 1]
else:
LOG.warn("Unknown or invalid DCS: %s" % spec[11])
def get_features(self, *args, **kwargs):
rf = super().get_features(*args, **kwargs)
rf.has_dtcs = True
rf.valid_tmodes = ["", "Tone", "TSQL", "DTCS"]
return rf
chirp-20261030/chirp/drivers/kenwood_hmk.py 0000644 0001750 0001750 00000010365 14665027664 017521 0 ustar hibby hibby # Copyright 2008 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 csv
import logging
from chirp import chirp_common, errors, directory
from chirp.drivers import generic_csv
LOG = logging.getLogger(__name__)
class OmittedHeaderError(Exception):
"""An internal exception to indicate that a header was omitted"""
pass
@directory.register
class HMKRadio(generic_csv.CSVRadio):
"""Kenwood HMK format"""
VENDOR = "Kenwood"
MODEL = "HMK"
FILE_EXTENSION = "hmk"
DUPLEX_MAP = {
" ": "",
"S": "split",
"+": "+",
"-": "-",
}
SKIP_MAP = {
"Off": "",
"On": "S",
}
TMODE_MAP = {
"Off": "",
"T": "Tone",
"CT": "TSQL",
"DCS": "DTCS",
"": "Cross",
}
ATTR_MAP = {
"!!Ch": (int, "number"),
"M.Name": (str, "name"),
"Rx Freq.": (chirp_common.parse_freq, "freq"),
"Shift/Split": (lambda v: HMKRadio.DUPLEX_MAP[v], "duplex"),
"Offset": (chirp_common.parse_freq, "offset"),
"T/CT/DCS": (lambda v: HMKRadio.TMODE_MAP[v], "tmode"),
"TO Freq.": (float, "rtone"),
"CT Freq.": (float, "ctone"),
"DCS Code": (int, "dtcs"),
"Mode": (str, "mode"),
"Rx Step": (float, "tuning_step"),
"L.Out": (lambda v: HMKRadio.SKIP_MAP[v], "skip"),
}
def load(self, filename=None):
if filename is None and self._filename is None:
raise errors.RadioError("Need a location to load from")
if filename:
self._filename = filename
self._blank()
f = open(self._filename, "r")
for line in f:
if line.strip() == "// Memory Channels":
break
reader = csv.reader(f, delimiter=chirp_common.SEPCHAR, quotechar='"')
good = 0
lineno = 0
for line in reader:
lineno += 1
if lineno == 1:
header = line
continue
if len(header) > len(line):
LOG.debug("Line %i has %i columns, expected %i" %
(lineno, len(line), len(header)))
self.errors.append("Column number mismatch on line %i" %
lineno)
continue
# hmk stores Tx Freq. in its own field, but Chirp expects the Tx
# Freq. for odd-split channels to be in the Offset field.
# If channel is odd-split, copy Tx Freq. field to Offset field.
if line[header.index('Shift/Split')] == "S":
line[header.index('Offset')] = line[header.index('Tx Freq.')]
# fix EU decimal
line = [i.replace(',', '.') for i in line]
try:
mem = self._parse_csv_data_line(header, line)
if mem.number is None:
raise Exception("Invalid Location field" % lineno)
except Exception as e:
LOG.error("Line %i: %s" % (lineno, e))
self.errors.append("Line %i: %s" % (lineno, e))
continue
self._grow(mem.number)
self.memories[mem.number] = mem
good += 1
if not good:
for error in errors:
LOG.error("kenwood_hmk: %s", error)
raise errors.InvalidDataError("No channels found")
@classmethod
def match_model(cls, filedata, filename):
"""Match files ending in .hmk"""
return filename.lower().endswith("." + cls.FILE_EXTENSION)
chirp-20261030/chirp/drivers/kenwood_itm.py 0000644 0001750 0001750 00000010177 15242013013 017503 0 ustar hibby hibby # Copyright 2008 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 csv
import logging
from chirp import chirp_common, errors, directory
from chirp import kenwood_tone
from chirp.drivers import generic_csv
LOG = logging.getLogger(__name__)
class OmittedHeaderError(Exception):
"""An internal exception to indicate that a header was omitted"""
pass
@directory.register
class ITMRadio(generic_csv.CSVRadio):
"""Kenwood ITM format"""
VENDOR = "Kenwood"
MODEL = "ITM"
FILE_EXTENSION = "itm"
ATTR_MAP = {
"CH": (int, "number"),
"RXF": (chirp_common.parse_freq, "freq"),
"NAME": (str, "name"),
}
def _clean_duplex(self, headers, line, mem):
try:
txfreq = chirp_common.parse_freq(
generic_csv.get_datum_by_header(headers, line, "TXF"))
except ValueError:
mem.duplex = "off"
return mem
if mem.freq == txfreq:
mem.duplex = ""
elif txfreq:
mem.duplex = "split"
mem.offset = txfreq
return mem
def _clean_number(self, headers, line, mem):
zone = int(generic_csv.get_datum_by_header(headers, line, "ZN"))
mem.number = zone * 100 + mem.number
return mem
def _clean_tmode(self, headers, line, mem):
txtone = kenwood_tone.parse_qtdqt(
generic_csv.get_datum_by_header(headers, line, "TXSIG"))
rxtone = kenwood_tone.parse_qtdqt(
generic_csv.get_datum_by_header(headers, line, "RXSIG"))
chirp_common.split_tone_decode(mem, txtone, rxtone)
return mem
def load(self, filename=None):
if filename is None and self._filename is None:
raise errors.RadioError("Need a location to load from")
if filename:
self._filename = filename
self._blank()
f = open(self._filename, "r")
for line in f:
if line.strip() == "// Conventional Data":
break
reader = csv.reader(f, delimiter=chirp_common.SEPCHAR, quotechar='"')
good = 0
lineno = 0
for line in reader:
lineno += 1
if lineno == 1:
header = line
continue
if len(line) == 0:
# End of channel data
break
if len(header) > len(line):
LOG.error("Line %i has %i columns, expected %i" %
(lineno, len(line), len(header)))
self.errors.append("Column number mismatch on line %i" %
lineno)
continue
# fix EU decimal
line = [i.replace(',', '.') for i in line]
try:
mem = self._parse_csv_data_line(header, line)
if mem.number is None:
raise Exception("Invalid Location field" % lineno)
except Exception as e:
LOG.error("Line %i: %s" % (lineno, e))
self.errors.append("Line %i: %s" % (lineno, e))
continue
self._grow(mem.number)
self.memories[mem.number] = mem
good += 1
if not good:
for error in errors:
LOG.error("kenwood_itm: %s", error)
raise errors.InvalidDataError("No channels found")
@classmethod
def match_model(cls, filedata, filename):
return filename.lower().endswith("." + cls.FILE_EXTENSION)
chirp-20261030/chirp/drivers/kenwood_live.py 0000644 0001750 0001750 00000212453 15255434330 017667 0 ustar hibby hibby # 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 threading
import os
import sys
import time
import logging
from chirp import chirp_common, errors, directory, util
from chirp.settings import RadioSetting, RadioSettingGroup, \
RadioSettingValueInteger, RadioSettingValueBoolean, \
RadioSettingValueString, RadioSettingValueList, RadioSettings
LOG = logging.getLogger(__name__)
NOCACHE = "CHIRP_NOCACHE" in os.environ
DUPLEX = {0: "", 1: "+", 2: "-"}
MODES = {0: "FM", 1: "AM"}
STEPS = list(chirp_common.TUNING_STEPS)
STEPS.append(100.0)
KENWOOD_TONES = list(chirp_common.TONES)
KENWOOD_TONES.remove(159.8)
KENWOOD_TONES.remove(165.5)
KENWOOD_TONES.remove(171.3)
KENWOOD_TONES.remove(177.3)
KENWOOD_TONES.remove(183.5)
KENWOOD_TONES.remove(189.9)
KENWOOD_TONES.remove(196.6)
KENWOOD_TONES.remove(199.5)
THF6_MODES = ["FM", "WFM", "AM", "LSB", "USB", "CW"]
RADIO_IDS = {
"ID007": "TS-790E",
"ID019": "TS-2000",
"ID009": "TS-850",
"ID020": "TS-480_LiveMode",
"ID021": "TS-590S/SG_LiveMode", # S-model uses same class
"ID023": "TS-590S/SG_LiveMode" # as SG
}
def get_tmode(tone, ctcss, dcs):
"""Get the tone mode based on the values of the tone, ctcss, dcs"""
if dcs and int(dcs) == 1:
return "DTCS"
elif int(ctcss):
return "TSQL"
elif int(tone):
return "Tone"
else:
return ""
def iserr(result):
"""Returns True if the @result from a radio is an error"""
return result in ["N", "?"]
class KenwoodLiveRadio(chirp_common.LiveRadio):
"""Base class for all live-mode Kenwood radios"""
BAUD_RATE = 9600
VENDOR = "Kenwood"
MODEL = ""
# Lots of Kenwood radios actually require RTS, even some of the ones with
# USB integrated
HARDWARE_FLOW = True
_vfo = 0
_upper = 200
_kenwood_split = False
_kenwood_valid_tones = list(chirp_common.TONES)
_has_name = True
def __init__(self, *args, **kwargs):
chirp_common.LiveRadio.__init__(self, *args, **kwargs)
self._memcache = {}
self.LOCK = threading.Lock()
self.LAST_BAUD = 4800
self.LAST_DELIMITER = ("\r", " ")
if self.pipe:
self.pipe.timeout = 0.1
radio_id = self.get_id(self.pipe)
if radio_id != self.MODEL.split(" ")[0]:
raise Exception("Radio reports %s (not %s)" % (radio_id,
self.MODEL))
self.command(self.pipe, "AI", "0")
# The Kenwood TS-2000, TS-480, TS-590 & TS-850 use ";"
# as a CAT command message delimiter, and all others use "\n".
# Also, TS-2000 and TS-590 don't space delimite the command
# fields, but others do.
def get_id(self, ser):
"""Get the ID of the radio attached to @ser"""
bauds = [4800, 9600, 19200, 38400, 57600, 115200]
bauds.remove(self.LAST_BAUD)
# Make sure LAST_BAUD is last so that it is tried first below
bauds.append(self.LAST_BAUD)
saw_response = False
command_delimiters = [("\r", " "), (";", "")]
for delimiter in command_delimiters:
# Process the baud options in reverse order so that we try the
# last one first, and then start with the high-speed ones next
for i in reversed(bauds):
self.LAST_DELIMITER = delimiter
LOG.info("Trying ID at baud %i with delimiter \"%s\"" %
(i, repr(delimiter)))
ser.baudrate = i
ser.write(self.LAST_DELIMITER[0].encode())
ser.read(25)
try:
resp = self.command(ser, "ID")
except UnicodeDecodeError:
# If we got binary here, we are using the wrong rate
# or not talking to a Kenwood live radio.
continue
if resp:
saw_response = True
# most Kenwood radios
if " " in resp:
self.LAST_BAUD = i
return resp.split(" ")[1]
# Radio responded in the right baud rate,
# but threw an error because of all the crap
# we have been hurling at it. Retry the ID at this
# baud rate, which will almost definitely work.
if "?" in resp:
resp = self.command(ser, "ID")
self.LAST_BAUD = i
if " " in resp:
return resp.split(" ")[1]
# Kenwood radios that return ID numbers
if resp in list(RADIO_IDS.keys()):
return RADIO_IDS[resp]
if saw_response:
raise errors.RadioError("Unexpected response from radio")
raise errors.RadioNoResponse()
def _command(self, ser, cmd, *args):
"""Send @cmd to radio via @ser"""
start = time.time()
if args:
cmd += self.LAST_DELIMITER[1] + self.LAST_DELIMITER[1].join(args)
cmd += self.LAST_DELIMITER[0]
LOG.debug("PC->RADIO: %s" % cmd.strip())
ser.write(cmd.encode('cp1252'))
result = ""
while not result.endswith(self.LAST_DELIMITER[0]):
result += ser.read(8).decode('cp1252')
if (time.time() - start) > 1:
# TXH sometimes takes longer on TH-D7G
LOG.error("Timeout waiting for data")
break
if result.endswith(self.LAST_DELIMITER[0]):
LOG.debug("RADIO->PC: %r" % result.strip())
result = result[:-1]
else:
LOG.error("Giving up")
return result.strip()
def command(self, ser, cmd, *args):
with self.LOCK:
return self._command(ser, cmd, *args)
def _cmd_get_memory(self, number):
return "MR", "%i,0,%03i" % (self._vfo, number)
def _cmd_get_memory_name(self, number):
return "MNA", "%i,%03i" % (self._vfo, number)
def _cmd_get_split(self, number):
return "MR", "%i,1,%03i" % (self._vfo, number)
def _cmd_set_memory(self, number, spec):
if spec:
spec = "," + spec
return "MW", "%i,0,%03i%s" % (self._vfo, number, spec)
def _cmd_set_memory_name(self, number, name):
return "MNA", "%i,%03i,%s" % (self._vfo, number, name)
def _cmd_set_split(self, number, spec):
return "MW", "%i,1,%03i,%s" % (self._vfo, number, spec)
def get_raw_memory(self, number):
return self.command(self.pipe, *self._cmd_get_memory(number))
def get_memory(self, number):
if number < 0 or number > self._upper:
raise errors.InvalidMemoryLocation(
"Number must be between 0 and %i" % self._upper)
if number in self._memcache and not NOCACHE:
return self._memcache[number]
result = self.command(self.pipe, *self._cmd_get_memory(number))
if result == "N" or result == "E":
mem = chirp_common.Memory()
mem.number = number
mem.empty = True
self._memcache[mem.number] = mem
return mem
elif " " not in result:
LOG.error("Not sure what to do with this: `%s'" % result)
raise errors.RadioError("Unexpected result returned from radio")
value = result.split(" ")[1]
spec = value.split(",")
mem = self._parse_mem_spec(spec)
self._memcache[mem.number] = mem
if self._has_name:
result = self.command(self.pipe,
*self._cmd_get_memory_name(number))
if " " in result:
value = result.split(" ", 1)[1]
if value.count(",") == 2:
_zero, _loc, mem.name = value.split(",")
else:
_loc, mem.name = value.split(",")
if mem.duplex == "" and self._kenwood_split:
result = self.command(self.pipe, *self._cmd_get_split(number))
if " " in result:
value = result.split(" ", 1)[1]
self._parse_split_spec(mem, value.split(","))
return mem
def _make_mem_spec(self, mem):
pass
def _parse_mem_spec(self, spec):
pass
def _parse_split_spec(self, mem, spec):
mem.duplex = "split"
mem.offset = int(spec[2])
def _make_split_spec(self, mem):
return ("%011i" % mem.offset, "0")
def set_memory(self, memory):
if memory.number < 0 or memory.number > self._upper:
raise errors.InvalidMemoryLocation(
"Number must be between 0 and %i" % self._upper)
spec = self._make_mem_spec(memory)
spec = ",".join(spec)
del self._memcache[memory.number]
r1 = self.command(self.pipe,
*self._cmd_set_memory(memory.number, spec))
if not iserr(r1) and self._has_name:
time.sleep(0.5)
r2 = self.command(
self.pipe,
*self._cmd_set_memory_name(memory.number, memory.name))
if iserr(r2):
raise errors.InvalidDataError("Radio refused name %i: %s" %
(memory.number,
repr(memory.name)))
elif self._has_name:
raise errors.InvalidDataError("Radio refused %i" % memory.number)
if memory.duplex == "split" and self._kenwood_split:
spec = ",".join(self._make_split_spec(memory))
result = self.command(self.pipe,
*self._cmd_set_split(memory.number, spec))
if iserr(result):
raise errors.InvalidDataError("Radio refused %i" %
memory.number)
def erase_memory(self, number):
if number not in self._memcache:
return
resp = self.command(self.pipe, *self._cmd_set_memory(number, ""))
if iserr(resp):
raise errors.RadioError("Radio refused delete of %i" % number)
del self._memcache[number]
def _kenwood_get(self, cmd):
resp = self.command(self.pipe, cmd)
if " " in resp:
return resp.split(" ", 1)
else:
if resp == cmd:
return [resp, ""]
else:
raise errors.RadioError("Radio refused to return %s" % cmd)
def _kenwood_set(self, cmd, value):
resp = self.command(self.pipe, cmd, value)
if resp[:len(cmd)] == cmd:
return
raise errors.RadioError("Radio refused to set %s" % cmd)
def _kenwood_get_bool(self, cmd):
_cmd, result = self._kenwood_get(cmd)
return result == "1"
def _kenwood_set_bool(self, cmd, value):
return self._kenwood_set(cmd, str(int(value)))
def _kenwood_get_int(self, cmd):
_cmd, result = self._kenwood_get(cmd)
return int(result)
def _kenwood_set_int(self, cmd, value, digits=1):
return self._kenwood_set(cmd, ("%%0%ii" % digits) % value)
def set_settings(self, settings):
for element in settings:
if not isinstance(element, RadioSetting):
self.set_settings(element)
continue
if not element.changed():
continue
if isinstance(element.value, RadioSettingValueBoolean):
self._kenwood_set_bool(element.get_name(), element.value)
elif isinstance(element.value, RadioSettingValueList):
options = self._get_setting_options(element.get_name())
if len(options) > 9:
digits = 2
else:
digits = 1
self._kenwood_set_int(element.get_name(),
options.index(str(element.value)),
digits)
elif isinstance(element.value, RadioSettingValueInteger):
if element.value.get_max() > 9:
digits = 2
else:
digits = 1
self._kenwood_set_int(element.get_name(),
element.value, digits)
elif isinstance(element.value, RadioSettingValueString):
self._kenwood_set(element.get_name(), str(element.value))
else:
LOG.error("Unknown type %s" % element.value)
class KenwoodOldLiveRadio(KenwoodLiveRadio):
_kenwood_valid_tones = list(chirp_common.OLD_TONES)
def set_memory(self, memory):
supported_tones = list(chirp_common.OLD_TONES)
supported_tones.remove(69.3)
if memory.rtone not in supported_tones:
raise errors.UnsupportedToneError("This radio does not support " +
"tone %.1fHz" % memory.rtone)
if memory.ctone not in supported_tones:
raise errors.UnsupportedToneError("This radio does not support " +
"tone %.1fHz" % memory.ctone)
return KenwoodLiveRadio.set_memory(self, memory)
@directory.register
class TMV7Radio(KenwoodOldLiveRadio):
"""Kenwood TM-V7"""
MODEL = "TM-V7"
HARDWARE_FLOW = False
mem_upper_limit = 200 # Will be updated
def get_features(self):
rf = chirp_common.RadioFeatures()
rf.has_dtcs = False
rf.has_dtcs_polarity = False
rf.has_bank = False
rf.has_mode = False
rf.has_tuning_step = False
rf.valid_modes = ["FM"]
rf.valid_tmodes = ["", "Tone", "TSQL"]
rf.valid_characters = chirp_common.CHARSET_ALPHANUMERIC
rf.valid_name_length = 7
rf.valid_tuning_steps = STEPS
rf.has_sub_devices = True
rf.memory_bounds = (1, self._upper)
rf.valid_bands = [(144000000, 148000000),
(420000000, 450000000)]
return rf
def _make_mem_spec(self, mem):
spec = (
"%011i" % mem.freq,
"%X" % STEPS.index(mem.tuning_step),
"%i" % util.get_dict_rev(DUPLEX, mem.duplex),
"0",
"%i" % (mem.tmode == "Tone"),
"%i" % (mem.tmode == "TSQL"),
"0",
"%02i" % (self._kenwood_valid_tones.index(mem.rtone) + 1),
"000",
"%02i" % (self._kenwood_valid_tones.index(mem.ctone) + 1),
"",
"0")
return spec
def _parse_mem_spec(self, spec):
mem = chirp_common.Memory()
mem.number = int(spec[2])
mem.freq = int(spec[3])
mem.tuning_step = STEPS[int(spec[4], 16)]
mem.duplex = DUPLEX[int(spec[5])]
if int(spec[7]):
mem.tmode = "Tone"
elif int(spec[8]):
mem.tmode = "TSQL"
mem.rtone = self._kenwood_valid_tones[int(spec[10]) - 1]
mem.ctone = self._kenwood_valid_tones[int(spec[12]) - 1]
return mem
def get_sub_devices(self):
return [TMV7RadioVHF(self.pipe), TMV7RadioUHF(self.pipe)]
def __test_location(self, loc):
mem = self.get_memory(loc)
if not mem.empty:
# Memory was not empty, must be valid
return True
# Mem was empty (or invalid), try to set it
if self._vfo == 0:
mem.freq = 144000000
else:
mem.freq = 440000000
mem.empty = False
try:
self.set_memory(mem)
except Exception:
# Failed, so we're past the limit
return False
# Erase what we did
try:
self.erase_memory(loc)
except Exception:
pass # V7A Can't delete just yet
return True
def _detect_split(self):
return 50
class TMV7RadioSub(TMV7Radio):
"""Base class for the TM-V7 sub devices"""
def __init__(self, pipe):
TMV7Radio.__init__(self, pipe)
self._detect_split()
class TMV7RadioVHF(TMV7RadioSub):
"""TM-V7 VHF subdevice"""
VARIANT = "VHF"
_vfo = 0
class TMV7RadioUHF(TMV7RadioSub):
"""TM-V7 UHF subdevice"""
VARIANT = "UHF"
_vfo = 1
@directory.register
class TMG707Radio(TMV7Radio):
"""Kenwood TM-G707"""
MODEL = "TM-G707"
def get_features(self):
rf = super().get_features()
rf.has_sub_devices = False
rf.memory_bounds = (1, 180)
rf.valid_bands = [(118000000, 174000000),
(300000000, 520000000),
(800000000, 999000000)]
return rf
THG71_STEPS = [5.0, 6.25, 10.0, 12.5, 15.0, 20.0, 25.0, 30.0, 50.0, 100.0]
THG71_TONES = tuple(x for x in chirp_common.OLD_TONES if x != 69.3)
THG71_SPECIALS = ["CALL-V", "CALL-U", "Pr"] + \
["L%i" % i for i in range(10)] + \
["U%i" % i for i in range(10)]
THG71_POWER_LEVELS = ["High", "Low", "Economic Low"]
THG71_POWER_MAP = {0: "High", 2: "Low", 3: "Economic Low"}
THG71_POWER_MAP_REV = {"High": 0, "Low": 2, "Economic Low": 3}
def _thg71_code_to_tone(code_str):
if not code_str or not code_str.isdigit():
return 88.5
idx = int(code_str) - 1
if 0 <= idx < len(chirp_common.OLD_TONES):
t = chirp_common.OLD_TONES[idx]
if t != 69.3:
return t
return 88.5
def _thg71_tone_to_code(tone):
if tone not in chirp_common.OLD_TONES or tone == 69.3:
tone = 88.5
idx = chirp_common.OLD_TONES.index(tone)
return "%02i" % (idx + 1)
@directory.register
class THG71Radio(KenwoodOldLiveRadio):
"""Kenwood TH-G71"""
MODEL = "TH-G71"
HARDWARE_FLOW = False
_upper = 199
_kenwood_split = True
_kenwood_valid_tones = list(chirp_common.OLD_TONES)
def get_features(self):
rf = chirp_common.RadioFeatures()
rf.has_tuning_step = True
rf.has_bank = False
rf.has_dtcs = False
rf.has_dtcs_polarity = False
rf.has_mode = False
rf.has_ctone = True
rf.has_settings = True
rf.has_sub_devices = False
rf.can_odd_split = True
rf.valid_modes = ["FM"]
rf.valid_tmodes = ["", "Tone", "TSQL"]
rf.valid_duplexes = ["", "+", "-", "split"]
rf.valid_skips = ["", "S"]
rf.valid_tuning_steps = list(THG71_STEPS)
rf.valid_tones = THG71_TONES
rf.valid_name_length = 6
rf.valid_characters = chirp_common.CHARSET_UPPER_NUMERIC + "-/ "
rf.memory_bounds = (0, 199)
rf.valid_special_chans = list(THG71_SPECIALS)
rf.valid_bands = [(118000000, 174000000),
(320000000, 470000000),
(800000000, 949000000)]
return rf
def _cmd_get_memory(self, number):
chan = ("%03i" % number) if isinstance(number, int) else str(number)
return "MR", "0,0,%s" % chan
def _cmd_get_memory_name(self, number):
chan = ("%03i" % number) if isinstance(number, int) else str(number)
return "MNA", "0,%s" % chan
def _cmd_get_split(self, number):
chan = ("%03i" % number) if isinstance(number, int) else str(number)
return "MR", "0,1,%s" % chan
def _cmd_set_memory(self, number, spec):
chan = ("%03i" % number) if isinstance(number, int) else str(number)
if spec:
return "MW", "0,0,%s,%s" % (chan, spec)
return "MW", "0,0,%s" % chan
def _cmd_set_memory_name(self, number, name):
chan = ("%03i" % number) if isinstance(number, int) else str(number)
return "MNA", "0,%s,%s" % (chan, name)
def _cmd_set_split(self, number, spec):
chan = ("%03i" % number) if isinstance(number, int) else str(number)
return "MW", "0,1,%s,%s" % (chan, spec)
def _parse_mem_spec(self, spec):
mem = chirp_common.Memory()
chan_str = spec[2]
if chan_str.isdigit():
mem.number = int(chan_str)
else:
mem.number = 0
mem.extd_number = chan_str
mem.freq = int(spec[3])
step_idx = int(spec[4], 16) if spec[4] else 0
if 0 <= step_idx < len(THG71_STEPS):
mem.tuning_step = THG71_STEPS[step_idx]
else:
mem.tuning_step = THG71_STEPS[0]
shift_idx = int(spec[5]) if spec[5] else 0
mem.duplex = DUPLEX.get(shift_idx, "")
if len(spec) > 7 and int(spec[7]):
mem.tmode = "Tone"
elif len(spec) > 8 and int(spec[8]):
mem.tmode = "TSQL"
else:
mem.tmode = ""
mem.rtone = _thg71_code_to_tone(spec[10]) if len(spec) > 10 else 88.5
mem.ctone = _thg71_code_to_tone(spec[12]) if len(spec) > 12 else 88.5
if len(spec) > 13 and spec[13] and spec[13].isdigit():
mem.offset = int(spec[13])
else:
mem.offset = 0
if len(spec) > 14 and spec[14] == "1":
mem.skip = "S"
else:
mem.skip = ""
return mem
def _parse_call_spec(self, band, spec):
mem = chirp_common.Memory()
mem.number = 0
mem.extd_number = "CALL-V" if band == 0 else "CALL-U"
mem.freq = int(spec[2])
step_idx = int(spec[3], 16) if spec[3] else 0
if 0 <= step_idx < len(THG71_STEPS):
mem.tuning_step = THG71_STEPS[step_idx]
else:
mem.tuning_step = THG71_STEPS[0]
shift_idx = int(spec[4]) if spec[4] else 0
mem.duplex = DUPLEX.get(shift_idx, "")
if len(spec) > 6 and int(spec[6]):
mem.tmode = "Tone"
elif len(spec) > 7 and int(spec[7]):
mem.tmode = "TSQL"
else:
mem.tmode = ""
mem.rtone = _thg71_code_to_tone(spec[9]) if len(spec) > 9 else 88.5
mem.ctone = _thg71_code_to_tone(spec[11]) if len(spec) > 11 else 88.5
if len(spec) > 12 and spec[12] and spec[12].isdigit():
mem.offset = int(spec[12])
else:
mem.offset = 0
mem.skip = ""
return mem
def _make_mem_spec(self, mem, is_call=False):
if mem.tuning_step in THG71_STEPS:
step_idx = THG71_STEPS.index(mem.tuning_step)
else:
step_idx = 0
if mem.duplex in ["", "+", "-"]:
duplex_code = util.get_dict_rev(DUPLEX, mem.duplex)
offset = mem.offset
elif mem.duplex == "split":
duplex_code = 0
offset = 0
else:
duplex_code = 0
offset = 0
spec = [
"%011i" % mem.freq,
"%X" % step_idx,
"%i" % duplex_code,
"0",
"%i" % (mem.tmode == "Tone"),
"%i" % (mem.tmode == "TSQL"),
"",
_thg71_tone_to_code(mem.rtone),
"",
_thg71_tone_to_code(mem.ctone),
"%09i" % offset,
]
if not is_call:
spec.append("%i" % (1 if mem.skip == "S" else 0))
return tuple(spec)
def _parse_split_spec(self, mem, spec):
mem.duplex = "split"
mem.offset = int(spec[3])
def _make_split_spec(self, mem):
if mem.tuning_step in THG71_STEPS:
step_idx = THG71_STEPS.index(mem.tuning_step)
else:
step_idx = 0
return ("%011i" % mem.offset, "%X" % step_idx)
def get_memory(self, number):
if isinstance(number, str):
if number not in THG71_SPECIALS:
raise errors.InvalidMemoryLocation(
"Special channel %s is not valid" % number)
mem_key = number
else:
if number < 0 or number > self._upper:
raise errors.InvalidMemoryLocation(
"Number must be between 0 and %i" % self._upper)
mem_key = number
if mem_key in self._memcache and not NOCACHE:
return self._memcache[mem_key]
is_call = False
call_band = 0
if mem_key == "CALL-V":
cmd = ("CR", "0,0")
is_call = True
call_band = 0
elif mem_key == "CALL-U":
cmd = ("CR", "1,0")
is_call = True
call_band = 1
else:
cmd = self._cmd_get_memory(mem_key)
result = self.command(self.pipe, *cmd)
if result in ["N", "E"] or result == "?" or not result:
mem = chirp_common.Memory()
if isinstance(mem_key, int):
mem.number = mem_key
else:
mem.number = 0
mem.extd_number = mem_key
mem.empty = True
self._memcache[mem_key] = mem
return mem
elif " " not in result:
LOG.error("Not sure what to do with this: %r" % result)
raise errors.RadioError("Unexpected result returned from radio")
value = result.split(" ", 1)[1]
spec = value.split(",")
if is_call:
mem = self._parse_call_spec(call_band, spec)
else:
mem = self._parse_mem_spec(spec)
self._memcache[mem_key] = mem
if not is_call and self._has_name:
result = self.command(self.pipe,
*self._cmd_get_memory_name(mem_key))
if " " in result:
value = result.split(" ", 1)[1]
parts = value.split(",")
if len(parts) >= 3:
mem.name = parts[2].rstrip()
elif len(parts) == 2:
mem.name = parts[1].rstrip()
if mem.duplex == "" and mem.offset == 0:
if is_call:
split_cmd = ("CR", "%i,1" % call_band)
result = self.command(self.pipe, *split_cmd)
if not iserr(result) and " " in result:
value = result.split(" ", 1)[1]
parts = value.split(",")
if len(parts) >= 3:
mem.duplex = "split"
mem.offset = int(parts[2])
else:
result = self.command(self.pipe,
*self._cmd_get_split(mem_key))
if not iserr(result) and " " in result:
value = result.split(" ", 1)[1]
self._parse_split_spec(mem, value.split(","))
return mem
def set_memory(self, memory):
mem_key = memory.extd_number if memory.extd_number else memory.number
if isinstance(mem_key, str):
if mem_key not in THG71_SPECIALS:
raise errors.InvalidMemoryLocation(
"Special channel %s is not valid" % mem_key)
else:
if mem_key < 0 or mem_key > self._upper:
raise errors.InvalidMemoryLocation(
"Number must be between 0 and %i" % self._upper)
if memory.tmode == "Tone" and memory.rtone not in THG71_TONES:
raise errors.UnsupportedToneError("This radio does not support "
"tone %.1fHz" % memory.rtone)
if memory.tmode == "TSQL" and memory.ctone not in THG71_TONES:
raise errors.UnsupportedToneError("This radio does not support "
"tone %.1fHz" % memory.ctone)
if memory.empty:
self.erase_memory(mem_key)
return
is_call = False
call_band = 0
if mem_key == "CALL-V":
is_call = True
call_band = 0
elif mem_key == "CALL-U":
is_call = True
call_band = 1
spec = self._make_mem_spec(memory, is_call=is_call)
spec_str = ",".join(spec)
self._memcache.pop(mem_key, None)
if is_call:
r1 = self.command(self.pipe, "CW",
"%i,0,%s" % (call_band, spec_str))
else:
r1 = self.command(self.pipe,
*self._cmd_set_memory(mem_key, spec_str))
if iserr(r1):
raise errors.InvalidDataError("Radio refused channel %s" % mem_key)
if not is_call and self._has_name:
time.sleep(0.05)
clean_name = memory.name.upper()[:6]
r2 = self.command(self.pipe,
*self._cmd_set_memory_name(mem_key, clean_name))
if iserr(r2):
raise errors.InvalidDataError("Radio refused name %s: %r" %
(mem_key, clean_name))
if memory.duplex == "split":
time.sleep(0.05)
if is_call:
if memory.tuning_step in THG71_STEPS:
step_idx = THG71_STEPS.index(memory.tuning_step)
else:
step_idx = 0
split_spec = "%011i,%X" % (memory.offset, step_idx)
result = self.command(self.pipe, "CW",
"%i,1,%s" % (call_band, split_spec))
else:
split_spec = ",".join(self._make_split_spec(memory))
result = self.command(self.pipe,
*self._cmd_set_split(mem_key,
split_spec))
if iserr(result):
raise errors.InvalidDataError("Radio refused split on %s" %
mem_key)
def erase_memory(self, number):
if number in ["CALL-V", "CALL-U"]:
return
resp = self.command(self.pipe, *self._cmd_set_memory(number, ""))
if iserr(resp):
raise errors.RadioError("Radio refused delete of %s" % number)
self._memcache.pop(number, None)
def get_settings(self):
basic = RadioSettingGroup("basic", "Basic Settings")
dtmf = RadioSettingGroup("dtmf", "DTMF Memories")
top = RadioSettings(basic, dtmf)
# VHF Transmit Power
curr = "High"
try:
resp = self.command(self.pipe, "PC", "0")
if not iserr(resp) and " " in resp:
val = int(resp.split(" ")[1].split(",")[1])
curr = THG71_POWER_MAP.get(val, "High")
except Exception:
pass
rs = RadioSetting("pc_vhf", "VHF Transmit Power",
RadioSettingValueList(
THG71_POWER_LEVELS,
current_index=THG71_POWER_LEVELS.index(curr)))
basic.append(rs)
# UHF Transmit Power
curr = "High"
try:
resp = self.command(self.pipe, "PC", "1")
if not iserr(resp) and " " in resp:
val = int(resp.split(" ")[1].split(",")[1])
curr = THG71_POWER_MAP.get(val, "High")
except Exception:
pass
rs = RadioSetting("pc_uhf", "UHF Transmit Power",
RadioSettingValueList(
THG71_POWER_LEVELS,
current_index=THG71_POWER_LEVELS.index(curr)))
basic.append(rs)
# Squelch Level (0-5)
val = 2
try:
resp = self.command(self.pipe, "SQ", "0")
if not iserr(resp) and " " in resp:
val = int(resp.split(" ")[1].split(",")[1])
except Exception:
pass
rs = RadioSetting("squelch", "Squelch Level",
RadioSettingValueInteger(0, 5, val))
basic.append(rs)
# DTMF Memories (DM 00 to DM 09)
dtmf_charset = "0123456789ABCD*#"
for i in range(10):
dtmf_val = ""
try:
resp = self.command(self.pipe, "DM", "%02i" % i)
if not iserr(resp) and " " in resp:
parts = resp.split(" ", 1)[1].split(",")
if len(parts) > 1:
dtmf_val = parts[1].replace("E", "*").replace("F", "#")
except Exception:
pass
rs = RadioSetting("dm_%02i" % i, "DTMF Memory %i" % i,
RadioSettingValueString(0, 16, dtmf_val,
autopad=False,
charset=dtmf_charset))
dtmf.append(rs)
return top
def set_settings(self, settings):
for element in settings:
if not isinstance(element, RadioSetting):
self.set_settings(element)
continue
if not element.changed():
continue
name = element.get_name()
if name == "pc_vhf":
code = THG71_POWER_MAP_REV.get(str(element.value), 0)
self.command(self.pipe, "PC", "0,%i" % code)
elif name == "pc_uhf":
code = THG71_POWER_MAP_REV.get(str(element.value), 0)
self.command(self.pipe, "PC", "1,%i" % code)
elif name == "squelch":
self.command(self.pipe, "SQ", "0,%02i" % int(element.value))
elif name.startswith("dm_"):
idx = int(name.split("_")[1])
raw = str(element.value).upper().replace("*", "E")
raw = raw.replace("#", "F")
self.command(self.pipe, "DM", "%02i,%s" % (idx, raw))
THF6A_STEPS = [5.0, 6.25, 8.33, 9.0, 10.0, 12.5, 15.0, 20.0, 25.0, 30.0, 50.0,
100.0]
THF6A_DUPLEX = dict(DUPLEX)
THF6A_DUPLEX[3] = "split"
@directory.register
class THF6ARadio(KenwoodLiveRadio):
"""Kenwood TH-F6"""
MODEL = "TH-F6"
HARDWARE_FLOW = False
_charset = chirp_common.CHARSET_ASCII
_upper = 399
_kenwood_split = True
_kenwood_valid_tones = list(KENWOOD_TONES)
def get_features(self):
rf = chirp_common.RadioFeatures()
rf.has_dtcs_polarity = False
rf.has_bank = False
rf.can_odd_split = True
rf.valid_modes = list(THF6_MODES)
rf.valid_tmodes = ["", "Tone", "TSQL", "DTCS"]
rf.valid_tuning_steps = list(THF6A_STEPS)
rf.valid_bands = [(1000, 1300000000)]
rf.valid_skips = ["", "S"]
rf.valid_duplexes = list(THF6A_DUPLEX.values())
rf.valid_characters = self._charset
rf.valid_name_length = 8
rf.memory_bounds = (0, self._upper)
rf.has_settings = True
return rf
def _cmd_set_memory(self, number, spec):
if spec:
spec = "," + spec
return "MW", "0,%03i%s" % (number, spec)
def _cmd_get_memory(self, number):
return "MR", "0,%03i" % number
def _cmd_get_memory_name(self, number):
return "MNA", "%03i" % number
def _cmd_set_memory_name(self, number, name):
return "MNA", "%03i,%s" % (number, name)
def _cmd_get_split(self, number):
return "MR", "1,%03i" % number
def _cmd_set_split(self, number, spec):
return "MW", "1,%03i,%s" % (number, spec)
def _parse_mem_spec(self, spec):
mem = chirp_common.Memory()
mem.number = int(spec[1])
mem.freq = int(spec[2])
mem.tuning_step = THF6A_STEPS[int(spec[3], 16)]
mem.duplex = THF6A_DUPLEX[int(spec[4])]
mem.tmode = get_tmode(spec[6], spec[7], spec[8])
mem.rtone = self._kenwood_valid_tones[int(spec[9])]
mem.ctone = self._kenwood_valid_tones[int(spec[10])]
if spec[11] and spec[11].isdigit():
mem.dtcs = chirp_common.DTCS_CODES[int(spec[11])]
else:
LOG.warn("Unknown or invalid DCS: %s" % spec[11])
if spec[12]:
mem.offset = int(spec[12])
else:
mem.offset = 0
mem.mode = THF6_MODES[int(spec[13])]
if spec[14] == "1":
mem.skip = "S"
return mem
def _make_mem_spec(self, mem):
if mem.duplex in " +-":
duplex = util.get_dict_rev(THF6A_DUPLEX, mem.duplex)
offset = mem.offset
elif mem.duplex == "split":
duplex = 0
offset = 0
else:
LOG.warn("Bug: unsupported duplex `%s'" % mem.duplex)
spec = (
"%011i" % mem.freq,
"%X" % THF6A_STEPS.index(mem.tuning_step),
"%i" % duplex,
"0",
"%i" % (mem.tmode == "Tone"),
"%i" % (mem.tmode == "TSQL"),
"%i" % (mem.tmode == "DTCS"),
"%02i" % (self._kenwood_valid_tones.index(mem.rtone)),
"%02i" % (self._kenwood_valid_tones.index(mem.ctone)),
"%03i" % (chirp_common.DTCS_CODES.index(mem.dtcs)),
"%09i" % offset,
"%i" % (THF6_MODES.index(mem.mode)),
"%i" % (mem.skip == "S"))
return spec
_SETTINGS_OPTIONS = {
"APO": ["Off", "30min", "60min"],
"BAL": ["100%:0%", "75%:25%", "50%:50%", "25%:75%", "%0:100%"],
"BAT": ["Lithium", "Alkaline"],
"CKEY": ["Call", "1750 Hz"],
"DATP": ["1200bps", "9600bps"],
"LAN": ["English", "Japanese"],
"MNF": ["Name", "Frequency"],
"MRM": ["All Band", "Current Band"],
"PT": ["100ms", "250ms", "500ms", "750ms",
"1000ms", "1500ms", "2000ms"],
"SCR": ["Time", "Carrier", "Seek"],
"SV": ["Off", "0.2s", "0.4s", "0.6s", "0.8s", "1.0s",
"2s", "3s", "4s", "5s"],
"VXD": ["250ms", "500ms", "750ms", "1s", "1.5s", "2s", "3s"],
}
def get_settings(self):
main = RadioSettingGroup("main", "Main")
aux = RadioSettingGroup("aux", "Aux")
save = RadioSettingGroup("save", "Save")
display = RadioSettingGroup("display", "Display")
dtmf = RadioSettingGroup("dtmf", "DTMF")
top = RadioSettings(main, aux, save, display, dtmf)
lists = [("APO", save, "Automatic Power Off"),
("BAL", main, "Balance"),
("BAT", save, "Battery Type"),
("CKEY", aux, "CALL Key Set Up"),
("DATP", aux, "Data Packet Speed"),
("LAN", display, "Language"),
("MNF", main, "Memory Display Mode"),
("MRM", main, "Memory Recall Method"),
("PT", dtmf, "DTMF Speed"),
("SCR", main, "Scan Resume"),
("SV", save, "Battery Save"),
("VXD", aux, "VOX Drop Delay"),
]
bools = [("ANT", aux, "Bar Antenna"),
("ATT", main, "Attenuator Enabled"),
("ARO", main, "Automatic Repeater Offset"),
("BEP", aux, "Beep for keypad"),
("DL", main, "Dual"),
("DLK", dtmf, "DTMF Lockout On Transmit"),
("ELK", aux, "Enable Locked Tuning"),
("LK", main, "Lock"),
("LMP", display, "Lamp"),
("NSFT", aux, "Noise Shift"),
("TH", aux, "Tx Hold for 1750"),
("TSP", dtmf, "DTMF Fast Transmission"),
("TXH", dtmf, "TX Hold DTMF"),
("TXS", main, "Transmit Inhibit"),
("VOX", aux, "VOX Enable"),
("VXB", aux, "VOX On Busy"),
]
ints = [("CNT", display, "Contrast", 1, 16),
("VXG", aux, "VOX Gain", 0, 9),
]
strings = [("MES", display, "Power-on Message", 8),
]
for setting, group, name in bools:
value = self._kenwood_get_bool(setting)
rs = RadioSetting(setting, name,
RadioSettingValueBoolean(value))
group.append(rs)
for setting, group, name in lists:
value = self._kenwood_get_int(setting)
options = self._SETTINGS_OPTIONS[setting]
rs = RadioSetting(setting, name,
RadioSettingValueList(options,
current_index=value))
group.append(rs)
for setting, group, name, minv, maxv in ints:
value = self._kenwood_get_int(setting)
rs = RadioSetting(setting, name,
RadioSettingValueInteger(minv, maxv, value))
group.append(rs)
for setting, group, name, length in strings:
_cmd, value = self._kenwood_get(setting)
rs = RadioSetting(setting, name,
RadioSettingValueString(0, length, value))
group.append(rs)
return top
@directory.register
class THF7ERadio(THF6ARadio):
"""Kenwood TH-F7"""
MODEL = "TH-F7"
_charset = chirp_common.CHARSET_1252
D710_DUPLEX = ["", "+", "-", "split"]
D710_MODES = ["FM", "NFM", "AM"]
D710_SKIP = ["", "S"]
D710_STEPS = [5.0, 6.25, 8.33, 10.0, 12.5, 15.0, 20.0, 25.0, 30.0, 50.0, 100.0]
@directory.register
class TMD710Radio(KenwoodLiveRadio):
"""Kenwood TM-D710"""
MODEL = "TM-D710"
_upper = 999
_kenwood_valid_tones = list(KENWOOD_TONES)
def get_features(self):
rf = chirp_common.RadioFeatures()
rf.can_odd_split = True
rf.has_dtcs_polarity = False
rf.has_bank = False
rf.valid_tmodes = ["", "Tone", "TSQL", "DTCS"]
rf.valid_modes = D710_MODES
rf.valid_duplexes = D710_DUPLEX
rf.valid_tuning_steps = D710_STEPS
rf.valid_characters = chirp_common.CHARSET_ASCII.replace(',', '')
rf.valid_name_length = 8
rf.valid_skips = D710_SKIP
rf.memory_bounds = (0, 999)
rf.valid_bands = [(118000000, 524000000), (800000000, 1300000000)]
return rf
def _cmd_get_memory(self, number):
return "ME", "%03i" % number
def _cmd_get_memory_name(self, number):
return "MN", "%03i" % number
def _cmd_set_memory(self, number, spec):
return "ME", "%03i,%s" % (number, spec)
def _cmd_set_memory_name(self, number, name):
return "MN", "%03i,%s" % (number, name)
def _parse_mem_spec(self, spec):
mem = chirp_common.Memory()
mem.number = int(spec[0])
mem.freq = int(spec[1])
mem.tuning_step = D710_STEPS[int(spec[2], 16)]
mem.duplex = D710_DUPLEX[int(spec[3])]
# Reverse
if int(spec[5]):
mem.tmode = "Tone"
elif int(spec[6]):
mem.tmode = "TSQL"
elif int(spec[7]):
mem.tmode = "DTCS"
mem.rtone = self._kenwood_valid_tones[int(spec[8])]
mem.ctone = self._kenwood_valid_tones[int(spec[9])]
mem.dtcs = chirp_common.DTCS_CODES[int(spec[10])]
mem.offset = int(spec[11])
mem.mode = D710_MODES[int(spec[12])]
# TX Frequency
if int(spec[13]):
mem.duplex = "split"
mem.offset = int(spec[13])
# Unknown
mem.skip = D710_SKIP[int(spec[15])] # Memory Lockout
return mem
def _make_mem_spec(self, mem):
spec = (
"%010i" % mem.freq,
"%X" % D710_STEPS.index(mem.tuning_step),
"%i" % (0 if mem.duplex == "split"
else D710_DUPLEX.index(mem.duplex)),
"0", # Reverse
"%i" % (mem.tmode == "Tone" and 1 or 0),
"%i" % (mem.tmode == "TSQL" and 1 or 0),
"%i" % (mem.tmode == "DTCS" and 1 or 0),
"%02i" % (self._kenwood_valid_tones.index(mem.rtone)),
"%02i" % (self._kenwood_valid_tones.index(mem.ctone)),
"%03i" % (chirp_common.DTCS_CODES.index(mem.dtcs)),
"%08i" % (0 if mem.duplex == "split" else mem.offset), # Offset
"%i" % D710_MODES.index(mem.mode),
"%010i" % (mem.offset if mem.duplex == "split" else 0), # TX Freq
"0", # Unknown
"%i" % D710_SKIP.index(mem.skip), # Memory Lockout
)
return spec
@directory.register
class THD72Radio(TMD710Radio):
"""Kenwood TH-D72"""
MODEL = "TH-D72 (live mode)"
HARDWARE_FLOW = sys.platform == "darwin" # only OS X driver needs hw flow
def _parse_mem_spec(self, spec):
mem = chirp_common.Memory()
mem.number = int(spec[0])
mem.freq = int(spec[1])
mem.tuning_step = D710_STEPS[int(spec[2], 16)]
mem.duplex = D710_DUPLEX[int(spec[3])]
# Reverse
if int(spec[5]):
mem.tmode = "Tone"
elif int(spec[6]):
mem.tmode = "TSQL"
elif int(spec[7]):
mem.tmode = "DTCS"
mem.rtone = self._kenwood_valid_tones[int(spec[9])]
mem.ctone = self._kenwood_valid_tones[int(spec[10])]
mem.dtcs = chirp_common.DTCS_CODES[int(spec[11])]
mem.offset = int(spec[13])
mem.mode = D710_MODES[int(spec[14])]
# TX Frequency
if int(spec[15]):
mem.duplex = "split"
mem.offset = int(spec[15])
# Lockout
mem.skip = D710_SKIP[int(spec[17])] # Memory Lockout
return mem
def _make_mem_spec(self, mem):
spec = (
"%010i" % mem.freq,
"%X" % D710_STEPS.index(mem.tuning_step),
"%i" % (0 if mem.duplex == "split"
else D710_DUPLEX.index(mem.duplex)),
"0", # Reverse
"%i" % (mem.tmode == "Tone" and 1 or 0),
"%i" % (mem.tmode == "TSQL" and 1 or 0),
"%i" % (mem.tmode == "DTCS" and 1 or 0),
"0",
"%02i" % (self._kenwood_valid_tones.index(mem.rtone)),
"%02i" % (self._kenwood_valid_tones.index(mem.ctone)),
"%03i" % (chirp_common.DTCS_CODES.index(mem.dtcs)),
"0",
"%08i" % (0 if mem.duplex == "split" else mem.offset), # Offset
"%i" % D710_MODES.index(mem.mode),
"%010i" % (mem.offset if mem.duplex == "split" else 0), # TX Freq
"0", # Unknown
"%i" % D710_SKIP.index(mem.skip), # Memory Lockout
)
return spec
@directory.register
class THD74Radio(TMD710Radio):
"""Kenwood TH_D74"""
MODEL = "TH-D74 (live mode)"
HARDWARE_FLOW = sys.platform == "darwin"
STEPS = [5.0, 6.25, 8.33, 9.0, 10.0, 12.5, 15.0, 20.0,
25.0, 30.0, 50.0, 100.0]
MODES = ['FM', 'DV', 'AM', 'LSB', 'USB', 'CW', 'NFM', 'DR',
'WFM', 'R-CW']
CROSS_MODES = ['DTCS->', 'Tone->DTCS', 'DTCS->Tone', 'Tone->Tone']
DUPLEX = ['', '+', '-', 'split']
_has_name = False
@classmethod
def get_prompts(cls):
rp = chirp_common.RadioPrompts()
rp.experimental = ("This driver is incomplete as the D74 lacks "
"the full serial command set of older radios. "
"As such, this should be considered permanently "
"experimental.")
return rp
def _cmd_get_memory_name(self, number):
return ''
def get_features(self):
rf = super(THD74Radio, self).get_features()
rf.valid_tuning_steps = self.STEPS
rf.valid_modes = self.MODES
rf.valid_cross_modes = self.CROSS_MODES
rf.valid_tmodes = ['', 'Tone', 'TSQL', 'DTCS', 'Cross']
rf.has_name = False # Radio has it, but no command to retrieve
rf.has_cross = True
return rf
def _parse_mem_spec(self, spec):
mem = chirp_common.Memory()
mem.number = int(spec[0])
mem.freq = int(spec[1])
mem.offset = int(spec[2])
mem.tuning_step = self.STEPS[int(spec[3])]
mem.mode = self.MODES[int(spec[5])]
if int(spec[11]):
mem.tmode = "Cross"
mem.cross_mode = self.CROSS_MODES[int(spec[18])]
elif int(spec[8]):
mem.tmode = "Tone"
elif int(spec[9]):
mem.tmode = "TSQL"
elif int(spec[10]):
mem.tmode = "DTCS"
mem.duplex = self.DUPLEX[int(spec[14])]
mem.rtone = chirp_common.TONES[int(spec[15])]
mem.ctone = chirp_common.TONES[int(spec[16])]
mem.dtcs = chirp_common.DTCS_CODES[int(spec[17])]
mem.skip = int(spec[22]) and 'S' or ''
return mem
def _make_mem_spec(self, mem):
spec = (
"%010i" % mem.freq,
"%010i" % mem.offset,
"%X" % self.STEPS.index(mem.tuning_step),
"%X" % self.STEPS.index(mem.tuning_step),
"%i" % self.MODES.index(mem.mode),
"0", # Fine mode
"0", # Fine step
"%i" % (mem.tmode == "Tone" and 1 or 0),
"%i" % (mem.tmode == "TSQL" and 1 or 0),
"%i" % (mem.tmode == "DTCS" and 1 or 0),
"%i" % (mem.tmode == 'Cross'),
"0", # Reverse
"0", # Odd split channel
"%i" % self.DUPLEX.index(mem.duplex),
"%02i" % (chirp_common.TONES.index(mem.rtone)),
"%02i" % (chirp_common.TONES.index(mem.ctone)),
"%03i" % (chirp_common.DTCS_CODES.index(mem.dtcs)),
"%i" % self.CROSS_MODES.index(mem.cross_mode),
"CQCQCQ", # URCALL
"0", # D-STAR squelch type
"00", # D-STAR squelch code
"%i" % D710_SKIP.index(mem.skip), # Memory Lockout
)
return spec
@directory.register
class TMV71Radio(TMD710Radio):
"""Kenwood TM-V71"""
MODEL = "TM-V71"
def get_features(self):
rf = super().get_features()
rf.valid_characters = ''.join(c for c in rf.valid_characters
if not c.isalpha() or
c not in rf.valid_characters.lower())
return rf
@directory.register
class TMD710GRadio(TMD710Radio):
"""Kenwood TM-D710G"""
MODEL = "TM-D710G"
@classmethod
def get_prompts(cls):
rp = chirp_common.RadioPrompts()
rp.experimental = ("This radio driver is currently under development, "
"and supports the same features as the TM-D710A/E. "
"There are no known issues with it, but you should "
"proceed with caution.")
return rp
THK2_DUPLEX = ["", "+", "-"]
THK2_MODES = ["FM", "NFM"]
THK2_CHARS = chirp_common.CHARSET_UPPER_NUMERIC + "-/"
@directory.register
class THK2Radio(KenwoodLiveRadio):
"""Kenwood TH-K2"""
MODEL = "TH-K2"
HARDWARE_FLOW = False
_probe_memory = 99
_kenwood_valid_tones = list(KENWOOD_TONES)
def __init__(self, *a, **k):
self._has_name = True
super().__init__(*a, **k)
if self.pipe:
self._probe_layout()
def _probe_layout(self):
"""Determine if the radio is in 100+alpha or 200 channel mode"""
rf = self.get_features()
wasempty = None
try:
LOG.debug('Getting memory %i', self._probe_memory)
m = self.get_memory(self._probe_memory)
wasempty = m.empty
if wasempty:
m.freq = rf.valid_bands[0][0]
m.empty = False
LOG.debug('Memory %i was empty using %s to probe',
self._probe_memory, m)
else:
LOG.debug('Memory %i was set: %s', self._probe_memory, m)
# This either sets the probe memory over itself, or creates one
# at the lower limit frequency. Note that in order for this to
# work, we need _has_name=True here so that we can detect if we
# failed to set the memory (because 199 is out of range) or the
# memory name (because 199 is in range, but names are not
# supported).
self.set_memory(m)
LOG.error('Unexpectedly successful at setting probe memory %s', m)
except errors.InvalidDataError as e:
LOG.debug('Probe response was %s', e)
if 'name' in str(e):
# Radio is configured for max channels, refused the name
self._has_name = False
LOG.info('Radio determined to be in max-channel mode')
else:
# Radio is configured for alpha channels, as it refused the
# whole thing
self._has_name = True
LOG.info('Radio determined to be in channel+alpha mode')
finally:
if wasempty:
LOG.debug('Erasing probe memory %s', m)
# This needs to do the raw command because of the memory
# caching in set_memory()
self.command(self.pipe, *self._cmd_set_memory(
self._probe_memory, ""))
LOG.debug('Done probing layout')
def get_features(self):
rf = chirp_common.RadioFeatures()
rf.can_odd_split = False
rf.has_dtcs_polarity = False
rf.has_bank = False
rf.has_tuning_step = False
rf.valid_tmodes = ["", "Tone", "TSQL", "DTCS"]
rf.valid_modes = THK2_MODES
rf.valid_duplexes = THK2_DUPLEX
rf.valid_characters = THK2_CHARS
rf.valid_name_length = 6
rf.valid_bands = [(136000000, 173990000)]
rf.valid_skips = ["", "S"]
rf.valid_tuning_steps = [5.0]
rf.memory_bounds = (0, 49 if self._has_name else 99)
return rf
def _cmd_get_memory(self, number):
return "ME", "%02i" % number
def _cmd_get_memory_name(self, number):
return "MN", "%02i" % number
def _cmd_set_memory(self, number, spec):
return "ME", "%02i,%s" % (number, spec)
def _cmd_set_memory_name(self, number, name):
return "MN", "%02i,%s" % (number, name)
def _parse_mem_spec(self, spec):
mem = chirp_common.Memory()
mem.number = int(spec[0])
mem.freq = int(spec[1])
# mem.tuning_step =
mem.duplex = THK2_DUPLEX[int(spec[3])]
if int(spec[5]):
mem.tmode = "Tone"
elif int(spec[6]):
mem.tmode = "TSQL"
elif int(spec[7]):
mem.tmode = "DTCS"
mem.rtone = self._kenwood_valid_tones[int(spec[8])]
mem.ctone = self._kenwood_valid_tones[int(spec[9])]
mem.dtcs = chirp_common.DTCS_CODES[int(spec[10])]
mem.offset = int(spec[11])
mem.mode = THK2_MODES[int(spec[12])]
mem.skip = int(spec[16]) and "S" or ""
return mem
def _make_mem_spec(self, mem):
try:
rti = self._kenwood_valid_tones.index(mem.rtone)
cti = self._kenwood_valid_tones.index(mem.ctone)
except ValueError:
raise errors.UnsupportedToneError()
spec = (
"%010i" % mem.freq,
"0",
"%i" % THK2_DUPLEX.index(mem.duplex),
"0",
"%i" % int(mem.tmode == "Tone"),
"%i" % int(mem.tmode == "TSQL"),
"%i" % int(mem.tmode == "DTCS"),
"%02i" % rti,
"%02i" % cti,
"%03i" % chirp_common.DTCS_CODES.index(mem.dtcs),
"%08i" % mem.offset,
"%i" % THK2_MODES.index(mem.mode),
"0",
"%010i" % 0,
"0",
"%i" % int(mem.skip == "S")
)
return spec
TM271_STEPS = [2.5, 5.0, 6.25, 10.0, 12.5, 15.0, 20.0, 25.0, 30.0, 50.0, 100.0]
@directory.register
class TM271Radio(THK2Radio):
"""Kenwood TM-271"""
MODEL = "TM-271"
_probe_memory = 199
def get_features(self):
rf = chirp_common.RadioFeatures()
rf.can_odd_split = False
rf.has_dtcs_polarity = False
rf.has_bank = False
rf.has_tuning_step = False
rf.has_name = self._has_name
rf.valid_tmodes = ["", "Tone", "TSQL", "DTCS"]
rf.valid_modes = THK2_MODES
rf.valid_duplexes = THK2_DUPLEX
rf.valid_characters = THK2_CHARS
rf.valid_name_length = 6
rf.valid_bands = [(137000000, 173990000)]
rf.valid_skips = ["", "S"]
rf.valid_tuning_steps = list(TM271_STEPS)
rf.memory_bounds = (0, 99 if self._has_name else 199)
return rf
def _cmd_get_memory(self, number):
return "ME", "%03i" % number
def _cmd_get_memory_name(self, number):
return "MN", "%03i" % number
def _cmd_set_memory(self, number, spec):
return "ME", "%03i,%s" % (number, spec)
def _cmd_set_memory_name(self, number, name):
return "MN", "%03i,%s" % (number, name)
@directory.register
class TM281Radio(TM271Radio):
"""Kenwood TM-281"""
MODEL = "TM-281"
# seems that this is a perfect clone of TM271 with just a different model
@directory.register
class TM471Radio(TM271Radio):
"""Kenwood TM-471"""
MODEL = "TM-471"
def get_features(self):
rf = super().get_features()
rf.valid_bands = [(444000000, 479990000)]
return rf
def _cmd_get_memory(self, number):
return "ME", "%03i" % number
def _cmd_get_memory_name(self, number):
return "MN", "%03i" % number
def _cmd_set_memory(self, number, spec):
return "ME", "%03i,%s" % (number, spec)
def _cmd_set_memory_name(self, number, name):
return "MN", "%03i,%s" % (number, name)
@directory.register
class TS590Radio(KenwoodLiveRadio):
"""Kenwood TS-590S/SG"""
MODEL = "TS-590S/SG_LiveMode"
_kenwood_valid_tones = list(KENWOOD_TONES)
_kenwood_valid_tones.append(1750)
_upper = 99
_duplex = ["", "-", "+"]
_skip = ["", "S"]
_modes = ["LSB", "USB", "CW", "FM", "AM", "FSK", "CW-R",
"FSK-R", "Data+LSB", "Data+USB", "Data+FM"]
_bands = [(1800000, 2000000), # 160M Band
(3500000, 4000000), # 80M Band
(5167500, 5450000), # 60M Band
(7000000, 7300000), # 40M Band
(10100000, 10150000), # 30M Band
(14000000, 14350000), # 20M Band
(18068000, 18168000), # 17M Band
(21000000, 21450000), # 15M Band
(24890000, 24990000), # 12M Band
(28000000, 29700000), # 10M Band
(50000000, 54000000)] # 6M Band
def get_features(self):
rf = chirp_common.RadioFeatures()
rf.can_odd_split = False
rf.has_bank = False
rf.has_ctone = True
rf.has_dtcs = False
rf.has_dtcs_polarity = False
rf.has_name = True
rf.has_settings = False
rf.has_offset = True
rf.has_mode = True
rf.has_tuning_step = False
rf.has_nostep_tuning = True
rf.has_cross = True
rf.has_comment = False
rf.memory_bounds = (0, self._upper)
rf.valid_bands = self._bands
rf.valid_characters = chirp_common.CHARSET_UPPER_NUMERIC + "*+-/"
rf.valid_duplexes = ["", "-", "+"]
rf.valid_modes = self._modes
rf.valid_skips = ["", "S"]
rf.valid_tmodes = ["", "Tone", "TSQL", "Cross"]
rf.valid_cross_modes = ["Tone->Tone", "->Tone"]
rf.valid_name_length = 8 # 8 character channel names
return rf
def _my_val_list(setting, opts, obj, atrb):
"""Callback:from ValueList. Set the integer index."""
value = opts.index(str(setting.value))
setattr(obj, atrb, value)
return
def get_memory(self, number):
"""Convert ascii channel data spec into UI columns (mem)"""
mem = chirp_common.Memory()
mem.extra = RadioSettingGroup("extra", "Extra")
# Read the base and split MR strings
mem.number = number
spec0 = self.command(self.pipe, "MR0 %02i" % mem.number)
spec1 = self.command(self.pipe, "MR1 %02i" % mem.number)
mem.name = spec0[41:49] # Max 8-Char Name if assigned
mem.name = mem.name.strip()
mem.name = mem.name.upper()
_p4 = int(spec0[6:17]) # Rx Frequency
_p4s = int(spec1[6:17]) # Offset freq (Tx)
_p5 = int(spec0[17]) # Mode
_p6 = int(spec0[18]) # Data Mode
_p7 = int(spec0[19]) # Tone Mode
_p8 = int(spec0[20:22]) # Tone Frequency Index
_p9 = int(spec0[22:24]) # CTCSS Frequency Index
_p11 = int(spec0[27]) # Filter A/B
_p14 = int(spec0[38:40]) # FM Mode
_p15 = int(spec0[40]) # Chan Lockout (Skip)
if _p4 == 0:
mem.empty = True
return mem
mem.empty = False
mem.freq = _p4
mem.duplex = self._duplex[0] # None by default
mem.offset = 0
if _p4 < _p4s: # + shift
mem.duplex = self._duplex[2]
mem.offset = _p4s - _p4
if _p4 > _p4s: # - shift
mem.duplex = self._duplex[1]
mem.offset = _p4 - _p4s
mx = _p5 - 1 # CAT modes start at 1
if _p5 == 9: # except CAT FSK-R is 9, there is no 8
mx = 7
if _p6: # LSB+Data= 8, USB+Data= 9, FM+Data= 10
if _p5 == 1: # CAT LSB
mx = 8
elif _p5 == 2: # CAT USB
mx = 9
elif _p5 == 4: # CAT FM
mx = 10
mem.mode = self._modes[mx]
mem.tmode = ""
mem.cross_mode = "Tone->Tone"
mem.ctone = self._kenwood_valid_tones[_p9]
mem.rtone = self._kenwood_valid_tones[_p8]
if _p7 == 1:
mem.tmode = "Tone"
elif _p7 == 2:
mem.tmode = "TSQL"
elif _p7 == 3:
mem.tmode = "Cross"
mem.skip = self._skip[_p15]
rx = RadioSettingValueBoolean(bool(_p14))
rset = RadioSetting("fmnrw", "FM Narrow mode (off = Wide)", rx)
mem.extra.append(rset)
return mem
def erase_memory(self, number):
""" Send the blank string to MW0 """
mem = chirp_common.Memory()
mem.empty = True
mem.freq = 0
mem.offset = 0
spx = "MW0%03i00000000000000000000000000000000000" % number
rx = self.command(self.pipe, spx) # Send MW0
return mem
def set_memory(self, mem):
"""Send UI column data (mem) to radio"""
pfx = "MW0%03i" % mem.number
xmode = 0
xtmode = 0
xrtone = 8
xctone = 8
xdata = 0
xfltr = 0
xfm = 0
xskip = 0
xfreq = mem.freq
if xfreq > 0: # if empty; use those defaults
ix = self._modes.index(mem.mode)
xmode = ix + 1 # stored as CAT values, LSB= 1
if ix == 7: # FSK-R
xmode = 9 # There is no CAT 8
if ix > 7: # a Data mode
xdata = 1
if ix == 8:
xmode = 1 # LSB
elif ix == 9:
xmode = 2 # USB
elif ix == 10:
xmode = 4 # FM
if mem.tmode == "Tone":
xtmode = 1
xrtone = self._kenwood_valid_tones.index(mem.rtone)
if mem.tmode == "TSQL" or mem.tmode == "Cross":
xtmode = 2
if mem.tmode == "Cross":
xtmode = 3
xctone = self._kenwood_valid_tones.index(mem.ctone)
for setting in mem.extra:
if setting.get_name() == "fmnrw":
xfm = setting.value
if mem.skip == "S":
xskip = 1
spx = "%011i%1i%1i%1i%02i%02i000%1i0000000000%02i%1i%s" \
% (xfreq, xmode, xdata, xtmode, xrtone,
xctone, xfltr, xfm, xskip, mem.name)
rx = self.command(self.pipe, pfx, spx) # Send MW0
if mem.offset != 0:
pfx = "MW1%03i" % mem.number
xfreq = mem.freq - mem.offset
if mem.duplex == "+":
xfreq = mem.freq + mem.offset
spx = "%011i%1i%1i%1i%02i%02i000%1i0000000000%02i%1i%s" \
% (xfreq, xmode, xdata, xtmode, xrtone,
xctone, xfltr, xfm, xskip, mem.name)
rx = self.command(self.pipe, pfx, spx) # Send MW1
@directory.register
class TS480Radio(KenwoodLiveRadio):
"""Kenwood TS-480"""
MODEL = "TS-480_LiveMode"
_kenwood_valid_tones = list(KENWOOD_TONES)
_kenwood_valid_tones.append(1750)
_upper = 99
_duplex = ["", "-", "+"]
_skip = ["", "S"]
_modes = ["LSB", "USB", "CW", "FM", "AM", "FSK", "CW-R", "N/A",
"FSK-R"]
_bands = [(1800000, 2000000), # 160M Band
(3500000, 4000000), # 80M Band
(5167500, 5450000), # 60M Band
(7000000, 7300000), # 40M Band
(10100000, 10150000), # 30M Band
(14000000, 14350000), # 20M Band
(18068000, 18168000), # 17M Band
(21000000, 21450000), # 15M Band
(24890000, 24990000), # 12M Band
(28000000, 29700000), # 10M Band
(50000000, 54000000)] # 6M Band
_tsteps = [0.5, 1.0, 2.5, 5.0, 6.25, 10.0, 12.5,
15.0, 20.0, 25.0, 30.0, 50.0, 100.0]
def get_features(self):
rf = chirp_common.RadioFeatures()
rf.can_odd_split = False
rf.has_bank = False
rf.has_ctone = True
rf.has_dtcs = False
rf.has_dtcs_polarity = False
rf.has_name = True
rf.has_settings = False
rf.has_offset = True
rf.has_mode = True
rf.has_tuning_step = True
rf.has_nostep_tuning = True
rf.has_cross = True
rf.has_comment = False
rf.memory_bounds = (0, self._upper)
rf.valid_bands = self._bands
rf.valid_characters = chirp_common.CHARSET_UPPER_NUMERIC + "*+-/"
rf.valid_duplexes = ["", "-", "+"]
rf.valid_modes = self._modes
rf.valid_skips = ["", "S"]
rf.valid_tmodes = ["", "Tone", "TSQL", "Cross"]
rf.valid_cross_modes = ["Tone->Tone", "->Tone"]
rf.valid_name_length = 8 # 8 character channel names
rf.valid_tuning_steps = self._tsteps
return rf
def _my_val_list(setting, opts, obj, atrb):
"""Callback:from ValueList. Set the integer index."""
value = opts.index(str(setting.value))
setattr(obj, atrb, value)
return
def get_memory(self, number):
"""Convert ascii channel data spec into UI columns (mem)"""
mem = chirp_common.Memory()
# Read the base and split MR strings
mem.number = number
spec0 = self.command(self.pipe, "MR0%03i" % mem.number)
spec1 = self.command(self.pipe, "MR1%03i" % mem.number)
# Add 1 to string idecis if referring to CAT manual
mem.name = spec0[41:49] # Max 8-Char Name if assigned
mem.name = mem.name.strip()
mem.name = mem.name.upper()
_p4 = int(spec0[6:17]) # Rx Frequency
_p4s = int(spec1[6:17]) # Offset freq (Tx)
_p5 = int(spec0[17]) # Mode
_p6 = int(spec0[18]) # Chan Lockout (Skip)
_p7 = int(spec0[19]) # Tone Mode
_p8 = int(spec0[20:22]) # Tone Frequency Index
_p9 = int(spec0[22:24]) # CTCSS Frequency Index
_p14 = int(spec0[38:40]) # Tune Step
if _p4 == 0:
mem.empty = True
return mem
mem.empty = False
mem.freq = _p4
mem.duplex = self._duplex[0] # None by default
mem.offset = 0
if _p4 < _p4s: # + shift
mem.duplex = self._duplex[2]
mem.offset = _p4s - _p4
if _p4 > _p4s: # - shift
mem.duplex = self._duplex[1]
mem.offset = _p4 - _p4s
mx = _p5 - 1 # CAT modes start at 1
mem.mode = self._modes[mx]
mem.tmode = ""
mem.cross_mode = "Tone->Tone"
mem.ctone = self._kenwood_valid_tones[_p9]
mem.rtone = self._kenwood_valid_tones[_p8]
if _p7 == 1:
mem.tmode = "Tone"
elif _p7 == 2:
mem.tmode = "TSQL"
elif _p7 == 3:
mem.tmode = "Cross"
mem.skip = self._skip[_p6]
# Tuning step depends on mode
options = [0.5, 1.0, 2.5, 5.0, 10.0] # SSB/CS/FSK
if _p14 == 4 or _p14 == 5: # AM/FM
options = self._tsteps[3:]
mem.tuning_step = options[_p14]
return mem
def erase_memory(self, number):
mem = chirp_common.Memory()
mem.empty = True
mem.freq = 0
mem.offset = 0
spx = "MW0%03i00000000000000000000000000000000000" % number
rx = self.command(self.pipe, spx) # Send MW0
return mem
def set_memory(self, mem):
"""Send UI column data (mem) to radio"""
pfx = "MW0%03i" % mem.number
xtmode = 0
xdata = 0
xrtone = 8
xctone = 8
xskip = 0
xstep = 0
xfreq = mem.freq
if xfreq > 0: # if empty, use those defaults
ix = self._modes.index(mem.mode)
xmode = ix + 1 # stored as CAT values, LSB= 1
if ix == 7: # FSK-R
xmode = 9 # There is no CAT 8
if mem.tmode == "Tone":
xtmode = 1
xrtone = self._kenwood_valid_tones.index(mem.rtone)
if mem.tmode == "TSQL" or mem.tmode == "Cross":
xtmode = 2
if mem.tmode == "Cross":
xtmode = 3
xctone = self._kenwood_valid_tones.index(mem.ctone)
if mem.skip == "S":
xskip = 1
options = [0.5, 1.0, 2.5, 5.0, 10.0] # SSB/CS/FSK
if xmode == 4 or xmode == 5:
options = self._tsteps[3:]
xstep = options.index(mem.tuning_step)
spx = "%011i%1i%1i%1i%02i%02i00000000000000%02i%s" \
% (xfreq, xmode, xskip, xtmode, xrtone,
xctone, xstep, mem.name)
rx = self.command(self.pipe, pfx, spx) # Send MW0
if mem.offset != 0: # Don't send MW1 if empty
pfx = "MW1%03i" % mem.number
xfreq = mem.freq - mem.offset
if mem.duplex == "+":
xfreq = mem.freq + mem.offset
spx = "%011i%1i%1i%1i%02i%02i00000000000000%02i%s" \
% (xfreq, xmode, xskip, xtmode, xrtone,
xctone, xstep, mem.name)
rx = self.command(self.pipe, pfx, spx) # Send MW1
chirp-20261030/chirp/drivers/kg935g.py 0000644 0001750 0001750 00000251016 15266622247 016221 0 ustar hibby hibby # Wouxun KG-935G Driver
# Updated to support the KG-935G Plus
# Updated to support the KG-UV8H
# melvin.terechenok@gmail.com
# Copyright 2019 Pavel Milanes CO7WT
#
# Based on the work of Krystian Struzik
# who figured out the crypt used and made possible the
# Wuoxun KG-UV8D Plus driver, in which this work is based.
#
# 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 .
"""Wouxun KG-935G radio management module"""
import struct
import time
import logging
from chirp import util, chirp_common, bitwise, memmap, errors, directory
from chirp import checksum
from chirp.settings import RadioSetting, RadioSettingGroup, \
RadioSettingValueBoolean, RadioSettingValueList, \
RadioSettingValueInteger, RadioSettingValueString, \
RadioSettingValueFloat, RadioSettingValueMap, RadioSettings, \
InvalidValueError
from chirp import kenwood_tone
LOG = logging.getLogger(__name__)
CMD_ID = 128 # \x80
CMD_END = 129 # \x81
CMD_RD = 130 # \82
CMD_WR = 131 # \83
MEM_VALID = 158
# set up map of addresses to upload to known memory locations only
# Add new regions as new settings are discovered
# 1st entry is start address in hex
# 2nd entry is write size (number of bytes to transfer in decimal (64-MAX))
# 3rd entry is write count
# so 0x00, 64, 512 = start at 0x00 and write 64*512 bytes = 0 - 32768
# NOTE : It looks like the radio MUST have write
# sizes in values of 1,2,4,8,16,32 or 64 bytes if using
# a write count value of > 1.
# OTHERWISE - it appears the radio gets out of sync
# and the memory write is messed up
# for that section
config_map_935G = ( # map address, write size, write count
# (0x00, 64, 512), #- Use for full upload testing
(0x44, 32, 1), # Freq Limits
(0x440, 8, 1), # Area Message
(0x480, 8, 5), # Scan Groups
(0x500, 8, 15), # Call Codes
(0x580, 8, 15), # Call Names
(0x600, 8, 5), # FM Presets
(0x800, 64, 2), # settings
(0x880, 16, 1), # VFO A
(0x8C0, 16, 1), # VFO B
(0x900, 64, 250), # Channel Memory 0-999
(0x4780, 32, 375), # Memory Names 0-999
(0x7670, 8, 125), # Ch Valid bytes 0-999
)
config_map_935H = ( # map address, write size, write count
# (0x00, 64, 512), #- Use for full upload testing
(0x14, 1, 1), # Tx Range Lock
(0x44, 32, 1), # Freq Limits
(0x440, 8, 1), # Area Message
(0x480, 8, 5), # Scan Groups
(0x500, 8, 15), # Call Codes
(0x580, 8, 15), # Call Names
(0x600, 8, 5), # FM Presets
(0x800, 64, 2), # settings
(0x880, 16, 1), # VFO A
(0x8C0, 16, 1), # VFO B
(0x900, 64, 250), # Channel Memory 0-999
(0x4780, 32, 375), # Memory Names 0-999
(0x7670, 8, 125), # Ch Valid bytes 0-999
)
config_map_x20h = ( # map address, write size, write count
# (0x00, 64, 512), #- Use for full upload testing
# (0x44, 32, 1), # Freq Limits
# (0x440, 8, 1), # Area Message
# (0x480, 8, 5), # Scan Groups
# (0x500, 8, 15), # Call Codes
# (0x580, 8, 15), # Call Names
# (0x600, 8, 5), # FM Presets
# (0x800, 64, 2), # settings
# (0x880, 16, 1), # VFO A
# (0x8C0, 16, 1), # VFO B
(0x900, 64, 250), # Channel Memory 0-999
(0x4780, 32, 375), # Memory Names 0-999
(0x7670, 8, 125), # Ch Valid bytes 0-999
)
AB_LIST = ["A", "B"]
STEPS = [2.5, 5.0, 6.25, 10.0, 12.5, 25.0, 50.0, 100.0]
STEP_LIST = [str(x) for x in STEPS]
ROGER_LIST = ["Off", "Begin", "End", "Both"]
TIMEOUT_LIST = ["Off"] + [str(x) + "s" for x in range(15, 901, 15)]
VOX_LIST = ["Off"] + ["%s" % x for x in range(1, 10)]
BANDWIDTH_LIST = ["Narrow", "Wide"]
VOICE_LIST = ["Off", "On"]
LANGUAGE_LIST = ["Chinese", "English", "Traditional Chinese"]
SCANMODE_LIST = ["TO", "CO", "SE"]
PFKEYLONG_LIST = ["undef", "FRQ2-PTT", "Selec Call", "Scan", "Flashlight",
"Alarm", "SOS", "FM Radio", "Moni", "Strobe", "Weather",
"Tlk A", "Reverse", "CTC Scan", "DCS Scan", "BRT"]
PFKEYSHORT_LIST = ["undef", "Scan", "Flashlight", "Alarm", "SOS", "FM Radio",
"Moni", "Strobe", "Weather", "Tlk A", "Reverse",
"CTC Scan", "DCS Scan", "BRT"]
PFKEYLONG_LIST_UV8H = ["Selec Call", "Sub Freq Tx"]
PFKEYSHORT_LIST_UV8H = ["undef", "Scan", "Lamp", "Alarm", "SOS", "Radio"]
PFKEYLONG_LIST_935GPLUS = ["undef", "FRQ2-PTT", "Selec Call", "Favorite",
"Bright+", "Scan", "Flashlight", "Alarm", "SOS",
"FM Radio", "Moni", "Strobe", "Weather", "Tlk A",
"Reverse", "CTC Scan", "DCS Scan", "Backlight"]
PFKEYSHORT_LIST_935GPLUS = ["undef", "Favorite", "Bright+", "Scan",
"Flashlight", "Alarm", "SOS", "FM Radio",
"Moni", "Strobe", "Weather", "Tlk A", "Reverse",
"CTC Scan", "DCS Scan", "Backlight"]
WORKMODE_LIST = ["VFO", "Ch.Number.", "Ch.Freq.", "Ch.Name"]
WORKMODE_LIST_935GPLUS = ["FREQ", "Ch.Number.", "Ch.Freq.", "Ch.Name"]
BACKLIGHT_LIST = ["Always On"] + [str(x) + "s" for x in range(1, 21)] + \
["Always Off"]
OFFSET_LIST = ["Off", "Plus Shift", "Minus Shift"]
PONMSG_LIST = ["MSG - Bitmap", "Battery Volts"]
SPMUTE_LIST = ["QT", "QT+DTMF", "QT*DTMF"]
DTMFST_LIST = ["OFF", "DTMF", "ANI", "DTMF+ANI"]
DTMF_TIMES = [('%dms' % dtmf, (dtmf // 10)) for dtmf in range(50, 501, 10)]
ALERTS = [1750, 2100, 1000, 1450]
ALERTS_LIST = [str(x) for x in ALERTS]
PTTID_LIST = ["BOT", "EOT", "Both"]
LIST_10 = ["Off"] + ["%s" % x for x in range(1, 11)]
SCANGRP_LIST = ["All"] + ["%s" % x for x in range(1, 11)]
SCQT_LIST = ["Decoder", "Encoder", "All"]
SMUTESET_LIST = ["Off", "Tx", "Rx", "Tx+Rx"]
POWER_LIST = ["Lo", "Mid", "Hi"]
HOLD_TIMES = ["Off"] + ["%s" % x for x in range(100, 5001, 100)]
RPTMODE_LIST = ["Radio", "Repeater"]
RPTTYPE_MAP = [("X-DIRPT", 1), ("X-TWRPT", 2)]
CALLGROUP_LIST = [str(x) for x in range(1, 21)]
THEME_LIST = ["White-1", "White-2", "Black-1", "Black-2"]
THEME_LIST_935GPLUS = ["White-1", "White-2", "Black-1", "Black-2",
"Cool", "Rain", "NotARubi", "Sky", "BTWR", "Candy"]
DSPBRTSBY_LIST = ["OFF", "1", "2", "3", "4", "5", "6", "7", "8", "9", "10"]
DSPBRTACT_MAP = [("1", 1), ("2", 2), ("3", 3), ("4", 4), ("5", 5),
("6", 6), ("7", 7), ("8", 8), ("9", 9), ("10", 10)]
TONESCANSAVELIST = ["Rx", "Tx", "Tx/Rx"]
PTTDELAY_TIMES = [('%dms' % pttdelay,
(pttdelay // 100)) for pttdelay in range(100, 3001, 100)]
SCRAMBLE_LIST = ["OFF"] + [str(x) for x in range(1, 9)]
ONOFF_LIST = ["OFF", "ON"]
# MRT - Map CTCSS Tones - Value in mem is hex value of
# (ctcss tone * 10) + 0x8000
# MRT - 0x8000 is for CTCSS tones
# MRT - Map DCS Tones - Value in mem is hex representation of
# DCS Tone# in Octal + either 0x4000 or 0x6000 for polarity
# MRT - 0x4000 is for DCS n tones
# MRT - 0x6000 is for DCS i tones
TONE_MAP = [('Off', 0x0000)] + \
[('%.1f' % tone,
int(0x8000 + tone * 10)) for tone in chirp_common.TONES] + \
[('D%03dn' % tone, int(0x4000 + int(str(tone), 8)))
for tone in chirp_common.DTCS_CODES] + \
[('D%03di' % tone, int(0x6000 + int(str(tone), 8)))
for tone in chirp_common.DTCS_CODES]
BATT_DISP_LIST = ["Icon", "Voltage", "Percent"]
WX_TYPE = ["Weather", "Icon-Only", "Tone", "Flash", "Tone-Flash"]
# memory slot 0 is not used, start at 1 (so need 1000 slots, not 999)
# structure elements whose name starts with x are currently unidentified
_MEM_FORMAT_935G = """
#seekto 0x0044;
struct {
u32 rx_start;
u32 rx_stop;
u32 tx_start;
u32 tx_stop;
} uhf_limits;
#seekto 0x0054;
struct {
u32 rx_start;
u32 rx_stop;
u32 tx_start;
u32 tx_stop;
} vhf_limits;
#seekto 0x0400;
struct {
char oem1[8];
char unknown[2];
u8 unknown2[10];
u8 unknown3[10];
u8 unknown4[8];
char oem2[10];
u8 version[6];
char date[8];
u8 unknown5[2];
char model[8];
} oem_info;
#seekto 0x0480;
struct {
u16 Group_lower1;
u16 Group_upper1;
u16 Group_lower2;
u16 Group_upper2;
u16 Group_lower3;
u16 Group_upper3;
u16 Group_lower4;
u16 Group_upper4;
u16 Group_lower5;
u16 Group_upper5;
u16 Group_lower6;
u16 Group_upper6;
u16 Group_lower7;
u16 Group_upper7;
u16 Group_lower8;
u16 Group_upper8;
u16 Group_lower9;
u16 Group_upper9;
u16 Group_lower10;
u16 Group_upper10;
} scan_groups;
#seekto 0x0500;
struct {
u8 cid[6];
} call_ids[20];
#seekto 0x0580;
struct {
char call_name1[6];
char call_name2[6];
char call_name3[6];
char call_name4[6];
char call_name5[6];
char call_name6[6];
char call_name7[6];
char call_name8[6];
char call_name9[6];
char call_name10[6];
char call_name11[6];
char call_name12[6];
char call_name13[6];
char call_name14[6];
char call_name15[6];
char call_name16[6];
char call_name17[6];
char call_name18[6];
char call_name19[6];
char call_name20[6];
} call_names;
#seekto 0x0600;
struct {
u16 FM_radio1;
u16 FM_radio2;
u16 FM_radio3;
u16 FM_radio4;
u16 FM_radio5;
u16 FM_radio6;
u16 FM_radio7;
u16 FM_radio8;
u16 FM_radio9;
u16 FM_radio10;
u16 FM_radio11;
u16 FM_radio12;
u16 FM_radio13;
u16 FM_radio14;
u16 FM_radio15;
u16 FM_radio16;
u16 FM_radio17;
u16 FM_radio18;
u16 FM_radio19;
u16 FM_radio20;
u16 unknown_pad_x0640[235];
u8 unknown07fe;
u8 unknown07ff;
u8 ponmsg;
char dispstr[15];
u8 unknown0810;
u8 unknown0811;
u8 unknown0812;
u8 unknown0813;
u8 unknown0814;
u8 voice;
u8 timeout;
u8 toalarm;
u8 channel_menu;
u8 power_save;
u8 autolock;
u8 keylock;
u8 beep;
u8 stopwatch;
u8 vox;
u8 scan_rev;
u8 backlight;
u8 roger_beep;
char mode_sw_pwd[6];
char reset_pwd[6];
u16 pri_ch;
u8 ani_sw;
u8 ptt_delay;
u8 ani_code[6];
u8 dtmf_st;
u8 BCL_A;
u8 BCL_B;
u8 ptt_id;
u8 prich_sw;
u8 unknown083d;
u8 unknown083e;
u8 unknown083f;
u8 alert;
u8 pf1_shrt;
u8 pf1_long;
u8 pf2_shrt;
u8 pf2_long;
u8 unknown0845;
u8 work_mode_a;
u8 work_mode_b;
u8 dtmf_tx_time;
u8 dtmf_interval;
u8 main_band;
u16 work_ch_a;
u16 work_ch_b;
u8 unknown084f;
u8 unknown0850;
u8 unknown0851;
u8 unknown0852;
u8 unknown0853;
u8 unknown0854;
u8 unknown0855;
u8 unknown0856;
u8 unknown0857;
u8 unknown0858;
u8 unknown0859;
u8 unknown085a;
u8 unknown085b;
u8 unknown085c;
u8 unknown085d;
u8 unknown085e;
u8 unknown085f;
u8 unknown0860;
u8 TDR_single_mode;
u8 ring_time;
u8 ScnGrpA_Act;
u8 ScnGrpB_Act;
u8 unknown0865;
u8 rpt_tone;
u8 unknown0867;
u8 scan_det;
u8 ToneScnSave;
u8 unknown086a;
u8 smuteset;
u8 cur_call_grp;
u8 DspBrtAct;
u8 DspBrtSby;
u8 unknown086f;
u8 theme;
u8 wxalert;
u8 VFO_repeater_a;
u8 VFO_repeater_b;
u8 unknown0874;
u8 unknown0875;
u8 unknown0876;
u8 unknown0877;
u8 unknown0878;
u8 unknown0879;
u8 unknown087a;
u8 unknown087b;
u8 unknown087c;
u8 unknown087d;
u8 unknown087e;
u8 unknown087f;
} settings;
#seekto 0x0880;
struct {
u32 rxfreq;
u32 unknown0;
u16 rxtone;
u16 txtone;
u8 scrambler:4,
power:4;
u8 unknown3:1,
unknown5:2,
unknown4:1,
cmpndr:1,
mute_mode:2,
iswide:1;
u8 step;
u8 squelch;
} vfoa;
#seekto 0x08c0;
struct {
u32 rxfreq;
u32 unknown0;
u16 rxtone;
u16 txtone;
u8 scrambler:4,
power:4;
u8 unknown3:1,
unknown5:2,
unknown4:1,
cmpndr:1,
mute_mode:2,
iswide:1;
u8 step;
u8 squelch;
} vfob;
#seekto 0x0900;
struct {
u32 rxfreq;
u32 txfreq;
u16 rxtone;
u16 txtone;
u8 scrambler:4,
power:4;
u8 unknown3:2,
scan_add:1,
unknown4:1,
compander:1,
mute_mode:2,
iswide:1;
u8 unknown5;
u8 unknown6;
} memory[1000];
#seekto 0x4780;
struct {
u8 name[8];
u8 unknown[4];
} names[1000];
#seekto 0x7670;
u8 valid[1000];
"""
_MEM_FORMAT_935GPLUS = """
#seekto 0x0044;
struct {
u32 rx_start;
u32 rx_stop;
u32 tx_start;
u32 tx_stop;
} uhf_limits;
#seekto 0x0054;
struct {
u32 rx_start;
u32 rx_stop;
u32 tx_start;
u32 tx_stop;
} vhf_limits;
#seekto 0x0400;
struct {
char oem1[8];
char unknown[2];
u8 unknown2[10];
u8 unknown3[10];
u8 unknown4[8];
char oem2[10];
u8 version[6];
char date[8];
u8 unknown5[2];
char model[8];
} oem_info;
#seekto 0x0480;
struct {
u16 Group_lower1;
u16 Group_upper1;
u16 Group_lower2;
u16 Group_upper2;
u16 Group_lower3;
u16 Group_upper3;
u16 Group_lower4;
u16 Group_upper4;
u16 Group_lower5;
u16 Group_upper5;
u16 Group_lower6;
u16 Group_upper6;
u16 Group_lower7;
u16 Group_upper7;
u16 Group_lower8;
u16 Group_upper8;
u16 Group_lower9;
u16 Group_upper9;
u16 Group_lower10;
u16 Group_upper10;
} scan_groups;
#seekto 0x0500;
struct {
u8 cid[6];
} call_ids[20];
#seekto 0x0580;
struct {
char call_name1[6];
char call_name2[6];
char call_name3[6];
char call_name4[6];
char call_name5[6];
char call_name6[6];
char call_name7[6];
char call_name8[6];
char call_name9[6];
char call_name10[6];
char call_name11[6];
char call_name12[6];
char call_name13[6];
char call_name14[6];
char call_name15[6];
char call_name16[6];
char call_name17[6];
char call_name18[6];
char call_name19[6];
char call_name20[6];
} call_names;
#seekto 0x0600;
struct {
u16 FM_radio1;
u16 FM_radio2;
u16 FM_radio3;
u16 FM_radio4;
u16 FM_radio5;
u16 FM_radio6;
u16 FM_radio7;
u16 FM_radio8;
u16 FM_radio9;
u16 FM_radio10;
u16 FM_radio11;
u16 FM_radio12;
u16 FM_radio13;
u16 FM_radio14;
u16 FM_radio15;
u16 FM_radio16;
u16 FM_radio17;
u16 FM_radio18;
u16 FM_radio19;
u16 FM_radio20;
u16 unknown_pad_x0640[235];
u8 unknown07fe;
u8 unknown07ff;
u8 ponmsg;
char dispstr[15];
u8 unknown0810;
u8 unknown0811;
u8 unknown0812;
u8 unknown0813;
u8 unknown0814;
u8 voice;
u8 timeout;
u8 toalarm;
u8 channel_menu;
u8 power_save;
u8 autolock;
u8 keylock;
u8 beep;
u8 stopwatch;
u8 vox;
u8 scan_rev;
u8 backlight;
u8 roger_beep;
char mode_sw_pwd[6];
char reset_pwd[6];
u16 pri_ch;
u8 ani_sw;
u8 ptt_delay;
u8 ani_code[6];
u8 dtmf_st;
u8 BCL_A;
u8 BCL_B;
u8 ptt_id;
u8 prich_sw;
u8 unknown083d;
u8 unknown083e;
u8 unknown083f;
u8 alert;
u8 pf1_shrt;
u8 pf1_long;
u8 pf2_shrt;
u8 pf2_long;
u8 unknown0845;
u8 work_mode_a;
u8 work_mode_b;
u8 dtmf_tx_time;
u8 dtmf_interval;
u8 main_band;
u16 work_ch_a;
u16 work_ch_b;
u8 unknown084f;
u8 unknown0850;
u8 unknown0851;
u8 unknown0852;
u8 unknown0853;
u8 unknown0854;
u8 unknown0855;
u8 unknown0856;
u8 unknown0857;
u8 unknown0858;
u8 unknown0859;
u8 unknown085a;
u8 unknown085b;
u8 unknown085c;
u8 unknown085d;
u8 unknown085e;
u8 unknown085f;
u8 unknown0860;
u8 TDR_single_mode;
u8 ring_time;
u8 ScnGrpA_Act;
u8 ScnGrpB_Act;
u8 unknown0865;
u8 rpt_tone;
u8 unknown0867;
u8 scan_det;
u8 ToneScnSave;
u8 unknown086a;
u8 smuteset;
u8 cur_call_grp;
u8 DspBrtAct;
u8 DspBrtSby;
u8 unknown086f;
u8 theme;
u8 batt_ind;
u8 wxalert;
u8 wxalert_type;
u8 VFO_repeater_a;
u8 VFO_repeater_b;
u8 sim_rec;
u8 unknown0877;
u8 unknown0878;
u8 unknown0879;
u8 unknown087a;
u8 unknown087b;
u8 unknown087c;
u8 unknown087d;
u8 unknown087e;
u8 unknown087f;
} settings;
#seekto 0x0880;
struct {
u32 rxfreq;
u32 unknown0;
u16 rxtone;
u16 txtone;
u8 scrambler:4,
power:4;
u8 unknown3:1,
unknown5:2,
unknown4:1,
cmpndr:1,
mute_mode:2,
iswide:1;
u8 step;
u8 squelch;
} vfoa;
#seekto 0x08c0;
struct {
u32 rxfreq;
u32 unknown0;
u16 rxtone;
u16 txtone;
u8 scrambler:4,
power:4;
u8 unknown3:1,
unknown5:2,
unknown4:1,
cmpndr:1,
mute_mode:2,
iswide:1;
u8 step;
u8 squelch;
} vfob;
#seekto 0x0900;
struct {
u32 rxfreq;
u32 txfreq;
u16 rxtone;
u16 txtone;
u8 scrambler:4,
power:4;
u8 unknown3:2,
scan_add:1,
favorite:1,
compander:1,
mute_mode:2,
iswide:1;
u8 unknown5;
u8 unknown6;
} memory[1000];
#seekto 0x4780;
struct {
u8 name[8];
u8 unknown[4];
} names[1000];
#seekto 0x7670;
u8 valid[1000];
"""
_MEM_FORMAT_UV8H = """
#seekto 0x0044;
struct {
u32 rx_start;
u32 rx_stop;
u32 tx_start;
u32 tx_stop;
} uhf_limits;
#seekto 0x0054;
struct {
u32 rx_start;
u32 rx_stop;
u32 tx_start;
u32 tx_stop;
} vhf_limits;
#seekto 0x0400;
struct {
char oem1[8];
char unknown[2];
u8 unknown2[10];
u8 unknown3[10];
u8 unknown4[8];
char oem2[10];
u8 version[6];
char date[8];
u8 unknown5[2];
char model[8];
} oem_info;
#seekto 0x0480;
struct {
u16 Group_lower1;
u16 Group_upper1;
u16 Group_lower2;
u16 Group_upper2;
u16 Group_lower3;
u16 Group_upper3;
u16 Group_lower4;
u16 Group_upper4;
u16 Group_lower5;
u16 Group_upper5;
u16 Group_lower6;
u16 Group_upper6;
u16 Group_lower7;
u16 Group_upper7;
u16 Group_lower8;
u16 Group_upper8;
u16 Group_lower9;
u16 Group_upper9;
u16 Group_lower10;
u16 Group_upper10;
} scan_groups;
#seekto 0x0500;
struct {
u8 cid[6];
} call_ids[20];
#seekto 0x0580;
struct {
char call_name1[6];
char call_name2[6];
char call_name3[6];
char call_name4[6];
char call_name5[6];
char call_name6[6];
char call_name7[6];
char call_name8[6];
char call_name9[6];
char call_name10[6];
char call_name11[6];
char call_name12[6];
char call_name13[6];
char call_name14[6];
char call_name15[6];
char call_name16[6];
char call_name17[6];
char call_name18[6];
char call_name19[6];
char call_name20[6];
} call_names;
#seekto 0x0600;
struct {
u16 FM_radio1;
u16 FM_radio2;
u16 FM_radio3;
u16 FM_radio4;
u16 FM_radio5;
u16 FM_radio6;
u16 FM_radio7;
u16 FM_radio8;
u16 FM_radio9;
u16 FM_radio10;
u16 FM_radio11;
u16 FM_radio12;
u16 FM_radio13;
u16 FM_radio14;
u16 FM_radio15;
u16 FM_radio16;
u16 FM_radio17;
u16 FM_radio18;
u16 FM_radio19;
u16 FM_radio20;
u16 unknown_pad_x0640[235];
u8 unknown07fe;
u8 unknown07ff;
u8 ponmsg;
char dispstr[15];
u8 unknown0810;
u8 unknown0811;
u8 unknown0812;
u8 unknown0813;
u8 unknown0814;
u8 voice;
u8 timeout;
u8 toalarm;
u8 channel_menu;
u8 power_save;
u8 autolock;
u8 keylock;
u8 beep;
u8 stopwatch;
u8 vox;
u8 scan_rev;
u8 backlight;
u8 roger_beep;
char mode_sw_pwd[6];
char reset_pwd[6];
u16 pri_ch;
u8 ani_sw;
u8 ptt_delay;
u8 ani_code[6];
u8 dtmf_st;
u8 BCL_A;
u8 BCL_B;
u8 ptt_id;
u8 prich_sw;
u8 rpttype;
u8 rptspk;
u8 rptptt;
u8 alert;
u8 pf1_long;
u8 pf1_shrt;
u8 unk845;
u8 work_mode_a;
u8 work_mode_b;
u8 dtmf_tx_time;
u8 dtmf_interval;
u8 unk4a;
u16 work_ch_a;
u16 work_ch_b;
u8 unk4d;
u8 main_band;
u8 unknown084f;
u8 unknown0850;
u8 unknown0851;
u8 unknown0852;
u8 unknown0853;
u8 unknown0854;
u8 rptmode;
u8 language;
u8 unknown0857;
u8 unknown0858;
u8 unknown0859;
u8 unknown085a;
u8 unknown085b;
u8 unknown085c;
u8 unknown085d;
u8 unknown085e;
u8 TDR_single_mode;
u8 ring_time;
u8 ScnGrpA_Act;
u8 ScnGrpB_Act;
u8 unk863;
u8 rpt_tone;
u8 rpt_hold;
u8 scan_det;
u8 ToneScnSave;
u8 unk868;
u8 smuteset;
u8 cur_call_grp;
u8 DspBrtAct;
u8 unknown086c;
u8 theme;
u8 wxalert;
u8 unknown086f;
u8 unknown0870;
u8 unk871;
u8 unk872;
u8 unk873;
u8 unknown0874;
u8 unknown0875;
u8 unknown0876;
u8 unknown0877;
u8 unknown0878;
u8 unknown0879;
u8 unknown087a;
u8 unknown087b;
u8 unknown087c;
u8 unknown087d;
u8 unknown087e;
u8 unknown087f;
} settings;
#seekto 0x0880;
struct {
u32 rxfreq;
u32 offset;
u16 rxtone;
u16 txtone;
u8 scrambler:4,
power:4;
u8 unknown3:1,
ofst_dir:2,
unknown4:1,
cmpndr:1,
mute_mode:2,
iswide:1;
u8 step;
u8 squelch;
} vfoa;
#seekto 0x08c0;
struct {
u32 rxfreq;
u32 offset;
u16 rxtone;
u16 txtone;
u8 scrambler:4,
power:4;
u8 unknown3:1,
ofst_dir:2,
unknown4:1,
cmpndr:1,
mute_mode:2,
iswide:1;
u8 step;
u8 squelch;
} vfob;
#seekto 0x0900;
struct {
u32 rxfreq;
u32 txfreq;
u16 rxtone;
u16 txtone;
u8 scrambler:4,
power:4;
u8 unknown3:2,
scan_add:1,
unknown4:1,
compander:1,
mute_mode:2,
iswide:1;
u8 unknown5;
u8 unknown6;
} memory[1000];
#seekto 0x4780;
struct {
u8 name[8];
u8 unknown[4];
} names[1000];
#seekto 0x7670;
u8 valid[1000];
"""
_MEM_FORMAT_935H = """
#seekto 0x0014;
struct {
u8 txlocked;
} lock;
#seekto 0x0044;
struct {
u32 rx_start;
u32 rx_stop;
u32 tx_start;
u32 tx_stop;
} uhf_limits;
#seekto 0x0054;
struct {
u32 rx_start;
u32 rx_stop;
u32 tx_start;
u32 tx_stop;
} vhf_limits;
#seekto 0x0400;
struct {
char oem1[8];
char unknown[2];
u8 unknown2[10];
u8 unknown3[10];
u8 unknown4[8];
char oem2[10];
u8 version[6];
char date[8];
u8 unknown5[2];
char model[8];
} oem_info;
#seekto 0x0480;
struct {
u16 Group_lower1;
u16 Group_upper1;
u16 Group_lower2;
u16 Group_upper2;
u16 Group_lower3;
u16 Group_upper3;
u16 Group_lower4;
u16 Group_upper4;
u16 Group_lower5;
u16 Group_upper5;
u16 Group_lower6;
u16 Group_upper6;
u16 Group_lower7;
u16 Group_upper7;
u16 Group_lower8;
u16 Group_upper8;
u16 Group_lower9;
u16 Group_upper9;
u16 Group_lower10;
u16 Group_upper10;
} scan_groups;
#seekto 0x0500;
struct {
u8 cid[6];
} call_ids[20];
#seekto 0x0580;
struct {
char call_name1[6];
char call_name2[6];
char call_name3[6];
char call_name4[6];
char call_name5[6];
char call_name6[6];
char call_name7[6];
char call_name8[6];
char call_name9[6];
char call_name10[6];
char call_name11[6];
char call_name12[6];
char call_name13[6];
char call_name14[6];
char call_name15[6];
char call_name16[6];
char call_name17[6];
char call_name18[6];
char call_name19[6];
char call_name20[6];
} call_names;
#seekto 0x0600;
struct {
u16 FM_radio1;
u16 FM_radio2;
u16 FM_radio3;
u16 FM_radio4;
u16 FM_radio5;
u16 FM_radio6;
u16 FM_radio7;
u16 FM_radio8;
u16 FM_radio9;
u16 FM_radio10;
u16 FM_radio11;
u16 FM_radio12;
u16 FM_radio13;
u16 FM_radio14;
u16 FM_radio15;
u16 FM_radio16;
u16 FM_radio17;
u16 FM_radio18;
u16 FM_radio19;
u16 FM_radio20;
u16 unknown_pad_x0640[235];
u8 unknown07fe;
u8 unknown07ff;
u8 ponmsg;
char dispstr[15];
u8 unknown0810;
u8 unknown0811;
u8 unknown0812;
u8 unknown0813;
u8 unknown0814;
u8 voice;
u8 timeout;
u8 toalarm;
u8 channel_menu;
u8 power_save;
u8 autolock;
u8 keylock;
u8 beep;
u8 stopwatch;
u8 vox;
u8 scan_rev;
u8 backlight;
u8 roger_beep;
char mode_sw_pwd[6];
char reset_pwd[6];
u16 pri_ch;
u8 ani_sw;
u8 ptt_delay;
u8 ani_code[6];
u8 dtmf_st;
u8 BCL_A;
u8 BCL_B;
u8 ptt_id;
u8 prich_sw;
u8 rpttype;
u8 rptspk;
u8 rptptt;
u8 alert;
u8 pf1_shrt;
u8 pf1_long;
u8 pf2_shrt;
u8 pf2_long;
u8 unknown0845;
u8 work_mode_a;
u8 work_mode_b;
u8 dtmf_tx_time;
u8 dtmf_interval;
u8 main_band;
u16 work_ch_a;
u16 work_ch_b;
u8 unknown084f;
u8 unknown0850;
u8 unknown0851;
u8 unknown0852;
u8 unknown0853;
u8 unknown0854;
u8 unknown0855;
u8 unknown0856;
u8 unknown0857;
u8 unknown0858;
u8 unknown0859;
u8 unknown085a;
u8 unknown085b;
u8 unknown085c;
u8 unknown085d;
u8 unknown085e;
u8 unknown085f;
u8 unknown0860;
u8 TDR_single_mode;
u8 ring_time;
u8 ScnGrpA_Act;
u8 ScnGrpB_Act;
u8 unknown0865;
u8 rpt_tone;
u8 unknown0867;
u8 scan_det;
u8 ToneScnSave;
u8 unknown086a;
u8 smuteset;
u8 cur_call_grp;
u8 DspBrtAct;
u8 DspBrtSby;
u8 unknown086f;
u8 theme;
u8 batt_ind;
u8 wxalert;
u8 wxalert_type;
u8 VFO_repeater_a;
u8 VFO_repeater_b;
u8 sim_rec;
u8 unknown0877;
u8 unknown0878;
u8 unknown0879;
u8 unknown087a;
u8 unknown087b;
u8 unknown087c;
u8 unknown087d;
u8 unknown087e;
u8 unknown087f;
} settings;
#seekto 0x0880;
struct {
u32 rxfreq;
u32 unknown0;
u16 rxtone;
u16 txtone;
u8 scrambler:4,
power:4;
u8 unknown3:1,
unknown5:2,
unknown4:1,
cmpndr:1,
mute_mode:2,
iswide:1;
u8 step;
u8 squelch;
} vfoa;
#seekto 0x08c0;
struct {
u32 rxfreq;
u32 unknown0;
u16 rxtone;
u16 txtone;
u8 scrambler:4,
power:4;
u8 unknown3:1,
unknown5:2,
unknown4:1,
cmpndr:1,
mute_mode:2,
iswide:1;
u8 step;
u8 squelch;
} vfob;
#seekto 0x0900;
struct {
u32 rxfreq;
u32 txfreq;
u16 rxtone;
u16 txtone;
u8 scrambler:4,
power:4;
u8 unknown3:2,
scan_add:1,
favorite:1,
compander:1,
mute_mode:2,
iswide:1;
u8 unknown5;
u8 unknown6;
} memory[1000];
#seekto 0x4780;
struct {
u8 name[8];
u8 unknown[4];
} names[1000];
#seekto 0x7670;
u8 valid[1000];
"""
# Support for the Wouxun KG-935G radio
# Serial coms are at 19200 baud
# The data is passed in variable length records
# Record structure:
# Offset Usage
# 0 start of record (\x7c)
# 1 Command (\x80 Identify \x81 End/Reboot \x82 Read \x83 Write)
# 2 direction (\xff PC-> Radio, \x00 Radio -> PC)
# 3 length of payload (excluding header/checksum) (n)
# 4 payload (n bytes)
# 4+n+1 checksum - byte sum (% 256) of bytes 1 -> 4+n
#
# Memory Read Records:
# the payload is 3 bytes, first 2 are offset (big endian),
# 3rd is number of bytes to read
# Memory Write Records:
# the maximum payload size (from the Wouxun software) seems to be 66 bytes
# (2 bytes location + 64 bytes data).
# MRT 1.2 correct spelling of Wouxon
def str2callid(val):
""" Convert caller id strings from callid2str.
"""
ascii2bin = "0123456789"
s = str(val).strip()
LOG.debug("val = %s" % val)
LOG.debug("s = %s" % s)
if len(s) < 3 or len(s) > 6:
raise InvalidValueError(
"Caller ID must be at least 3 and no more than 6 digits")
if s[0] == '0':
raise InvalidValueError(
"First digit of a Caller ID cannot be a zero '0'")
blk = bytearray()
for c in s:
if c not in ascii2bin:
raise InvalidValueError(
"Caller ID must be all digits 0x%x" % c)
b = ascii2bin.index(c)
blk.append(b)
if len(blk) < 6:
blk.append(0x0F) # EOL a short ID
if len(blk) < 6:
for i in range(0, (6 - len(blk))):
blk.append(0xf0)
LOG.debug("blk = %s" % blk)
return blk
@directory.register
class KG935GRadio(chirp_common.CloneModeRadio,
chirp_common.ExperimentalRadio):
"""Wouxun KG-935G"""
VENDOR = "Wouxun"
MODEL = "KG-935G"
_model = b"KG-UV8D-B"
BAUD_RATE = 19200
# MRT - Added Medium Power level for 935G support
POWER_LEVELS = [chirp_common.PowerLevel("L", watts=0.5),
chirp_common.PowerLevel("M", watts=4.5),
chirp_common.PowerLevel("H", watts=5.5)]
_record_start = 0x7C
config_map = config_map_935G
HAS_SCRAMBLER = True
HAS_FAVORITE = False
def __init__(self, pipe):
super().__init__(pipe)
self.tone_model = kenwood_tone.KenwoodToneModel(
dcs_base=0x4000,
pol_mask=0x2000,
tone_init=0x0000)
def get_extra(self, _mem, mem):
mem.extra = RadioSettingGroup("Extra", "Extra")
_mem.mute_mode = 2 if _mem.mute_mode > 2 else _mem.mute_mode
rs = RadioSetting("mute_mode", "Mute Mode",
RadioSettingValueList(
SPMUTE_LIST, current_index=_mem.mute_mode))
mem.extra.append(rs)
if self.HAS_SCRAMBLER:
rs = RadioSetting("scrambler", "Scramble Descramble",
RadioSettingValueList(
SCRAMBLE_LIST, current_index=_mem.scrambler))
mem.extra.append(rs)
rs = RadioSetting("compander", "Compander",
RadioSettingValueList(
ONOFF_LIST, current_index=_mem.compander))
mem.extra.append(rs)
if self.HAS_FAVORITE:
rs = RadioSetting("favorite", "Favorite",
RadioSettingValueList(
ONOFF_LIST, current_index=_mem.favorite))
mem.extra.append(rs)
return
def _write_record(self, cmd, payload=b''):
_packet = struct.pack('BBBB', self._record_start, cmd, 0xFF,
len(payload))
cs = bytes([checksum.checksum_8bit(_packet[1:] + payload)])
_packet += self.encrypt(payload + cs)
LOG.debug("Sent:\n%s" % util.hexprint(_packet))
self.pipe.write(_packet)
def _read_record(self):
# read 4 chars for the header
_header = self.pipe.read(4)
if not _header:
raise errors.RadioNoResponse()
if len(_header) != 4:
raise errors.RadioError('Radio sent short header')
_length = struct.unpack('xxxB', _header)[0]
_packet = self.pipe.read(_length)
_rcs_xor = _packet[-1]
_packet = self.decrypt(_packet)
_cs = checksum.checksum_8bit(_header[1:])
_cs += checksum.checksum_8bit(_packet)
_cs %= 256
_rcs = self.strxor(self.pipe.read(1)[0], _rcs_xor)[0]
LOG.debug("_cs =%x", _cs)
LOG.debug("_rcs=%x", _rcs)
return (_rcs != _cs, _packet)
def decrypt(self, data):
result = b''
for i in range(len(data)-1, 0, -1):
result += self.strxor(data[i], data[i - 1])
result += self.strxor(data[0], 0x57)
return result[::-1]
def encrypt(self, data):
result = self.strxor(0x57, data[0])
for i in range(1, len(data), 1):
result += self.strxor(result[i - 1], data[i])
return result
def strxor(self, xora, xorb):
return bytes([xora ^ xorb])
# Identify the radio
#
# A Gotcha: the first identify packet returns a bad checksum, subsequent
# attempts return the correct checksum... (well it does on my radio!)
#
# The ID record returned by the radio also includes the
# current frequency range
# as 4 bytes big-endian in 10 Hz increments
#
# Offset
# 0:10 Model, zero padded (Looks for 'KG-UV8D-B')
def _identify(self):
"""Do the identification dance"""
for _i in range(0, 10):
self._write_record(CMD_ID)
_chksum_err, _resp = self._read_record()
LOG.debug("Got:\n%s" % util.hexprint(_resp))
if _chksum_err:
LOG.error("Checksum error: retrying ident...")
time.sleep(0.100)
continue
LOG.debug("Model %s" % util.hexprint(_resp[0:9]))
if _resp[0:9] == self._model:
return
if len(_resp) == 0:
raise errors.RadioNoResponse()
else:
raise Exception("Unable to identify radio")
def _finish(self):
self._write_record(CMD_END)
def process_mmap(self):
self._memobj = bitwise.parse(_MEM_FORMAT_935G, self._mmap)
def sync_in(self):
try:
self._mmap = self._download()
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):
self._upload()
# TODO: Load all memory.
# It would be smarter to only load the active areas and none of
# the padding/unused areas. Padding still need to be investigated.
def _download(self):
"""Talk to a Wouxun KG-935G and do a download"""
try:
self._identify()
return self._do_download(0, 32768, 64)
except errors.RadioError:
raise
except Exception as e:
LOG.exception('Unknown error during download process')
raise errors.RadioError("Failed to communicate with radio: %s" % e)
def _do_download(self, start, end, blocksize):
# allocate & fill memory
image = b""
for i in range(start, end, blocksize):
req = struct.pack('>HB', i, blocksize)
self._write_record(CMD_RD, req)
cs_error, resp = self._read_record()
if cs_error:
LOG.debug(util.hexprint(resp))
raise Exception("Checksum error on read")
# LOG.debug("Got:\n%s" % util.hexprint(resp))
image += resp[2:]
if self.status_fn:
status = chirp_common.Status()
status.cur = i
status.max = end
status.msg = "Cloning from radio"
self.status_fn(status)
self._finish()
return memmap.MemoryMapBytes(image)
def _upload(self):
"""Talk to a Wouxun KG-935G and do a upload"""
try:
self._identify()
self._do_upload()
except errors.RadioError:
raise
except Exception as e:
raise errors.RadioError("Failed to communicate with radio: %s" % e)
return
def _do_upload(self):
cfgmap = self.config_map
for start, blocksize, count in cfgmap:
end = start + (blocksize * count)
LOG.debug("start = " + str(start))
LOG.debug("end = " + str(end))
LOG.debug("blksize = " + str(blocksize))
for addr in range(start, end, blocksize):
ptr = addr
LOG.debug("ptr = " + str(ptr))
req = struct.pack('>H', addr)
chunk = self.get_mmap()[ptr:ptr + blocksize]
self._write_record(CMD_WR, req + chunk)
LOG.debug(util.hexprint(req + chunk))
cserr, ack = self._read_record()
LOG.debug(util.hexprint(ack))
j = struct.unpack('>H', ack)[0]
if cserr or j != ptr:
raise Exception("Radio did not ack block %i" % ptr)
ptr += blocksize
if self.status_fn:
status = chirp_common.Status()
status.cur = ptr
status.max = 0x8000
status.msg = "Cloning to radio"
self.status_fn(status)
self._finish()
def get_features(self):
rf = chirp_common.RadioFeatures()
rf.has_settings = True
rf.has_ctone = True
rf.has_rx_dtcs = True
rf.has_cross = True
rf.has_tuning_step = False
rf.has_bank = False
rf.can_odd_split = True
rf.valid_skips = ["", "S"]
rf.valid_tmodes = ["", "Tone", "TSQL", "DTCS", "Cross"]
rf.valid_cross_modes = [
"Tone->Tone",
"Tone->DTCS",
"DTCS->Tone",
"DTCS->",
"->Tone",
"->DTCS",
"DTCS->DTCS",
]
rf.valid_modes = ["FM", "NFM"]
rf.valid_power_levels = self.POWER_LEVELS
rf.valid_name_length = 8
rf.valid_duplexes = ["", "-", "+", "split", "off"]
# MRT - Open up channel memory freq range to support
# RxFreq limit expansion
rf.valid_bands = [(30000000, 299999990), # supports 2m
(300000000, 999999990)] # supports 70cm
rf.valid_characters = chirp_common.CHARSET_ASCII
rf.memory_bounds = (1, 999) # 999 memories
rf.valid_tuning_steps = STEPS
return rf
@classmethod
def get_prompts(cls):
rp = chirp_common.RadioPrompts()
rp.experimental = \
('This driver is experimental. USE AT YOUR OWN RISK\n'
'\n'
'Please save a copy of the image from your radio with Chirp '
'before modifying any values.\n'
'\n'
'Please keep a copy of your memories with the original Wouxon'
'CPS software if you treasure them, this driver is new and'
'may contain bugs.\n'
)
return rp
def get_raw_memory(self, number):
return repr(self._memobj.memory[number])
def _get_power(self, _mem, mem):
_mem.power = _mem.power & 0x3
try:
mem.power = self.POWER_LEVELS[_mem.power]
except IndexError:
mem.power = self.POWER_LEVELS[-1]
def _set_power(self, mem):
temp_val = self.POWER_LEVELS.index(mem.power)
return temp_val
def get_memory(self, number):
_mem = self._memobj.memory[number]
_nam = self._memobj.names[number]
mem = chirp_common.Memory()
mem.number = number
_valid = self._memobj.valid[mem.number]
if _valid != MEM_VALID:
mem.empty = True
return mem
else:
mem.empty = False
mem.freq = int(_mem.rxfreq) * 10
tx_disabled = (
_mem.txfreq == 0xFFFFFFFF or
(self.MODEL in ("KG-XS20G", "KG-XS20G Plus", "KG-XS20H") and
int(_mem.txfreq) == 0))
if tx_disabled:
# TX freq not set
mem.duplex = "off"
mem.offset = 0
elif int(_mem.rxfreq) == int(_mem.txfreq):
mem.duplex = ""
mem.offset = 0
elif abs(int(_mem.rxfreq) * 10 - int(_mem.txfreq) * 10) > 70000000:
mem.duplex = "split"
mem.offset = int(_mem.txfreq) * 10
else:
mem.duplex = int(_mem.rxfreq) > int(_mem.txfreq) and "-" or "+"
mem.offset = abs(int(_mem.rxfreq) - int(_mem.txfreq)) * 10
for char in _nam.name:
if char != 0:
mem.name += chr(char)
mem.name = mem.name.rstrip()
self.get_extra(_mem, mem)
self.tone_model.get_tone(_mem, mem)
mem.skip = "" if bool(_mem.scan_add) else "S"
self._get_power(_mem, mem)
mem.mode = _mem.iswide and "FM" or "NFM"
return mem
def set_memory(self, mem):
number = mem.number
# Determine if we are moving from Empty -> Not Empty
was_empty = (self._memobj.valid[number] != MEM_VALID)
_mem = self._memobj.memory[number]
_nam = self._memobj.names[number]
if mem.empty:
_mem.fill_raw(b'\x00')
self._memobj.valid[number] = 0
self._memobj.names[number].fill_raw(b'\x00')
return
# If it's a NEW channel, wipe it to all 0s first
# This prevents "ghost" data from previous radio use or factory junk
if was_empty:
_mem.fill_raw(b'\x00')
_nam.fill_raw(b'\x00')
_mem.rxfreq = int(mem.freq / 10)
if mem.duplex == "off":
_mem.txfreq = 0xFFFFFFFF
elif mem.duplex == "split":
_mem.txfreq = int(mem.offset / 10)
elif mem.duplex == "off":
for i in range(0, 4):
_mem.txfreq[i].set_raw("\xFF")
elif mem.duplex == "+":
_mem.txfreq = int(mem.freq / 10) + int(mem.offset / 10)
elif mem.duplex == "-":
_mem.txfreq = int(mem.freq / 10) - int(mem.offset / 10)
else:
_mem.txfreq = int(mem.freq / 10)
_mem.scan_add = int(mem.skip != "S")
_mem.iswide = int(mem.mode == "FM")
# set the tone
self.tone_model.set_tone(mem, _mem)
# set the power
_mem.power = _mem.power & 0x3
if mem.power is not None:
_mem.power = self._set_power(mem)
else:
_mem.power = 0 # default to Low power if not set
for setting in mem.extra:
setattr(_mem, setting.get_name(), setting.value)
# MRT it is unknown what impact these values have
# MRT This changes them in the channel memory to match what
# Wouxun CPS shows when creating a channel
# MRT It is likely that these are just left as is and not
# written to by CPS - bit remnants of 0xFF in the unused memory
# _mem.unknown1 = 0
# MRT Set to 3 to TO MATCH CPS VALUES
_mem.unknown3 = 3
# MRT set unknown5 to 1 and unknown6 to 0
_mem.unknown5 = 1
_mem.unknown6 = 255
for i in range(0, len(_nam.name)):
if i < len(mem.name) and mem.name[i]:
_nam.name[i] = ord(mem.name[i])
else:
_nam.name[i] = 0x0
self._memobj.valid[mem.number] = MEM_VALID
def _get_settings(self):
_settings = self._memobj.settings
# _txlock = self._memobj.lock
_vfoa = self._memobj.vfoa
_vfob = self._memobj.vfob
_scan = self._memobj.scan_groups
_callname = self._memobj.call_names
if self.MODEL == "KG-935G":
themelist = THEME_LIST
vfoa_grp_label = "VFO A Settings"
vfob_grp_label = "VFO B Settings"
workmodelist = WORKMODE_LIST
pfkeyshort = PFKEYSHORT_LIST
pfkeylong = PFKEYLONG_LIST
dispmesg = "Display Message - Interface Display Edit"
areamsglabel = "Model / Bottom Banner"
vfo_area = "VFO "
ani_msg = "ANI-ID Switch (ANI-SW)"
pttdly_msg = "PTT-DLY"
idtx_msg = "PTT-ID"
elif self.MODEL == "KG-UV8H":
themelist = THEME_LIST
vfoa_grp_label = "VFO A Settings"
vfob_grp_label = "VFO B Settings"
workmodelist = WORKMODE_LIST
pfkeyshort = PFKEYSHORT_LIST_UV8H
pfkeylong = PFKEYLONG_LIST_UV8H
dispmesg = "Display Message - Interface Display Edit"
areamsglabel = "Model / Bottom Banner"
vfo_area = "VFO "
ani_msg = "ANI-ID Switch (ANI-SW)"
pttdly_msg = "PTT-DLY"
idtx_msg = "PTT-ID"
elif (self.MODEL == "KG-935G Plus" or
self.MODEL == "KG-935H"):
themelist = THEME_LIST_935GPLUS
vfoa_grp_label = "Freq Mode A Settings"
vfob_grp_label = "Freq Mode B Settings"
workmodelist = WORKMODE_LIST_935GPLUS
pfkeyshort = PFKEYSHORT_LIST_935GPLUS
pfkeylong = PFKEYLONG_LIST_935GPLUS
dispmesg = "Top Message"
areamsglabel = "Area Message"
vfo_area = "Area "
ani_msg = "Send ANI-ID on Tx(ANI-SW)"
pttdly_msg = "ID Delay"
idtx_msg = "ID Transmit Setting"
cfg_grp = RadioSettingGroup("cfg_grp", "Config Settings")
vfoa_grp = RadioSettingGroup("vfoa_grp", vfoa_grp_label)
vfob_grp = RadioSettingGroup("vfob_grp", vfob_grp_label)
key_grp = RadioSettingGroup("key_grp", "Key Settings")
fmradio_grp = RadioSettingGroup("fmradio_grp", "FM Broadcast Memory")
lmt_grp = RadioSettingGroup("lmt_grp", "Frequency Limits")
oem_grp = RadioSettingGroup("oem_grp", "OEM Info")
scan_grp = RadioSettingGroup("scan_grp", "Scan Group")
call_grp = RadioSettingGroup("call_grp", "Call Settings")
group = RadioSettings(cfg_grp, vfoa_grp, vfob_grp,
fmradio_grp, key_grp, scan_grp,
call_grp, lmt_grp, oem_grp)
# Call Settings
rs = RadioSetting("cur_call_grp", "Current Call Group",
RadioSettingValueList(CALLGROUP_LIST,
current_index=_settings.
cur_call_grp))
call_grp.append(rs)
def apply_callid(setting, obj):
c = str2callid(setting.value)
obj.cid = c
callchars = "0123456789"
for i in range(1, 21):
_msg = ""
_msg1 = str(getattr(_callname, 'call_name%i' % i)).split('\0')[0]
# MRT - Handle default factory values of 0xFF or
# non-ascii values in Call Name memory
for char in _msg1:
if char < chr(0x20) or char > chr(0x7E):
_msg += ""
else:
_msg += str(char)
val = RadioSettingValueString(0, 6, _msg)
val.set_mutable(True)
rs = RadioSetting("call_names.call_name%i" % i,
"Call Name %i" % i, val)
call_grp.append(rs)
x = i - 1
callid = self._memobj.call_ids[x]
c_id = RadioSettingValueString(0, 6,
self.callid2str(callid.cid),
False)
rs = RadioSetting("call_ids[%i].cid" % x,
"Call Code %i" % i, c_id)
rs.set_apply_callback(apply_callid, callid)
call_grp.append(rs)
# Configuration Settings
#
rs = RadioSetting("DspBrtAct", "Display Brightness ACTIVE",
RadioSettingValueMap(DSPBRTACT_MAP,
_settings.DspBrtAct))
cfg_grp.append(rs)
if self.MODEL != "KG-UV8H":
rs = RadioSetting(
"DspBrtSby", "Display Brightness STANDBY",
RadioSettingValueList(
DSPBRTSBY_LIST, current_index=_settings.DspBrtSby))
cfg_grp.append(rs)
rs = RadioSetting("wxalert", "Weather Alert",
RadioSettingValueBoolean(_settings.wxalert))
cfg_grp.append(rs)
if (self.MODEL == "KG-935G Plus" or self.MODEL == "KG-935H"):
rs = RadioSetting("wxalert_type", "Weather Alert Notification",
RadioSettingValueList(WX_TYPE,
current_index=_settings.
wxalert_type))
cfg_grp.append(rs)
rs = RadioSetting("power_save", "Battery Saver",
RadioSettingValueBoolean(_settings.power_save))
cfg_grp.append(rs)
rs = RadioSetting("theme", "Theme",
RadioSettingValueList(
themelist, current_index=_settings.theme))
cfg_grp.append(rs)
rs = RadioSetting("backlight", "Backlight Active Time",
RadioSettingValueList(BACKLIGHT_LIST,
current_index=_settings.
backlight))
cfg_grp.append(rs)
rs = RadioSetting("scan_rev", "Scan Mode",
RadioSettingValueList(SCANMODE_LIST,
current_index=_settings.
scan_rev))
cfg_grp.append(rs)
rs = RadioSetting("prich_sw", "Priority Channel Scan",
RadioSettingValueBoolean(_settings.prich_sw))
cfg_grp.append(rs)
rs = RadioSetting("pri_ch",
"Priority Channel - Can not be empty Channel",
RadioSettingValueInteger(1, 999, _settings.pri_ch))
cfg_grp.append(rs)
rs = RadioSetting("scan_det", "Scan Mode Tone Detect",
RadioSettingValueBoolean(_settings.scan_det))
cfg_grp.append(rs)
rs = RadioSetting("ToneScnSave", "Tone Scan Save",
RadioSettingValueList(TONESCANSAVELIST,
current_index=_settings.
ToneScnSave))
cfg_grp.append(rs)
rs = RadioSetting("roger_beep", "Roger Beep",
RadioSettingValueList(ROGER_LIST,
current_index=_settings.
roger_beep))
cfg_grp.append(rs)
rs = RadioSetting("timeout", "Timeout Timer (TOT)",
RadioSettingValueList(
TIMEOUT_LIST, current_index=_settings.timeout))
cfg_grp.append(rs)
rs = RadioSetting("toalarm", "Timeout Alarm (TOA)",
RadioSettingValueInteger(0, 10, _settings.toalarm))
cfg_grp.append(rs)
rs = RadioSetting("vox", "VOX",
RadioSettingValueList(LIST_10,
current_index=_settings.vox))
cfg_grp.append(rs)
rs = RadioSetting("voice", "Voice Guide",
RadioSettingValueBoolean(_settings.voice))
cfg_grp.append(rs)
rs = RadioSetting("beep", "Keypad Beep",
RadioSettingValueBoolean(_settings.beep))
cfg_grp.append(rs)
rs = RadioSetting("BCL_A", "Busy Channel Lock-out A",
RadioSettingValueBoolean(_settings.BCL_A))
cfg_grp.append(rs)
rs = RadioSetting("BCL_B", "Busy Channel Lock-out B",
RadioSettingValueBoolean(_settings.BCL_B))
cfg_grp.append(rs)
rs = RadioSetting("smuteset", "Secondary Area Mute (SMUTESET)",
RadioSettingValueList(SMUTESET_LIST,
current_index=_settings.
smuteset))
cfg_grp.append(rs)
rs = RadioSetting("ani_sw", ani_msg,
RadioSettingValueBoolean(_settings.ani_sw))
cfg_grp.append(rs)
rs = RadioSetting("dtmf_st", "DTMF Sidetone (SIDETONE)",
RadioSettingValueList(DTMFST_LIST,
current_index=_settings.
dtmf_st))
cfg_grp.append(rs)
rs = RadioSetting("alert", "Alert Tone",
RadioSettingValueList(ALERTS_LIST,
current_index=_settings.alert))
cfg_grp.append(rs)
rs = RadioSetting("ptt_delay", pttdly_msg,
RadioSettingValueMap(PTTDELAY_TIMES,
_settings.ptt_delay))
cfg_grp.append(rs)
rs = RadioSetting(
"ptt_id", idtx_msg,
RadioSettingValueList(
PTTID_LIST, current_index=_settings.ptt_id))
cfg_grp.append(rs)
rs = RadioSetting(
"ring_time", "Ring Time",
RadioSettingValueList(
LIST_10, current_index=_settings.ring_time))
cfg_grp.append(rs)
if self.MODEL == "KG-UV8H":
rs = RadioSetting("language", "Language",
RadioSettingValueList(
LANGUAGE_LIST,
current_index=_settings.language))
cfg_grp.append(rs)
rs = RadioSetting("rptmode", "Radio Work Mode",
RadioSettingValueList(
RPTMODE_LIST,
current_index=_settings.rptmode))
cfg_grp.append(rs)
rs = RadioSetting("rpttype", "Repeater Type",
RadioSettingValueMap(RPTTYPE_MAP,
_settings.rpttype))
cfg_grp.append(rs)
rs = RadioSetting("rptspk", "Repeater Speaker",
RadioSettingValueBoolean(_settings.rptspk))
cfg_grp.append(rs)
rs = RadioSetting("rptptt", "Repeater PTT",
RadioSettingValueBoolean(_settings.rptptt))
cfg_grp.append(rs)
rs = RadioSetting(
"rpt_hold", "RPT Hold Time",
RadioSettingValueList(
HOLD_TIMES, current_index=_settings.rpt_hold))
cfg_grp.append(rs)
if self.MODEL == "KG-935H":
rs = RadioSetting("rpttype", "Repeater Type",
RadioSettingValueMap(RPTTYPE_MAP,
_settings.rpttype))
cfg_grp.append(rs)
rs = RadioSetting("rptspk", "Repeater Speaker",
RadioSettingValueBoolean(_settings.rptspk))
cfg_grp.append(rs)
rs = RadioSetting("rptptt", "Repeater PTT",
RadioSettingValueBoolean(_settings.rptptt))
cfg_grp.append(rs)
rs = RadioSetting("rpt_tone", "Repeater Tone",
RadioSettingValueBoolean(_settings.rpt_tone))
cfg_grp.append(rs)
rs = RadioSetting("stopwatch", "Timer / Stopwatch",
RadioSettingValueBoolean(_settings.stopwatch))
cfg_grp.append(rs)
rs = RadioSetting("autolock", "Autolock",
RadioSettingValueBoolean(_settings.autolock))
cfg_grp.append(rs)
rs = RadioSetting("keylock", "Keypad Lock",
RadioSettingValueBoolean(_settings.keylock))
cfg_grp.append(rs)
rs = RadioSetting("ponmsg", "Poweron message",
RadioSettingValueList(
PONMSG_LIST, current_index=_settings.ponmsg))
cfg_grp.append(rs)
rs = RadioSetting("dtmf_tx_time", "DTMF Transmit Time",
RadioSettingValueMap(DTMF_TIMES,
_settings.dtmf_tx_time))
cfg_grp.append(rs)
rs = RadioSetting("dtmf_interval", "DTMF Interval Time",
RadioSettingValueMap(DTMF_TIMES,
_settings.dtmf_interval))
cfg_grp.append(rs)
if (self.MODEL == "KG-935G Plus" or self.MODEL == "KG-935H"):
rs = RadioSetting("batt_ind", "Battery Indicator",
RadioSettingValueList(
BATT_DISP_LIST,
current_index=_settings.batt_ind))
cfg_grp.append(rs)
rs = RadioSetting("sim_rec", "Simultaneous Receive",
RadioSettingValueBoolean(_settings.sim_rec))
cfg_grp.append(rs)
rs = RadioSetting("channel_menu", "Menu available in channel mode",
RadioSettingValueBoolean(_settings.channel_menu))
cfg_grp.append(rs)
pswdchars = "0123456789"
_msg = str(_settings.mode_sw_pwd).split("\0")[0]
val = RadioSettingValueString(0, 6, _msg, False)
val.set_mutable(True)
val.set_charset(pswdchars)
rs = RadioSetting("mode_sw_pwd", "Mode SW Pwd", val)
cfg_grp.append(rs)
_msg = str(_settings.reset_pwd).split("\0")[0]
val = RadioSettingValueString(0, 6, _msg, False)
val.set_charset(pswdchars)
val.set_mutable(True)
rs = RadioSetting("reset_pwd", "Reset Pwd", val)
cfg_grp.append(rs)
# Key Settings
#
_msg = str(_settings.dispstr).split("\0")[0]
val = RadioSettingValueString(0, 15, _msg)
val.set_mutable(True)
rs = RadioSetting("dispstr",
dispmesg, val)
key_grp.append(rs)
def apply_ani_id(setting, obj):
c = str2callid(setting.value)
obj.ani_code = c
cid = self._memobj.settings
my_callid = RadioSettingValueString(3, 6,
self.callid2str(cid.ani_code),
False)
rs = RadioSetting("ani_code", "ANI Edit", my_callid)
rs.set_apply_callback(apply_ani_id, cid)
key_grp.append(rs)
rs = RadioSetting("pf1_shrt", "PF1 SHORT Key function",
RadioSettingValueList(
pfkeyshort,
current_index=_settings.pf1_shrt))
key_grp.append(rs)
rs = RadioSetting("pf1_long", "PF1 LONG Key function",
RadioSettingValueList(
pfkeylong,
current_index=_settings.pf1_long))
key_grp.append(rs)
if self.MODEL != "KG-UV8H":
rs = RadioSetting("pf2_shrt", "PF2 SHORT Key function",
RadioSettingValueList(
pfkeyshort,
current_index=_settings.pf2_shrt))
key_grp.append(rs)
rs = RadioSetting("pf2_long", "PF2 LONG Key function",
RadioSettingValueList(
pfkeylong,
current_index=_settings.pf2_long))
key_grp.append(rs)
# SCAN GROUP settings
rs = RadioSetting("ScnGrpA_Act", "Scan Group A Active",
RadioSettingValueList(SCANGRP_LIST,
current_index=_settings.
ScnGrpA_Act))
scan_grp.append(rs)
rs = RadioSetting("ScnGrpB_Act", "Scan Group B Active",
RadioSettingValueList(SCANGRP_LIST,
current_index=_settings.
ScnGrpB_Act))
scan_grp.append(rs)
for i in range(1, 11):
val = getattr(self._memobj.scan_groups, 'Group_lower%i' % i)
rs = RadioSetting("scan_groups.Group_lower%i" % i,
"Scan Group %i Lower" % i,
RadioSettingValueInteger(1, 999, val))
scan_grp.append(rs)
val = getattr(self._memobj.scan_groups, 'Group_upper%i' % i)
rs = RadioSetting("scan_groups.Group_upper%i" % i,
"Scan Group %i Upper" % i,
RadioSettingValueInteger(1, 999, val))
scan_grp.append(rs)
# VFO A Settings
#
rs = RadioSetting("work_mode_a", vfo_area + "A Workmode",
RadioSettingValueList(workmodelist,
current_index=_settings.
work_mode_a))
vfoa_grp.append(rs)
rs = RadioSetting("work_ch_a", vfo_area + "A Work Channel",
RadioSettingValueInteger(1, 999,
_settings.work_ch_a))
vfoa_grp.append(rs)
rs = RadioSetting("vfoa.rxfreq", vfo_area + "A Rx Frequency (MHz)",
RadioSettingValueFloat(
30.00000, 999.999999,
(_vfoa.rxfreq / 100000.0), 0.000001, 6))
vfoa_grp.append(rs)
if self.MODEL == "KG-UV8H":
rs = RadioSetting("vfoa.offset", vfo_area + "A Offset (MHz)",
RadioSettingValueFloat(
0.00000, 599.999999,
(_vfoa.offset / 100000.0), 0.000001, 6))
vfoa_grp.append(rs)
rs = RadioSetting("vfoa.rxtone", vfo_area + "A Rx tone",
RadioSettingValueMap(
TONE_MAP, _vfoa.rxtone))
vfoa_grp.append(rs)
rs = RadioSetting("vfoa.txtone", vfo_area + "A Tx tone",
RadioSettingValueMap(
TONE_MAP, _vfoa.txtone))
vfoa_grp.append(rs)
# MRT - AND power with 0x03 to display only the lower 2 bits for
# power level and to clear the upper bits
# MRT - any bits set in the upper 2 bits will cause radio to show
# invalid values for power level and a display glitch
# MRT - when PTT is pushed
_vfoa.power = _vfoa.power & 0x3
if _vfoa.power > 2:
_vfoa.power = 2
rs = RadioSetting("vfoa.power", vfo_area + "A Power",
RadioSettingValueList(
POWER_LIST, current_index=_vfoa.power))
vfoa_grp.append(rs)
rs = RadioSetting("vfoa.iswide", vfo_area + "A Wide/Narrow",
RadioSettingValueList(
BANDWIDTH_LIST, current_index=_vfoa.iswide))
vfoa_grp.append(rs)
rs = RadioSetting("vfoa.mute_mode", vfo_area + "A Mute (SP Mute)",
RadioSettingValueList(
SPMUTE_LIST, current_index=_vfoa.mute_mode))
vfoa_grp.append(rs)
if self.MODEL == "KG-UV8H":
rs = RadioSetting("vfoa.ofst_dir", vfo_area + "A Shift Dir",
RadioSettingValueList(
OFFSET_LIST, current_index=_vfoa.ofst_dir))
vfoa_grp.append(rs)
else:
rs = RadioSetting("VFO_repeater_a", vfo_area + "A Repeater",
RadioSettingValueBoolean(
_settings.VFO_repeater_a))
vfoa_grp.append(rs)
rs = RadioSetting("vfoa.scrambler", vfo_area + "A Descramble",
RadioSettingValueList(
SCRAMBLE_LIST, current_index=_vfoa.scrambler))
vfoa_grp.append(rs)
rs = RadioSetting("vfoa.cmpndr", vfo_area + "A Compander",
RadioSettingValueList(
ONOFF_LIST, current_index=_vfoa.cmpndr))
vfoa_grp.append(rs)
rs = RadioSetting("vfoa.step", vfo_area + "A Step (kHz)",
RadioSettingValueList(
STEP_LIST, current_index=_vfoa.step))
vfoa_grp.append(rs)
rs = RadioSetting("vfoa.squelch", vfo_area + "A Squelch",
RadioSettingValueList(
LIST_10, current_index=_vfoa.squelch))
vfoa_grp.append(rs)
# VFO B Settings
rs = RadioSetting("work_mode_b", vfo_area + "B Workmode",
RadioSettingValueList(workmodelist,
current_index=_settings.
work_mode_b))
vfob_grp.append(rs)
rs = RadioSetting("work_ch_b", vfo_area + "B Work Channel",
RadioSettingValueInteger(1, 999,
_settings.work_ch_b))
vfob_grp.append(rs)
rs = RadioSetting("vfob.rxfreq", vfo_area + "B Rx Frequency (MHz)",
RadioSettingValueFloat(
30.000000, 999.999999,
(_vfob.rxfreq / 100000.0), 0.000001, 6))
vfob_grp.append(rs)
if self.MODEL == "KG-UV8H":
rs = RadioSetting("vfob.offset", vfo_area + "B Offset (MHz)",
RadioSettingValueFloat(
0.00000, 599.999999,
(_vfob.offset / 100000.0), 0.000001, 6))
vfob_grp.append(rs)
rs = RadioSetting("vfob.rxtone", vfo_area + "B Rx tone",
RadioSettingValueMap(
TONE_MAP, _vfob.rxtone))
vfob_grp.append(rs)
rs = RadioSetting("vfob.txtone", vfo_area + "B Tx tone",
RadioSettingValueMap(
TONE_MAP, _vfob.txtone))
vfob_grp.append(rs)
# MRT - AND power with 0x03 to display only the lower 2 bits for
# power level and to clear the upper bits
# MRT - any bits set in the upper 2 bits will cause radio to show
# invalid values for power level and a display glitch
# MRT - when PTT is pushed
_vfob.power = _vfob.power & 0x3
if _vfob.power > 2:
_vfob.power = 2
rs = RadioSetting("vfob.power", vfo_area + "B Power",
RadioSettingValueList(
POWER_LIST, current_index=_vfob.power))
vfob_grp.append(rs)
rs = RadioSetting("vfob.iswide", vfo_area + "B Wide/Narrow",
RadioSettingValueList(
BANDWIDTH_LIST, current_index=_vfob.iswide))
vfob_grp.append(rs)
rs = RadioSetting("vfob.mute_mode", vfo_area + "B Mute (SP Mute)",
RadioSettingValueList(
SPMUTE_LIST, current_index=_vfob.mute_mode))
vfob_grp.append(rs)
if self.MODEL == "KG-UV8H":
rs = RadioSetting("vfob.ofst_dir", vfo_area + "B Shift Dir",
RadioSettingValueList(
OFFSET_LIST, current_index=_vfob.ofst_dir))
vfob_grp.append(rs)
else:
rs = RadioSetting("VFO_repeater_b", vfo_area + "B Repeater",
RadioSettingValueBoolean(
_settings.VFO_repeater_b))
vfob_grp.append(rs)
rs = RadioSetting("vfob.scrambler", vfo_area + "B Descramble",
RadioSettingValueList(
SCRAMBLE_LIST, current_index=_vfob.scrambler))
vfob_grp.append(rs)
rs = RadioSetting("vfob.cmpndr", vfo_area + "B Compander",
RadioSettingValueList(
ONOFF_LIST, current_index=_vfob.cmpndr))
vfob_grp.append(rs)
rs = RadioSetting("vfob.step", vfo_area + "B Step (kHz)",
RadioSettingValueList(
STEP_LIST, current_index=_vfob.step))
vfob_grp.append(rs)
rs = RadioSetting("vfob.squelch", vfo_area + "B Squelch",
RadioSettingValueList(
LIST_10, current_index=_vfob.squelch))
vfob_grp.append(rs)
# FM RADIO PRESETS
# memory stores raw integer value like 760
# radio will divide 760 by 10 and interpret correctly at 76.0 MHz
for i in range(1, 21):
val = getattr(_settings, 'FM_radio%i' % i)
rs = RadioSetting("FM_radio%i" % i, "FM Preset %i" % i,
RadioSettingValueFloat(76.0, 108.0, val / 10,
0.1, 1))
fmradio_grp.append(rs)
# Freq Limits settings
rs = RadioSetting("vhf_limits.rx_start", "VHF RX Lower Limit (MHz)",
RadioSettingValueFloat(
30.000000, 299.999999,
(self._memobj.vhf_limits.rx_start / 100000.0),
0.000001, 6))
lmt_grp.append(rs)
rs = RadioSetting("vhf_limits.rx_stop", "VHF RX Upper Limit (MHz)",
RadioSettingValueFloat(
30.000000, 299.999999,
(self._memobj.vhf_limits.rx_stop / 100000.0),
0.000001, 6))
lmt_grp.append(rs)
rs = RadioSetting("vhf_limits.tx_start", "VHF TX Lower Limit (MHz)",
RadioSettingValueFloat(
30.000000, 299.999999,
(self._memobj.vhf_limits.tx_start / 100000.0),
0.000001, 6))
lmt_grp.append(rs)
rs = RadioSetting("vhf_limits.tx_stop", "VHF TX Upper Limit (MHz)",
RadioSettingValueFloat(
30.000000, 299.999999,
(self._memobj.vhf_limits.tx_stop / 100000.0),
0.000001, 6))
lmt_grp.append(rs)
# MRT - TX Limits do not appear to change radio's ability
# to transmit on other freqs.
# MRT - Appears that the radio firmware prevent Tx
# on anything other than a valid GMRS Freq
rs = RadioSetting("uhf_limits.rx_start", "UHF RX Lower Limit (MHz)",
RadioSettingValueFloat(
300.000000, 999.999999,
(self._memobj.uhf_limits.rx_start / 100000.0),
0.000001, 6))
lmt_grp.append(rs)
rs = RadioSetting("uhf_limits.rx_stop", "UHF RX Upper Limit (MHz)",
RadioSettingValueFloat(
300.000000, 999.999999,
(self._memobj.uhf_limits.rx_stop / 100000.0),
0.000001, 6))
lmt_grp.append(rs)
rs = RadioSetting("uhf_limits.tx_start", "UHF TX Lower Limit (MHz)",
RadioSettingValueFloat(
300.000000, 999.999999,
(self._memobj.uhf_limits.tx_start / 100000.0),
0.000001, 6))
lmt_grp.append(rs)
rs = RadioSetting("uhf_limits.tx_stop", "UHF TX Upper Limit (MHz)",
RadioSettingValueFloat(
300.000000, 999.999999,
(self._memobj.uhf_limits.tx_stop / 100000.0),
0.000001, 6))
lmt_grp.append(rs)
if self.MODEL == "KG-935H":
val = self._memobj.lock.txlocked
rs = RadioSetting("lock.txlocked",
"Locked - Limits Tx to Factory Range",
RadioSettingValueBoolean(val))
lmt_grp.append(rs)
# OEM info
def _decode(lst):
_str = ''.join([chr(int(c)) for c in lst
if chr(int(c)) in chirp_common.CHARSET_ASCII])
return _str
def do_nothing(setting, obj):
return
_str = _decode(self._memobj.oem_info.model)
val = RadioSettingValueString(0, 8, _str)
val.set_mutable(True)
rs = RadioSetting("oem_info.model", areamsglabel, val)
if self.MODEL == "KG-935G":
oem_grp.append(rs)
else:
key_grp.append(rs)
_str = _decode(self._memobj.oem_info.oem1)
val = RadioSettingValueString(0, 15, _str)
val.set_mutable(False)
rs = RadioSetting("oem_info.oem1", "OEM String 1", val)
rs.set_apply_callback(do_nothing, _settings)
oem_grp.append(rs)
_str = _decode(self._memobj.oem_info.oem2)
val = RadioSettingValueString(0, 15, _str)
val.set_mutable(False)
rs = RadioSetting("oem_info.oem2", "Firmware Version ??", val)
rs.set_apply_callback(do_nothing, _settings)
oem_grp.append(rs)
_str = _decode(self._memobj.oem_info.date)
val = RadioSettingValueString(0, 15, _str)
val.set_mutable(False)
rs = RadioSetting("oem_info.date", "OEM Date", val)
rs.set_apply_callback(do_nothing, _settings)
oem_grp.append(rs)
return group
def get_settings(self):
try:
return self._get_settings()
except:
import traceback
LOG.error("Failed to parse settings: %s", traceback.format_exc())
return None
def set_settings(self, settings):
for element in settings:
if not isinstance(element, RadioSetting):
self.set_settings(element)
continue
else:
try:
if "." in element.get_name():
bits = element.get_name().split(".")
obj = self._memobj
for bit in bits[:-1]:
if "[" in bit and "]" in bit:
bit, index = bit.split("[", 1)
index, junk = index.split("]", 1)
index = int(index)
obj = getattr(obj, bit)[index]
else:
obj = getattr(obj, bit)
setting = bits[-1]
else:
obj = self._memobj.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))
if self._is_freq(element):
# setattr(obj, setting, int(element.value / 10))
# MRT rescale freq values to match radio
# expected values
setattr(obj, setting,
int(element.values()[0]._current *
100000.0))
elif self._is_fmradio(element):
# MRT rescale FM Radio values to match radio
# expected values
setattr(obj, setting,
int(element.values()[0]._current * 10.0))
else:
setattr(obj, setting, element.value)
except Exception:
LOG.debug(element.get_name())
raise
def _is_freq(self, element):
return ("rxfreq" in element.get_name() or
"txoffset" in element.get_name() or
"rx_start" in element.get_name() or
"rx_stop" in element.get_name() or
"tx_start" in element.get_name() or
"tx_stop" in element.get_name())
def _is_fmradio(self, element):
return "FM_radio" in element.get_name()
def callid2str(self, cid):
"""Caller ID per MDC-1200 spec? Must be 3-6 digits (100 - 999999).
One digit (binary) per byte, terminated with '0xc'
"""
bin2ascii = "0123456789"
cidstr = ""
for i in range(0, 6):
b = cid[i].get_value()
# Handle fluky firmware 0x0a is sometimes used instead of 0x00
if b == 0x0a:
b = 0x00
if b == 0xc or b == 0xf: # the cid EOL
break
if b > 0xa:
raise InvalidValueError(
"Caller ID code has illegal byte 0x%x" % b)
cidstr += bin2ascii[b]
return cidstr
@directory.register
class KG935GPlusRadio(KG935GRadio):
"""Wouxun KG-935G Plus"""
VENDOR = "Wouxun"
MODEL = "KG-935G Plus"
HAS_SCRAMBLER = True
HAS_FAVORITE = True
def process_mmap(self):
self._memobj = bitwise.parse(_MEM_FORMAT_935GPLUS, self._mmap)
@directory.register
class KGUV8HRadio(KG935GRadio):
"""Wouxun KG-UV8H"""
VENDOR = "Wouxun"
MODEL = "KG-UV8H"
def process_mmap(self):
self._memobj = bitwise.parse(_MEM_FORMAT_UV8H, self._mmap)
@directory.register
class KG935HRadio(KG935GRadio):
"""Wouxun KG-935H"""
VENDOR = "Wouxun"
MODEL = "KG-935H"
POWER_LEVELS = [chirp_common.PowerLevel("L", watts=2.0),
chirp_common.PowerLevel("M", watts=5.0),
chirp_common.PowerLevel("H", watts=8.0)]
config_map = config_map_935H
HAS_SCRAMBLER = True
HAS_FAVORITE = True
def process_mmap(self):
self._memobj = bitwise.parse(_MEM_FORMAT_935H, self._mmap)
@directory.register
class KGXS20G(KG935GRadio):
"""Wouxun KG-XS20G"""
VENDOR = "Wouxun"
MODEL = "KG-XS20G"
_model = b"KG-UV8D-A"
_record_start = 0x79
config_map = config_map_x20h
POWER_LEVELS = [chirp_common.PowerLevel("L", watts=5.0),
chirp_common.PowerLevel("H", watts=20.0)]
HAS_SCRAMBLER = False
HAS_FAVORITE = False
def get_features(self):
rf = super().get_features()
rf.has_settings = False # Set to False until settings are mapped
return rf
def process_mmap(self):
self._memobj = bitwise.parse(_MEM_FORMAT_935GPLUS, self._mmap)
def _get_power(self, _mem, mem):
if _mem.power == 0:
mem.power = self.POWER_LEVELS[0]
else:
mem.power = self.POWER_LEVELS[1]
def _set_power(self, mem):
temp_val = 0 if mem.power == self.POWER_LEVELS[0] else 2
return temp_val
@directory.register
class KGXS20HRadio(KGXS20G):
"""Wouxun KG-XS20H"""
VENDOR = "Wouxun"
MODEL = "KG-XS20H"
HAS_SCRAMBLER = True
@directory.register
class KGXS20GPlusRadio(KGXS20G):
"""Wouxun KG-XS20G Plus"""
VENDOR = "Wouxun"
MODEL = "KG-XS20G Plus"
chirp-20261030/chirp/drivers/kguv8d.py 0000644 0001750 0001750 00000105370 15172631605 016413 0 ustar hibby hibby # Copyright 2014 Ron Wellsted M0RNW
#
# 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 .
"""Wouxun KG-UV8D radio management module"""
import time
import logging
from chirp import util, chirp_common, bitwise, memmap, errors, directory
from chirp import checksum
from chirp.settings import RadioSetting, RadioSettingGroup, \
RadioSettingValueBoolean, RadioSettingValueList, \
RadioSettingValueInteger, RadioSettingValueString, \
RadioSettings
from chirp.kenwood_tone import KenwoodToneModel
import struct
LOG = logging.getLogger(__name__)
CMD_ID = 128
CMD_END = 129
CMD_RD = 130
CMD_WR = 131
MEM_VALID = 158
AB_LIST = ["A", "B"]
STEPS = [2.5, 5.0, 6.25, 10.0, 12.5, 25.0, 50.0, 100.0]
STEP_LIST = [str(x) for x in STEPS]
ROGER_LIST = ["Off", "BOT", "EOT", "Both"]
TIMEOUT_LIST = ["Off"] + [str(x) + "s" for x in range(15, 901, 15)]
VOX_LIST = ["Off"] + ["%s" % x for x in range(1, 10)]
BANDWIDTH_LIST = ["Narrow", "Wide"]
VOICE_LIST = ["Off", "On"]
LANGUAGE_LIST = ["Chinese", "English"]
SCANMODE_LIST = ["TO", "CO", "SE"]
PF1KEY_LIST = ["Call", "VFTX"]
PF3KEY_LIST = ["Scan", "Lamp", "Tele Alarm", "SOS-CH", "Radio", "Disable"]
WORKMODE_LIST = ["VFO", "Channel No.", "Ch. No.+Freq.", "Ch. No.+Name"]
BACKLIGHT_LIST = ["Always On"] + [str(x) + "s" for x in range(1, 21)] + \
["Always Off"]
OFFSET_LIST = ["+", "-"]
PONMSG_LIST = ["Bitmap", "Battery Volts"]
SPMUTE_LIST = ["QT", "QT+DTMF", "QT*DTMF"]
DTMFST_LIST = ["DT-ST", "ANI-ST", "DT-ANI", "Off"]
DTMF_TIMES = ["%s" % x for x in range(50, 501, 10)]
RPTSET_LIST = ["X-TWRPT", "X-DIRRPT"]
ALERTS = [1750, 2100, 1000, 1450]
ALERTS_LIST = [str(x) for x in ALERTS]
PTTID_LIST = ["BOT", "EOT", "Both"]
LIST_10 = ["Off"] + ["%s" % x for x in range(1, 11)]
SCANGRP_LIST = ["All"] + ["%s" % x for x in range(1, 11)]
SCQT_LIST = ["All", "Decoder", "Encoder"]
SMUTESET_LIST = ["Off", "Tx", "Rx", "Tx/Rx"]
POWER_LIST = ["Lo", "Hi"]
HOLD_TIMES = ["Off"] + ["%s" % x for x in range(100, 5001, 100)]
RPTMODE_LIST = ["Radio", "Repeater"]
# memory slot 0 is not used, start at 1 (so need 1000 slots, not 999)
# structure elements whose name starts with x are currently unidentified
_MEM_FORMAT = """
#seekto 0x0044;
struct {
u32 rx_start;
u32 rx_stop;
u32 tx_start;
u32 tx_stop;
} uhf_limits;
#seekto 0x0054;
struct {
u32 rx_start;
u32 rx_stop;
u32 tx_start;
u32 tx_stop;
} vhf_limits;
#seekto 0x0400;
struct {
u8 model[8];
u8 unknown[2];
u8 oem1[10];
u8 oem2[10];
u8 unknown2[8];
u8 version[10];
u8 unknown3[6];
u8 date[8];
} oem_info;
#seekto 0x0480;
struct {
u16 lower;
u16 upper;
} scan_groups[10];
#seekto 0x0500;
struct {
u8 call_code[6];
} call_groups[20];
#seekto 0x0580;
struct {
char call_name[6];
} call_group_name[20];
#seekto 0x0800;
struct {
u8 ponmsg;
char dispstr[15];
u8 x0810;
u8 x0811;
u8 x0812;
u8 x0813;
u8 x0814;
u8 voice;
u8 timeout;
u8 toalarm;
u8 channel_menu;
u8 power_save;
u8 autolock;
u8 keylock;
u8 beep;
u8 stopwatch;
u8 vox;
u8 scan_rev;
u8 backlight;
u8 roger_beep;
u8 mode_sw_pwd[6];
u8 reset_pwd[6];
u16 pri_ch;
u8 ani_sw;
u8 ptt_delay;
u8 ani[6];
u8 dtmf_st;
u8 bcl_a;
u8 bcl_b;
u8 ptt_id;
u8 prich_sw;
u8 rpt_set;
u8 rpt_spk;
u8 rpt_ptt;
u8 alert;
u8 pf1_func;
u8 pf3_func;
u8 workmode_b;
u8 workmode_a;
u8 x0845;
u8 dtmf_tx_time;
u8 dtmf_interval;
u8 main_ab;
u16 work_cha;
u16 work_chb;
u8 x084d;
u8 x084e;
u8 x084f;
u8 x0850;
u8 x0851;
u8 x0852;
u8 x0853;
u8 x0854;
u8 rpt_mode;
u8 language;
u8 x0857;
u8 x0858;
u8 x0859;
u8 x085a;
u8 x085b;
u8 x085c;
u8 x085d;
u8 x085e;
u8 single_display;
u8 ring_time;
u8 scg_a;
u8 scg_b;
u8 x0863;
u8 rpt_tone;
u8 rpt_hold;
u8 scan_det;
u8 sc_qt;
u8 x0868;
u8 smuteset;
u8 callcode;
} settings;
#seekto 0x0880;
struct {
u32 rxfreq;
u32 txoffset;
u16 rxtone;
u16 txtone;
u8 unknown1:6,
power:1,
unknown2:1;
u8 unknown3:1,
shift_dir:2,
unknown4:2,
mute_mode:2,
iswide:1;
u8 step;
u8 squelch;
} vfoa;
#seekto 0x08c0;
struct {
u32 rxfreq;
u32 txoffset;
u16 rxtone;
u16 txtone;
u8 unknown1:6,
power:1,
unknown2:1;
u8 unknown3:1,
shift_dir:2,
unknown4:2,
mute_mode:2,
iswide:1;
u8 step;
u8 squelch;
} vfob;
#seekto 0x0900;
struct {
u32 rxfreq;
u32 txfreq;
u16 rxtone;
u16 txtone;
u8 unknown1:6,
power:1,
unknown2:1;
u8 unknown3:2,
scan_add:1,
unknown4:2,
mute_mode:2,
iswide:1;
u16 padding;
} memory[1000];
#seekto 0x4780;
struct {
u8 name[8];
} names[1000];
#seekto 0x6700;
u8 valid[1000];
"""
# Support for the Wouxun KG-UV8D radio
# Serial coms are at 19200 baud
# The data is passed in variable length records
# Record structure:
# Offset Usage
# 0 start of record (\x7d)
# 1 Command (\x80 Identify \x81 End/Reboot \x82 Read \x83 Write)
# 2 direction (\xff PC-> Radio, \x00 Radio -> PC)
# 3 length of payload (excluding header/checksum) (n)
# 4 payload (n bytes)
# 4+n+1 checksum - byte sum (% 256) of bytes 1 -> 4+n
#
# Memory Read Records:
# the payload is 3 bytes, first 2 are offset (big endian),
# 3rd is number of bytes to read
# Memory Write Records:
# the maximum payload size (from the Wouxun software) seems to be 66 bytes
# (2 bytes location + 64 bytes data).
@directory.register
class KGUV8DRadio(chirp_common.CloneModeRadio,
chirp_common.ExperimentalRadio):
"""Wouxun KG-UV8D"""
VENDOR = "Wouxun"
MODEL = "KG-UV8D"
_model = b"KG-UV8D"
_file_ident = b"KGUV8D"
BAUD_RATE = 19200
POWER_LEVELS = [chirp_common.PowerLevel("L", watts=1),
chirp_common.PowerLevel("H", watts=5)]
_record_start = 0x7D
def __init__(self, pipe):
super().__init__(pipe)
self.tone_model = KenwoodToneModel(
dcs_base=0x2800,
pol_mask=0x8000,
tone_init=0xFFFF)
def _write_record(self, cmd, payload=None):
# build the packet
_length = 0
if payload:
_length = len(payload)
_packet = struct.pack('BBBB', self._record_start, cmd, 0xFF, _length)
if payload:
# add the chars to the packet
_packet += payload
# calculate and add the checksum to the packet
_packet += bytes([checksum.checksum_8bit(_packet[1:])])
LOG.debug("Sent:\n%s" % util.hexprint(_packet))
self.pipe.write(_packet)
def _read_record(self):
# read 4 chars for the header
_header = self.pipe.read(4)
if not _header:
raise errors.RadioNoResponse()
if len(_header) != 4:
raise errors.RadioError('Radio sent short header')
_length = struct.unpack('xxxB', _header)[0]
_packet = self.pipe.read(_length)
_cs = checksum.checksum_8bit(_header[1:])
_cs += checksum.checksum_8bit(_packet)
_cs %= 256
_rcs = self.pipe.read(1)[0]
LOG.debug("_cs =%x", _cs)
LOG.debug("_rcs=%x", _rcs)
return (_rcs != _cs, _packet)
# Identify the radio
#
# A Gotcha: the first identify packet returns a bad checksum, subsequent
# attempts return the correct checksum... (well it does on my radio!)
#
# The ID record returned by the radio also includes the current frequency range
# as 4 bytes big-endian in 10 Hz increments
#
# Offset
# 0:10 Model, zero padded (Use first 7 chars for 'KG-UV8D')
# 11:14 UHF rx lower limit (in units of 10 Hz)
# 15:18 UHF rx upper limit
# 19:22 UHF tx lower limit
# 23:26 UHF tx upper limit
# 27:30 VHF rx lower limit
# 31:34 VHF rx upper limit
# 35:38 VHF tx lower limit
# 39:42 VHF tx upper limit
#
@classmethod
def match_model(cls, filedata, filename):
return cls._file_ident in filedata[0x400:0x408]
def _identify(self):
"""Do the identification dance"""
for _i in range(0, 10):
self._write_record(CMD_ID)
_chksum_err, _resp = self._read_record()
LOG.debug("Got:\n%s" % util.hexprint(_resp))
if _chksum_err:
LOG.error("Checksum error: retrying ident...")
time.sleep(0.100)
continue
LOG.debug("Model %s" % util.hexprint(_resp[0:7]))
if _resp[0:7] == self._model:
return
if len(_resp) == 0:
raise errors.RadioNoResponse()
else:
raise Exception("Unable to identify radio")
def _finish(self):
self._write_record(CMD_END)
def process_mmap(self):
self._memobj = bitwise.parse(_MEM_FORMAT, self._mmap)
def sync_in(self):
try:
self._mmap = self._download()
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):
self._upload()
# TODO: This is a dumb, brute force method of downloading the memory.
# it would be smarter to only load the active areas and none of
# the padding/unused areas.
def _download(self):
"""Talk to a Wouxun KG-UV8D and do a download"""
try:
self._identify()
return self._do_download(0, 32768, 64)
except errors.RadioError:
raise
except Exception as e:
LOG.exception('Unknown error during download process')
raise errors.RadioError("Failed to communicate with radio: %s" % e)
def _do_download(self, start, end, blocksize):
# allocate & fill memory
image = b""
for i in range(start, end, blocksize):
req = struct.pack('>HB', i, blocksize)
self._write_record(CMD_RD, req)
cs_error, resp = self._read_record()
if cs_error:
# TODO: probably should retry a few times here
LOG.debug(util.hexprint(resp))
raise Exception("Checksum error on read")
LOG.debug("Got:\n%s" % util.hexprint(resp))
image += resp[2:]
if self.status_fn:
status = chirp_common.Status()
status.cur = i
status.max = end
status.msg = "Cloning from radio"
self.status_fn(status)
self._finish()
return memmap.MemoryMapBytes(image)
def _upload(self):
"""Talk to a Wouxun KG-UV8D and do a upload"""
try:
self._identify()
self._do_upload(0, 32768, 64)
except errors.RadioError:
raise
except Exception as e:
raise errors.RadioError("Failed to communicate with radio: %s" % e)
return
def _do_upload(self, start, end, blocksize):
ptr = start
for i in range(start, end, blocksize):
req = struct.pack('>H', i)
chunk = self.get_mmap()[ptr:ptr + blocksize]
self._write_record(CMD_WR, req + chunk)
LOG.debug(util.hexprint(req + chunk))
cserr, ack = self._read_record()
LOG.debug(util.hexprint(ack))
j = struct.unpack('>H', ack)[0]
if cserr or j != ptr:
raise Exception("Radio did not ack block %i" % ptr)
ptr += blocksize
if self.status_fn:
status = chirp_common.Status()
status.cur = i
status.max = end
status.msg = "Cloning to radio"
self.status_fn(status)
self._finish()
def get_features(self):
# TODO: This probably needs to be setup correctly to match the true
# features of the radio
rf = chirp_common.RadioFeatures()
rf.has_settings = True
rf.has_ctone = True
rf.has_rx_dtcs = True
rf.has_cross = True
rf.has_tuning_step = False
rf.has_bank = False
rf.can_odd_split = True
rf.valid_skips = ["", "S"]
rf.valid_tmodes = ["", "Tone", "TSQL", "DTCS", "Cross"]
rf.valid_tuning_steps = STEPS
rf.valid_cross_modes = [
"Tone->Tone",
"Tone->DTCS",
"DTCS->Tone",
"DTCS->",
"->Tone",
"->DTCS",
"DTCS->DTCS",
]
rf.valid_modes = ["FM", "NFM"]
rf.valid_power_levels = self.POWER_LEVELS
rf.valid_name_length = 8
rf.valid_duplexes = ["", "-", "+", "split", "off"]
rf.valid_bands = [(134000000, 175000000), # supports 2m
(400000000, 520000000)] # supports 70cm
rf.valid_characters = chirp_common.CHARSET_ASCII
rf.memory_bounds = (1, 999) # 999 memories
return rf
@classmethod
def get_prompts(cls):
rp = chirp_common.RadioPrompts()
rp.experimental = ("This radio driver is currently under development. "
"There are no known issues with it, but you should "
"proceed with caution.")
return rp
def get_raw_memory(self, number):
return repr(self._memobj.memory[number])
def get_memory(self, number):
_mem = self._memobj.memory[number]
_nam = self._memobj.names[number]
mem = chirp_common.Memory()
mem.number = number
_valid = self._memobj.valid[mem.number]
LOG.debug("%d %s", number, _valid == MEM_VALID)
if _valid != MEM_VALID:
mem.empty = True
return mem
else:
mem.empty = False
mem.freq = int(_mem.rxfreq) * 10
if _mem.txfreq == 0xFFFFFFFF:
# TX freq not set
mem.duplex = "off"
mem.offset = 0
elif int(_mem.rxfreq) == int(_mem.txfreq):
mem.duplex = ""
mem.offset = 0
elif abs(int(_mem.rxfreq) * 10 - int(_mem.txfreq) * 10) > 70000000:
mem.duplex = "split"
mem.offset = int(_mem.txfreq) * 10
else:
mem.duplex = int(_mem.rxfreq) > int(_mem.txfreq) and "-" or "+"
mem.offset = abs(int(_mem.rxfreq) - int(_mem.txfreq)) * 10
for char in _nam.name:
if char != 0:
mem.name += chr(char)
mem.name = mem.name.rstrip()
self.tone_model.get_tone(_mem, mem)
mem.skip = "" if bool(_mem.scan_add) else "S"
mem.power = self.POWER_LEVELS[_mem.power]
mem.mode = _mem.iswide and "FM" or "NFM"
return mem
def set_memory(self, mem):
number = mem.number
_mem = self._memobj.memory[number]
_nam = self._memobj.names[number]
if mem.empty:
_mem.set_raw("\x00" * (_mem.size() // 8))
self._memobj.valid[number] = 0
self._memobj.names[number].set_raw("\x00" * (_nam.size() // 8))
return
_mem.rxfreq = int(mem.freq / 10)
if mem.duplex == "off":
_mem.txfreq = 0xFFFFFFFF
elif mem.duplex == "split":
_mem.txfreq = int(mem.offset / 10)
elif mem.duplex == "off":
for i in range(0, 4):
_mem.txfreq[i].set_raw("\xFF")
elif mem.duplex == "+":
_mem.txfreq = int(mem.freq / 10) + int(mem.offset / 10)
elif mem.duplex == "-":
_mem.txfreq = int(mem.freq / 10) - int(mem.offset / 10)
else:
_mem.txfreq = int(mem.freq / 10)
_mem.scan_add = int(mem.skip != "S")
_mem.iswide = int(mem.mode == "FM")
# set the tone
self.tone_model.set_tone(mem, _mem)
# set the power
if mem.power:
_mem.power = self.POWER_LEVELS.index(mem.power)
else:
_mem.power = True
# TODO: set the correct mute mode, for now just
# set to mute mode to QT (not QT+DTMF or QT*DTMF)
_mem.mute_mode = 0
for i in range(0, len(_nam.name)):
if i < len(mem.name) and mem.name[i]:
_nam.name[i] = ord(mem.name[i])
else:
_nam.name[i] = 0x0
self._memobj.valid[mem.number] = MEM_VALID
def _get_settings(self):
_settings = self._memobj.settings
_vfoa = self._memobj.vfoa
_vfob = self._memobj.vfob
cfg_grp = RadioSettingGroup("cfg_grp", "Configuration")
vfoa_grp = RadioSettingGroup("vfoa_grp", "VFO A Settings")
vfob_grp = RadioSettingGroup("vfob_grp", "VFO B Settings")
key_grp = RadioSettingGroup("key_grp", "Key Settings")
lmt_grp = RadioSettingGroup("lmt_grp", "Frequency Limits")
oem_grp = RadioSettingGroup("oem_grp", "OEM Info")
group = RadioSettings(cfg_grp, vfoa_grp, vfob_grp,
key_grp, lmt_grp, oem_grp)
#
# Configuration Settings
#
rs = RadioSetting("channel_menu", "Menu available in channel mode",
RadioSettingValueBoolean(_settings.channel_menu))
cfg_grp.append(rs)
rs = RadioSetting("ponmsg", "Poweron message",
RadioSettingValueList(
PONMSG_LIST, current_index=_settings.ponmsg))
cfg_grp.append(rs)
rs = RadioSetting("voice", "Voice Guide",
RadioSettingValueBoolean(_settings.voice))
cfg_grp.append(rs)
rs = RadioSetting("language", "Language",
RadioSettingValueList(LANGUAGE_LIST,
current_index=_settings.
language))
cfg_grp.append(rs)
rs = RadioSetting("timeout", "Timeout Timer",
RadioSettingValueInteger(15, 900,
_settings.timeout * 15, 15))
cfg_grp.append(rs)
rs = RadioSetting("toalarm", "Timeout Alarm",
RadioSettingValueInteger(0, 10, _settings.toalarm))
cfg_grp.append(rs)
rs = RadioSetting("roger_beep", "Roger Beep",
RadioSettingValueBoolean(_settings.roger_beep))
cfg_grp.append(rs)
rs = RadioSetting("power_save", "Power save",
RadioSettingValueBoolean(_settings.power_save))
cfg_grp.append(rs)
rs = RadioSetting("autolock", "Autolock",
RadioSettingValueBoolean(_settings.autolock))
cfg_grp.append(rs)
rs = RadioSetting("keylock", "Keypad Lock",
RadioSettingValueBoolean(_settings.keylock))
cfg_grp.append(rs)
rs = RadioSetting("beep", "Keypad Beep",
RadioSettingValueBoolean(_settings.beep))
cfg_grp.append(rs)
rs = RadioSetting("stopwatch", "Stopwatch",
RadioSettingValueBoolean(_settings.stopwatch))
cfg_grp.append(rs)
rs = RadioSetting("backlight", "Backlight",
RadioSettingValueList(BACKLIGHT_LIST,
current_index=_settings.
backlight))
cfg_grp.append(rs)
rs = RadioSetting("dtmf_st", "DTMF Sidetone",
RadioSettingValueList(DTMFST_LIST,
current_index=_settings.
dtmf_st))
cfg_grp.append(rs)
rs = RadioSetting("ani-id_sw", "ANI-ID Switch",
RadioSettingValueBoolean(_settings.ani_sw))
cfg_grp.append(rs)
rs = RadioSetting(
"ptt-id_delay", "PTT-ID Delay",
RadioSettingValueList(
PTTID_LIST, current_index=_settings.ptt_id))
cfg_grp.append(rs)
rs = RadioSetting(
"ring_time", "Ring Time",
RadioSettingValueList(
LIST_10, current_index=_settings.ring_time))
cfg_grp.append(rs)
rs = RadioSetting("scan_rev", "Scan Mode",
RadioSettingValueList(SCANMODE_LIST,
current_index=_settings.
scan_rev))
cfg_grp.append(rs)
rs = RadioSetting("vox", "VOX",
RadioSettingValueList(LIST_10,
current_index=_settings.vox))
cfg_grp.append(rs)
rs = RadioSetting("prich_sw", "Priority Channel Switch",
RadioSettingValueBoolean(_settings.prich_sw))
cfg_grp.append(rs)
rs = RadioSetting("pri_ch", "Priority Channel",
RadioSettingValueInteger(1, 999, _settings.pri_ch))
cfg_grp.append(rs)
rs = RadioSetting("rpt_mode", "Radio Mode",
RadioSettingValueList(RPTMODE_LIST,
current_index=_settings.
rpt_mode))
cfg_grp.append(rs)
rs = RadioSetting("rpt_set", "Repeater Setting",
RadioSettingValueList(RPTSET_LIST,
current_index=_settings.
rpt_set))
cfg_grp.append(rs)
rs = RadioSetting("rpt_spk", "Repeater Mode Speaker",
RadioSettingValueBoolean(_settings.rpt_spk))
cfg_grp.append(rs)
rs = RadioSetting("rpt_ptt", "Repeater PTT",
RadioSettingValueBoolean(_settings.rpt_ptt))
cfg_grp.append(rs)
rs = RadioSetting("dtmf_tx_time", "DTMF Tx Duration",
RadioSettingValueList(DTMF_TIMES,
current_index=_settings.
dtmf_tx_time))
cfg_grp.append(rs)
rs = RadioSetting("dtmf_interval", "DTMF Interval",
RadioSettingValueList(DTMF_TIMES,
current_index=_settings.
dtmf_interval))
cfg_grp.append(rs)
rs = RadioSetting("alert", "Alert Tone",
RadioSettingValueList(ALERTS_LIST,
current_index=_settings.alert))
cfg_grp.append(rs)
rs = RadioSetting("rpt_tone", "Repeater Tone",
RadioSettingValueBoolean(_settings.rpt_tone))
cfg_grp.append(rs)
rs = RadioSetting("rpt_hold", "Repeater Hold Time",
RadioSettingValueList(HOLD_TIMES,
current_index=_settings.
rpt_hold))
cfg_grp.append(rs)
rs = RadioSetting("scan_det", "Scan DET",
RadioSettingValueBoolean(_settings.scan_det))
cfg_grp.append(rs)
rs = RadioSetting(
"sc_qt", "SC-QT",
RadioSettingValueList(
SCQT_LIST, current_index=_settings.smuteset))
cfg_grp.append(rs)
rs = RadioSetting("smuteset", "SubFreq Mute",
RadioSettingValueList(SMUTESET_LIST,
current_index=_settings.
smuteset))
cfg_grp.append(rs)
_pwd = "".join(map(chr, _settings.mode_sw_pwd))
val = RadioSettingValueString(0, 6, _pwd)
val.set_mutable(True)
rs = RadioSetting("mode_sw_pwd", "Mode Switch Password", val)
cfg_grp.append(rs)
_pwd = "".join(map(chr, _settings.reset_pwd))
val = RadioSettingValueString(0, 6, _pwd)
val.set_mutable(True)
rs = RadioSetting("reset_pwd", "Reset Password", val)
cfg_grp.append(rs)
#
# VFO A Settings
#
rs = RadioSetting("vfoa_mode", "VFO A Workmode",
RadioSettingValueList(WORKMODE_LIST,
current_index=_settings.
workmode_a))
vfoa_grp.append(rs)
rs = RadioSetting("vfoa_chan", "VFO A Channel",
RadioSettingValueInteger(1, 999, _settings.work_cha))
vfoa_grp.append(rs)
rs = RadioSetting("rxfreqa", "VFO A Rx Frequency",
RadioSettingValueInteger(
134000000, 520000000, _vfoa.rxfreq * 10, 5000))
vfoa_grp.append(rs)
rs = RadioSetting("txoffa", "VFO A Tx Offset",
RadioSettingValueInteger(
0, 520000000, _vfoa.txoffset * 10, 5000))
vfoa_grp.append(rs)
# u16 rxtone;
# u16 txtone;
rs = RadioSetting("vfoa_power", "VFO A Power",
RadioSettingValueList(
POWER_LIST, current_index=_vfoa.power))
vfoa_grp.append(rs)
# shift_dir:2
rs = RadioSetting("vfoa_iswide", "VFO A NBFM",
RadioSettingValueList(
BANDWIDTH_LIST, current_index=_vfoa.iswide))
vfoa_grp.append(rs)
rs = RadioSetting("vfoa_mute_mode", "VFO A Mute",
RadioSettingValueList(
SPMUTE_LIST, current_index=_vfoa.mute_mode))
vfoa_grp.append(rs)
rs = RadioSetting("vfoa_step", "VFO A Step (kHz)",
RadioSettingValueList(
STEP_LIST, current_index=_vfoa.step))
vfoa_grp.append(rs)
rs = RadioSetting("vfoa_squelch", "VFO A Squelch",
RadioSettingValueList(
LIST_10, current_index=_vfoa.squelch))
vfoa_grp.append(rs)
rs = RadioSetting("bcl_a", "Busy Channel Lock-out A",
RadioSettingValueBoolean(_settings.bcl_a))
vfoa_grp.append(rs)
#
# VFO B Settings
#
rs = RadioSetting("vfob_mode", "VFO B Workmode",
RadioSettingValueList(
WORKMODE_LIST,
current_index=_settings.workmode_b))
vfob_grp.append(rs)
rs = RadioSetting("vfob_chan", "VFO B Channel",
RadioSettingValueInteger(1, 999, _settings.work_chb))
vfob_grp.append(rs)
rs = RadioSetting("rxfreqb", "VFO B Rx Frequency",
RadioSettingValueInteger(
134000000, 520000000, _vfob.rxfreq * 10, 5000))
vfob_grp.append(rs)
rs = RadioSetting("txoffb", "VFO B Tx Offset",
RadioSettingValueInteger(
0, 520000000, _vfob.txoffset * 10, 5000))
vfob_grp.append(rs)
# u16 rxtone;
# u16 txtone;
rs = RadioSetting("vfob_power", "VFO B Power",
RadioSettingValueList(
POWER_LIST, current_index=_vfob.power))
vfob_grp.append(rs)
# shift_dir:2
rs = RadioSetting("vfob_iswide", "VFO B NBFM",
RadioSettingValueList(
BANDWIDTH_LIST, current_index=_vfob.iswide))
vfob_grp.append(rs)
rs = RadioSetting("vfob_mute_mode", "VFO B Mute",
RadioSettingValueList(
SPMUTE_LIST, current_index=_vfob.mute_mode))
vfob_grp.append(rs)
rs = RadioSetting("vfob_step", "VFO B Step (kHz)",
RadioSettingValueList(
STEP_LIST, current_index=_vfob.step))
vfob_grp.append(rs)
rs = RadioSetting("vfob_squelch", "VFO B Squelch",
RadioSettingValueList(
LIST_10, current_index=_vfob.squelch))
vfob_grp.append(rs)
rs = RadioSetting("bcl_b", "Busy Channel Lock-out B",
RadioSettingValueBoolean(_settings.bcl_b))
vfob_grp.append(rs)
#
# Key Settings
#
_msg = str(_settings.dispstr).split("\0")[0]
val = RadioSettingValueString(0, 15, _msg)
val.set_mutable(True)
rs = RadioSetting("dispstr", "Display Message", val)
key_grp.append(rs)
_ani = ""
for i in _settings.ani:
if i < 10:
_ani += chr(i + 0x30)
else:
break
val = RadioSettingValueString(0, 6, _ani)
val.set_mutable(True)
rs = RadioSetting("ani", "ANI code", val)
key_grp.append(rs)
rs = RadioSetting("pf1_func", "PF1 Key function",
RadioSettingValueList(
PF1KEY_LIST,
current_index=self._memobj.settings.pf1_func))
key_grp.append(rs)
rs = RadioSetting("pf3_func", "PF3 Key function",
RadioSettingValueList(
PF3KEY_LIST,
current_index=self._memobj.settings.pf3_func))
key_grp.append(rs)
#
# Scan Group Settings
#
# settings:
# u8 scg_a;
# u8 scg_b;
#
# struct {
# u16 lower;
# u16 upper;
# } scan_groups[10];
#
# Call group settings
#
#
# Limits settings
#
rs = RadioSetting("urx_start", "UHF RX Lower Limit",
RadioSettingValueInteger(
400000000, 520000000,
self._memobj.uhf_limits.rx_start * 10, 5000))
lmt_grp.append(rs)
rs = RadioSetting("urx_stop", "UHF RX Upper Limit",
RadioSettingValueInteger(
400000000, 520000000,
self._memobj.uhf_limits.rx_stop * 10, 5000))
lmt_grp.append(rs)
rs = RadioSetting("utx_start", "UHF TX Lower Limit",
RadioSettingValueInteger(
400000000, 520000000,
self._memobj.uhf_limits.tx_start * 10, 5000))
lmt_grp.append(rs)
rs = RadioSetting("utx_stop", "UHF TX Upper Limit",
RadioSettingValueInteger(
400000000, 520000000,
self._memobj.uhf_limits.tx_stop * 10, 5000))
lmt_grp.append(rs)
rs = RadioSetting("vrx_start", "VHF RX Lower Limit",
RadioSettingValueInteger(
134000000, 174997500,
self._memobj.vhf_limits.rx_start * 10, 5000))
lmt_grp.append(rs)
rs = RadioSetting("vrx_stop", "VHF RX Upper Limit",
RadioSettingValueInteger(
134000000, 174997500,
self._memobj.vhf_limits.rx_stop * 10, 5000))
lmt_grp.append(rs)
rs = RadioSetting("vtx_start", "VHF TX Lower Limit",
RadioSettingValueInteger(
134000000, 174997500,
self._memobj.vhf_limits.tx_start * 10, 5000))
lmt_grp.append(rs)
rs = RadioSetting("vtx_stop", "VHF TX Upper Limit",
RadioSettingValueInteger(
134000000, 174997500,
self._memobj.vhf_limits.tx_stop * 10, 5000))
lmt_grp.append(rs)
#
# OEM info
#
def _decode(lst):
_str = ''.join([chr(c) for c in lst
if chr(c) in chirp_common.CHARSET_ASCII])
return _str
_str = _decode(self._memobj.oem_info.model)
val = RadioSettingValueString(0, 15, _str)
val.set_mutable(False)
rs = RadioSetting("model", "Model", val)
oem_grp.append(rs)
_str = _decode(self._memobj.oem_info.oem1)
val = RadioSettingValueString(0, 15, _str)
val.set_mutable(False)
rs = RadioSetting("oem1", "OEM String 1", val)
oem_grp.append(rs)
_str = _decode(self._memobj.oem_info.oem2)
val = RadioSettingValueString(0, 15, _str)
val.set_mutable(False)
rs = RadioSetting("oem2", "OEM String 2", val)
oem_grp.append(rs)
_str = _decode(self._memobj.oem_info.version)
val = RadioSettingValueString(0, 15, _str)
val.set_mutable(False)
rs = RadioSetting("version", "Software Version", val)
oem_grp.append(rs)
_str = _decode(self._memobj.oem_info.date)
val = RadioSettingValueString(0, 15, _str)
val.set_mutable(False)
rs = RadioSetting("date", "OEM Date", val)
oem_grp.append(rs)
return group
def get_settings(self):
try:
return self._get_settings()
except Exception:
import traceback
LOG.error("Failed to parse settings: %s", traceback.format_exc())
return None
chirp-20261030/chirp/drivers/kguv8dplus.py 0000644 0001750 0001750 00000113613 15172631605 017316 0 ustar hibby hibby # Copyright 2017 Krystian Struzik
# Based on Ron Wellsted driver for Wouxun KG-UV8D.
# KG-UV8D Plus model has all serial data encrypted.
# Figured out how the data is encrypted and implement
# serial data encryption and decryption functions.
# The algorithm of decryption works like this:
# - the first byte of data stream is XOR by const 57h
# - each next byte is encoded by previous byte using the XOR
# including the checksum (e.g data[i - 1] xor data[i])
# I also changed the data structure to fit radio memory
# and implement set_settings function.
#
# 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 .
"""Wouxun KG-UV8D Plus radio management module"""
import struct
import time
import logging
from chirp import util, chirp_common, bitwise, memmap, errors, directory
from chirp import checksum
from chirp.settings import RadioSetting, RadioSettingGroup, \
RadioSettingValueBoolean, RadioSettingValueList, \
RadioSettingValueInteger, RadioSettingValueString, \
RadioSettings
from chirp.kenwood_tone import KenwoodToneModel
LOG = logging.getLogger(__name__)
CMD_ID = 128
CMD_END = 129
CMD_RD = 130
CMD_WR = 131
MEM_VALID = 158
AB_LIST = ["A", "B"]
STEPS = [2.5, 5.0, 6.25, 10.0, 12.5, 25.0, 50.0, 100.0]
STEP_LIST = [str(x) for x in STEPS]
ROGER_LIST = ["Off", "Begin", "End", "Both"]
TIMEOUT_LIST = ["Off"] + [str(x) + "s" for x in range(15, 901, 15)]
VOX_LIST = ["Off"] + ["%s" % x for x in range(1, 10)]
BANDWIDTH_LIST = ["Narrow", "Wide"]
VOICE_LIST = ["Off", "On"]
LANGUAGE_LIST = ["Chinese", "English"]
SCANMODE_LIST = ["TO", "CO", "SE"]
PF1KEY_LIST = ["Call", "VFTX"]
PF3KEY_LIST = ["Disable", "Scan", "Lamp", "Tele Alarm", "SOS-CH", "Radio"]
WORKMODE_LIST = ["VFO", "Channel No.", "Ch. No.+Freq.", "Ch. No.+Name"]
BACKLIGHT_LIST = ["Always On"] + [str(x) + "s" for x in range(1, 21)] + \
["Always Off"]
OFFSET_LIST = ["+", "-"]
PONMSG_LIST = ["Bitmap", "Battery Volts"]
SPMUTE_LIST = ["QT", "QT+DTMF", "QT*DTMF"]
DTMFST_LIST = ["DT-ST", "ANI-ST", "DT-ANI", "Off"]
DTMF_TIMES = ["%s" % x for x in range(50, 501, 10)]
RPTSET_LIST = ["X-TWRPT", "X-DIRRPT"]
ALERTS = [1750, 2100, 1000, 1450]
ALERTS_LIST = [str(x) for x in ALERTS]
PTTID_LIST = ["Begin", "End", "Both"]
LIST_10 = ["Off"] + ["%s" % x for x in range(1, 11)]
SCANGRP_LIST = ["All"] + ["%s" % x for x in range(1, 11)]
SCQT_LIST = ["Decoder", "Encoder", "All"]
SMUTESET_LIST = ["Off", "Tx", "Rx", "Tx/Rx"]
POWER_LIST = ["Lo", "Hi"]
HOLD_TIMES = ["Off"] + ["%s" % x for x in range(100, 5001, 100)]
RPTMODE_LIST = ["Radio", "Repeater"]
# memory slot 0 is not used, start at 1 (so need 1000 slots, not 999)
# structure elements whose name starts with x are currently unidentified
_MEM_FORMAT = """
#seekto 0x0044;
struct {
u32 rx_start;
u32 rx_stop;
u32 tx_start;
u32 tx_stop;
} uhf_limits;
#seekto 0x0054;
struct {
u32 rx_start;
u32 rx_stop;
u32 tx_start;
u32 tx_stop;
} vhf_limits;
#seekto 0x0400;
struct {
u8 oem1[8];
u8 unknown[2];
u8 unknown2[10];
u8 unknown3[10];
u8 unknown4[8];
u8 model[10];
u8 version[6];
u8 date[8];
u8 unknown5[1];
u8 oem2[8];
} oem_info;
#seekto 0x0480;
struct {
u16 lower;
u16 upper;
} scan_groups[10];
#seekto 0x0500;
struct {
u8 call_code[6];
} call_groups[20];
#seekto 0x0580;
struct {
char call_name[6];
} call_group_name[20];
#seekto 0x0800;
struct {
u8 ponmsg;
char dispstr[15];
u8 x0810;
u8 x0811;
u8 x0812;
u8 x0813;
u8 x0814;
u8 voice;
u8 timeout;
u8 toalarm;
u8 channel_menu;
u8 power_save;
u8 autolock;
u8 keylock;
u8 beep;
u8 stopwatch;
u8 vox;
u8 scan_rev;
u8 backlight;
u8 roger_beep;
u8 x0822[6];
u8 x0823[6];
u16 pri_ch;
u8 ani_sw;
u8 ptt_delay;
u8 ani_code[6];
u8 dtmf_st;
u8 bcl_a;
u8 bcl_b;
u8 ptt_id;
u8 prich_sw;
u8 rpt_set;
u8 rpt_spk;
u8 rpt_ptt;
u8 alert;
u8 pf1_func;
u8 pf3_func;
u8 x0843;
u8 workmode_a;
u8 workmode_b;
u8 dtmf_tx_time;
u8 dtmf_interval;
u8 main_ab;
u16 work_cha;
u16 work_chb;
u8 x084d;
u8 x084e;
u8 x084f;
u8 x0850;
u8 x0851;
u8 x0852;
u8 x0853;
u8 x0854;
u8 rpt_mode;
u8 language;
u8 x0857;
u8 x0858;
u8 x0859;
u8 x085a;
u8 x085b;
u8 x085c;
u8 x085d;
u8 x085e;
u8 single_display;
u8 ring_time;
u8 scg_a;
u8 scg_b;
u8 x0863;
u8 rpt_tone;
u8 rpt_hold;
u8 scan_det;
u8 sc_qt;
u8 x0868;
u8 smuteset;
u8 callcode;
} settings;
#seekto 0x0880;
struct {
u32 rxfreq;
u32 txoffset;
u16 rxtone;
u16 txtone;
u8 scrambler:4,
unknown1:2,
power:1,
unknown2:1;
u8 unknown3:1,
shift_dir:2,
unknown4:1,
compander:1,
mute_mode:2,
iswide:1;
u8 step;
u8 squelch;
} vfoa;
#seekto 0x08c0;
struct {
u32 rxfreq;
u32 txoffset;
u16 rxtone;
u16 txtone;
u8 scrambler:4,
unknown1:2,
power:1,
unknown2:1;
u8 unknown3:1,
shift_dir:2,
unknown4:1,
compander:1,
mute_mode:2,
iswide:1;
u8 step;
u8 squelch;
} vfob;
#seekto 0x0900;
struct {
u32 rxfreq;
u32 txfreq;
u16 rxtone;
u16 txtone;
u8 scrambler:4,
unknown1:2,
power:1,
unknown2:1;
u8 unknown3:2,
scan_add:1,
unknown4:1,
compander:1,
mute_mode:2,
iswide:1;
u16 padding;
} memory[1000];
#seekto 0x4780;
struct {
u8 name[8];
u8 unknown[4];
} names[1000];
#seekto 0x7670;
u8 valid[1000];
"""
# Support for the Wouxun KG-UV8D Plus radio
# Serial coms are at 19200 baud
# The data is passed in variable length records
# Record structure:
# Offset Usage
# 0 start of record (\x7a)
# 1 Command (\x80 Identify \x81 End/Reboot \x82 Read \x83 Write)
# 2 direction (\xff PC-> Radio, \x00 Radio -> PC)
# 3 length of payload (excluding header/checksum) (n)
# 4 payload (n bytes)
# 4+n+1 checksum - byte sum (% 256) of bytes 1 -> 4+n
#
# Memory Read Records:
# the payload is 3 bytes, first 2 are offset (big endian),
# 3rd is number of bytes to read
# Memory Write Records:
# the maximum payload size (from the Wouxun software) seems to be 66 bytes
# (2 bytes location + 64 bytes data).
@directory.register
class KGUV8DPlusRadio(chirp_common.CloneModeRadio,
chirp_common.ExperimentalRadio):
"""Wouxun KG-UV8D Plus"""
VENDOR = "Wouxun"
MODEL = "KG-UV8D Plus"
_model = b"KG-UV8D"
_file_ident = b"kguv8dplus"
BAUD_RATE = 19200
POWER_LEVELS = [chirp_common.PowerLevel("L", watts=1),
chirp_common.PowerLevel("H", watts=5)]
_mmap = ""
_record_start = 0x7A
def __init__(self, pipe):
super().__init__(pipe)
self.tone_model = KenwoodToneModel(
dcs_base=0x4000,
pol_mask=0x2000,
tone_init=0x0000)
def _write_record(self, cmd, payload=b''):
_packet = struct.pack('BBBB', self._record_start, cmd, 0xFF,
len(payload))
cs = bytes([checksum.checksum_8bit(_packet[1:] + payload)])
_packet += self.encrypt(payload + cs)
LOG.debug("Sent:\n%s" % util.hexprint(_packet))
self.pipe.write(_packet)
def _read_record(self):
# read 4 chars for the header
_header = self.pipe.read(4)
if not _header:
raise errors.RadioNoResponse()
if len(_header) != 4:
raise errors.RadioError('Radio sent short header')
_length = struct.unpack('xxxB', _header)[0]
_packet = self.pipe.read(_length)
_rcs_xor = _packet[-1]
_packet = self.decrypt(_packet)
_cs = checksum.checksum_8bit(_header[1:])
_cs += checksum.checksum_8bit(_packet)
_cs %= 256
_rcs = self.strxor(self.pipe.read(1)[0], _rcs_xor)[0]
LOG.debug("_cs =%x", _cs)
LOG.debug("_rcs=%x", _rcs)
return (_rcs != _cs, _packet)
def decrypt(self, data):
result = b''
for i in range(len(data)-1, 0, -1):
result += self.strxor(data[i], data[i - 1])
result += self.strxor(data[0], 0x57)
return result[::-1]
def encrypt(self, data):
result = self.strxor(0x57, data[0])
for i in range(1, len(data), 1):
result += self.strxor(result[i - 1], data[i])
return result
def strxor(self, xora, xorb):
return bytes([xora ^ xorb])
# Identify the radio
#
# A Gotcha: the first identify packet returns a bad checksum, subsequent
# attempts return the correct checksum... (well it does on my radio!)
#
# The ID record returned by the radio also includes the current frequency range
# as 4 bytes big-endian in 10 Hz increments
#
# Offset
# 0:10 Model, zero padded (Use first 7 chars for 'KG-UV8D')
# 11:14 UHF rx lower limit (in units of 10 Hz)
# 15:18 UHF rx upper limit
# 19:22 UHF tx lower limit
# 23:26 UHF tx upper limit
# 27:30 VHF rx lower limit
# 31:34 VHF rx upper limit
# 35:38 VHF tx lower limit
# 39:42 VHF tx upper limit
#
@classmethod
def match_model(cls, filedata, filename):
return (cls._file_ident in b'kg' +
filedata[0x426:0x430].replace(b'(', b'').replace(b')',
b'').lower())
def _identify(self):
"""Do the identification dance"""
for _i in range(0, 10):
self._write_record(CMD_ID)
_chksum_err, _resp = self._read_record()
LOG.debug("Got:\n%s" % util.hexprint(_resp))
if _chksum_err:
LOG.error("Checksum error: retrying ident...")
time.sleep(0.100)
continue
LOG.debug("Model %s" % util.hexprint(_resp[0:7]))
if _resp[0:7] == self._model:
return
if len(_resp) == 0:
raise errors.RadioNoResponse()
else:
raise Exception("Unable to identify radio")
def _finish(self):
self._write_record(CMD_END)
def process_mmap(self):
self._memobj = bitwise.parse(_MEM_FORMAT, self._mmap)
def sync_in(self):
try:
self._mmap = self._download()
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):
self._upload()
# TODO: Load all memory.
# It would be smarter to only load the active areas and none of
# the padding/unused areas. Padding still need to be investigated.
def _download(self):
"""Talk to a Wouxun KG-UV8D Plus and do a download"""
try:
self._identify()
return self._do_download(0, 32768, 64)
except errors.RadioError:
raise
except Exception as e:
LOG.exception('Unknown error during download process')
raise errors.RadioError("Failed to communicate with radio: %s" % e)
def _do_download(self, start, end, blocksize):
# allocate & fill memory
image = b""
for i in range(start, end, blocksize):
req = struct.pack('>HB', i, blocksize)
self._write_record(CMD_RD, req)
cs_error, resp = self._read_record()
if cs_error:
LOG.debug(util.hexprint(resp))
raise Exception("Checksum error on read")
LOG.debug("Got:\n%s" % util.hexprint(resp))
image += resp[2:]
if self.status_fn:
status = chirp_common.Status()
status.cur = i
status.max = end
status.msg = "Cloning from radio"
self.status_fn(status)
self._finish()
return memmap.MemoryMapBytes(image)
def _upload(self):
"""Talk to a Wouxun KG-UV8D Plus and do a upload"""
try:
self._identify()
self._do_upload(0, 32768, 64)
except errors.RadioError:
raise
except Exception as e:
raise errors.RadioError("Failed to communicate with radio: %s" % e)
return
def _do_upload(self, start, end, blocksize):
ptr = start
for i in range(start, end, blocksize):
req = struct.pack('>H', i)
chunk = self.get_mmap()[ptr:ptr + blocksize]
self._write_record(CMD_WR, req + chunk)
LOG.debug(util.hexprint(req + chunk))
cserr, ack = self._read_record()
LOG.debug(util.hexprint(ack))
j = struct.unpack('>H', ack)[0]
if cserr or j != ptr:
raise Exception("Radio did not ack block %i" % ptr)
ptr += blocksize
if self.status_fn:
status = chirp_common.Status()
status.cur = i
status.max = end
status.msg = "Cloning to radio"
self.status_fn(status)
self._finish()
def get_features(self):
rf = chirp_common.RadioFeatures()
rf.has_settings = True
rf.has_ctone = True
rf.has_rx_dtcs = True
rf.has_cross = True
rf.has_tuning_step = False
rf.has_bank = False
rf.can_odd_split = True
rf.valid_skips = ["", "S"]
rf.valid_tmodes = ["", "Tone", "TSQL", "DTCS", "Cross"]
rf.valid_cross_modes = [
"Tone->Tone",
"Tone->DTCS",
"DTCS->Tone",
"DTCS->",
"->Tone",
"->DTCS",
"DTCS->DTCS",
]
rf.valid_modes = ["FM", "NFM"]
rf.valid_power_levels = self.POWER_LEVELS
rf.valid_name_length = 8
rf.valid_duplexes = ["", "-", "+", "split", "off"]
rf.valid_bands = [(134000000, 175000000), # supports 2m
(300000000, 520000000)] # supports 70cm
rf.valid_characters = chirp_common.CHARSET_ASCII
rf.valid_tuning_steps = STEPS
rf.memory_bounds = (1, 999) # 999 memories
return rf
@classmethod
def get_prompts(cls):
rp = chirp_common.RadioPrompts()
rp.experimental = ("This radio driver is currently under development. "
"There are no known issues with it, but you should "
"proceed with caution.")
return rp
def get_raw_memory(self, number):
return repr(self._memobj.memory[number])
def get_memory(self, number):
_mem = self._memobj.memory[number]
_nam = self._memobj.names[number]
mem = chirp_common.Memory()
mem.number = number
_valid = self._memobj.valid[mem.number]
LOG.debug("%d %s", number, _valid == MEM_VALID)
if _valid != MEM_VALID:
mem.empty = True
return mem
else:
mem.empty = False
mem.freq = int(_mem.rxfreq) * 10
if _mem.txfreq == 0xFFFFFFFF:
# TX freq not set
mem.duplex = "off"
mem.offset = 0
elif int(_mem.rxfreq) == int(_mem.txfreq):
mem.duplex = ""
mem.offset = 0
elif abs(int(_mem.rxfreq) * 10 - int(_mem.txfreq) * 10) > 70000000:
mem.duplex = "split"
mem.offset = int(_mem.txfreq) * 10
else:
mem.duplex = int(_mem.rxfreq) > int(_mem.txfreq) and "-" or "+"
mem.offset = abs(int(_mem.rxfreq) - int(_mem.txfreq)) * 10
for char in _nam.name:
if char != 0:
mem.name += chr(char)
mem.name = mem.name.rstrip()
self.tone_model.get_tone(_mem, mem)
mem.skip = "" if bool(_mem.scan_add) else "S"
mem.power = self.POWER_LEVELS[_mem.power]
mem.mode = _mem.iswide and "FM" or "NFM"
return mem
def set_memory(self, mem):
number = mem.number
_mem = self._memobj.memory[number]
_nam = self._memobj.names[number]
if mem.empty:
_mem.set_raw("\x00" * (_mem.size() // 8))
self._memobj.valid[number] = 0
self._memobj.names[number].set_raw("\x00" * (_nam.size() // 8))
return
_mem.rxfreq = int(mem.freq / 10)
if mem.duplex == "off":
_mem.txfreq = 0xFFFFFFFF
elif mem.duplex == "split":
_mem.txfreq = int(mem.offset / 10)
elif mem.duplex == "off":
for i in range(0, 4):
_mem.txfreq[i].set_raw("\xFF")
elif mem.duplex == "+":
_mem.txfreq = int(mem.freq / 10) + int(mem.offset / 10)
elif mem.duplex == "-":
_mem.txfreq = int(mem.freq / 10) - int(mem.offset / 10)
else:
_mem.txfreq = int(mem.freq / 10)
_mem.scan_add = int(mem.skip != "S")
_mem.iswide = int(mem.mode == "FM")
# set the tone
self.tone_model.set_tone(mem, _mem)
# set the scrambler and compander to off by default
_mem.scrambler = 0
_mem.compander = 0
# set the power
if mem.power:
_mem.power = self.POWER_LEVELS.index(mem.power)
else:
_mem.power = True
# set to mute mode to QT (not QT+DTMF or QT*DTMF) by default
_mem.mute_mode = 0
for i in range(0, len(_nam.name)):
if i < len(mem.name) and mem.name[i]:
_nam.name[i] = ord(mem.name[i])
else:
_nam.name[i] = 0x0
self._memobj.valid[mem.number] = MEM_VALID
def _get_settings(self):
_settings = self._memobj.settings
_vfoa = self._memobj.vfoa
_vfob = self._memobj.vfob
cfg_grp = RadioSettingGroup("cfg_grp", "Configuration")
vfoa_grp = RadioSettingGroup("vfoa_grp", "VFO A Settings")
vfob_grp = RadioSettingGroup("vfob_grp", "VFO B Settings")
key_grp = RadioSettingGroup("key_grp", "Key Settings")
lmt_grp = RadioSettingGroup("lmt_grp", "Frequency Limits")
uhf_lmt_grp = RadioSettingGroup("uhf_lmt_grp", "UHF")
vhf_lmt_grp = RadioSettingGroup("vhf_lmt_grp", "VHF")
oem_grp = RadioSettingGroup("oem_grp", "OEM Info")
lmt_grp.append(uhf_lmt_grp)
lmt_grp.append(vhf_lmt_grp)
group = RadioSettings(cfg_grp, vfoa_grp, vfob_grp,
key_grp, lmt_grp, oem_grp)
#
# Configuration Settings
#
rs = RadioSetting("channel_menu", "Menu available in channel mode",
RadioSettingValueBoolean(_settings.channel_menu))
cfg_grp.append(rs)
rs = RadioSetting("ponmsg", "Poweron message",
RadioSettingValueList(
PONMSG_LIST, current_index=_settings.ponmsg))
cfg_grp.append(rs)
rs = RadioSetting("voice", "Voice Guide",
RadioSettingValueBoolean(_settings.voice))
cfg_grp.append(rs)
rs = RadioSetting("language", "Language",
RadioSettingValueList(LANGUAGE_LIST,
current_index=_settings.
language))
cfg_grp.append(rs)
rs = RadioSetting("timeout", "Timeout Timer",
RadioSettingValueList(
TIMEOUT_LIST, current_index=_settings.timeout))
cfg_grp.append(rs)
rs = RadioSetting("toalarm", "Timeout Alarm",
RadioSettingValueInteger(0, 10, _settings.toalarm))
cfg_grp.append(rs)
rs = RadioSetting(
"roger_beep", "Roger Beep",
RadioSettingValueList(
ROGER_LIST, current_index=_settings.roger_beep))
cfg_grp.append(rs)
rs = RadioSetting("power_save", "Power save",
RadioSettingValueBoolean(_settings.power_save))
cfg_grp.append(rs)
rs = RadioSetting("autolock", "Autolock",
RadioSettingValueBoolean(_settings.autolock))
cfg_grp.append(rs)
rs = RadioSetting("keylock", "Keypad Lock",
RadioSettingValueBoolean(_settings.keylock))
cfg_grp.append(rs)
rs = RadioSetting("beep", "Keypad Beep",
RadioSettingValueBoolean(_settings.beep))
cfg_grp.append(rs)
rs = RadioSetting("stopwatch", "Stopwatch",
RadioSettingValueBoolean(_settings.stopwatch))
cfg_grp.append(rs)
rs = RadioSetting("backlight", "Backlight",
RadioSettingValueList(BACKLIGHT_LIST,
current_index=_settings.
backlight))
cfg_grp.append(rs)
rs = RadioSetting("dtmf_st", "DTMF Sidetone",
RadioSettingValueList(DTMFST_LIST,
current_index=_settings.
dtmf_st))
cfg_grp.append(rs)
rs = RadioSetting("ani_sw", "ANI-ID Switch",
RadioSettingValueBoolean(_settings.ani_sw))
cfg_grp.append(rs)
rs = RadioSetting(
"ptt_id", "PTT-ID Delay",
RadioSettingValueList(
PTTID_LIST, current_index=_settings.ptt_id))
cfg_grp.append(rs)
rs = RadioSetting(
"ring_time", "Ring Time",
RadioSettingValueList(
LIST_10, current_index=_settings.ring_time))
cfg_grp.append(rs)
rs = RadioSetting("scan_rev", "Scan Mode",
RadioSettingValueList(SCANMODE_LIST,
current_index=_settings.
scan_rev))
cfg_grp.append(rs)
rs = RadioSetting("vox", "VOX",
RadioSettingValueList(LIST_10,
current_index=_settings.vox))
cfg_grp.append(rs)
rs = RadioSetting("prich_sw", "Priority Channel Switch",
RadioSettingValueBoolean(_settings.prich_sw))
cfg_grp.append(rs)
rs = RadioSetting("pri_ch", "Priority Channel",
RadioSettingValueInteger(1, 999, _settings.pri_ch))
cfg_grp.append(rs)
rs = RadioSetting("rpt_mode", "Radio Mode",
RadioSettingValueList(RPTMODE_LIST,
current_index=_settings.
rpt_mode))
cfg_grp.append(rs)
rs = RadioSetting("rpt_set", "Repeater Setting",
RadioSettingValueList(RPTSET_LIST,
current_index=_settings.
rpt_set))
cfg_grp.append(rs)
rs = RadioSetting("rpt_spk", "Repeater Mode Speaker",
RadioSettingValueBoolean(_settings.rpt_spk))
cfg_grp.append(rs)
rs = RadioSetting("rpt_ptt", "Repeater PTT",
RadioSettingValueBoolean(_settings.rpt_ptt))
cfg_grp.append(rs)
rs = RadioSetting("dtmf_tx_time", "DTMF Tx Duration",
RadioSettingValueList(DTMF_TIMES,
current_index=_settings.
dtmf_tx_time))
cfg_grp.append(rs)
rs = RadioSetting("dtmf_interval", "DTMF Interval",
RadioSettingValueList(DTMF_TIMES,
current_index=_settings.
dtmf_interval))
cfg_grp.append(rs)
rs = RadioSetting("alert", "Alert Tone",
RadioSettingValueList(ALERTS_LIST,
current_index=_settings.alert))
cfg_grp.append(rs)
rs = RadioSetting("rpt_tone", "Repeater Tone",
RadioSettingValueBoolean(_settings.rpt_tone))
cfg_grp.append(rs)
rs = RadioSetting("rpt_hold", "Repeater Hold Time",
RadioSettingValueList(HOLD_TIMES,
current_index=_settings.
rpt_hold))
cfg_grp.append(rs)
rs = RadioSetting("scan_det", "Scan DET",
RadioSettingValueBoolean(_settings.scan_det))
cfg_grp.append(rs)
rs = RadioSetting("sc_qt", "SC-QT",
RadioSettingValueList(SCQT_LIST,
current_index=_settings.sc_qt))
cfg_grp.append(rs)
rs = RadioSetting("smuteset", "SubFreq Mute",
RadioSettingValueList(SMUTESET_LIST,
current_index=_settings.
smuteset))
cfg_grp.append(rs)
# VFO A Settings
#
rs = RadioSetting(
"workmode_a", "VFO A Workmode",
RadioSettingValueList(
WORKMODE_LIST, current_index=_settings.workmode_a))
vfoa_grp.append(rs)
rs = RadioSetting("work_cha", "VFO A Channel",
RadioSettingValueInteger(1, 999, _settings.work_cha))
vfoa_grp.append(rs)
rs = RadioSetting("vfoa.rxfreq", "VFO A Rx Frequency",
RadioSettingValueInteger(
134000000, 520000000, _vfoa.rxfreq * 10, 5000))
vfoa_grp.append(rs)
rs = RadioSetting("vfoa.txoffset", "VFO A Tx Offset",
RadioSettingValueInteger(
0, 520000000, _vfoa.txoffset * 10, 5000))
vfoa_grp.append(rs)
# u16 rxtone;
# u16 txtone;
rs = RadioSetting("vfoa.power", "VFO A Power",
RadioSettingValueList(
POWER_LIST, current_index=_vfoa.power))
vfoa_grp.append(rs)
# shift_dir:2
rs = RadioSetting("vfoa.iswide", "VFO A NBFM",
RadioSettingValueList(
BANDWIDTH_LIST, current_index=_vfoa.iswide))
vfoa_grp.append(rs)
rs = RadioSetting("vfoa.mute_mode", "VFO A Mute",
RadioSettingValueList(
SPMUTE_LIST, current_index=_vfoa.mute_mode))
vfoa_grp.append(rs)
rs = RadioSetting("vfoa.step", "VFO A Step (kHz)",
RadioSettingValueList(
STEP_LIST, current_index=_vfoa.step))
vfoa_grp.append(rs)
rs = RadioSetting("vfoa.squelch", "VFO A Squelch",
RadioSettingValueList(
LIST_10, current_index=_vfoa.squelch))
vfoa_grp.append(rs)
rs = RadioSetting("bcl_a", "Busy Channel Lock-out A",
RadioSettingValueBoolean(_settings.bcl_a))
vfoa_grp.append(rs)
# VFO B Settings
#
rs = RadioSetting(
"workmode_b", "VFO B Workmode",
RadioSettingValueList(
WORKMODE_LIST, current_index=_settings.workmode_b))
vfob_grp.append(rs)
rs = RadioSetting("work_chb", "VFO B Channel",
RadioSettingValueInteger(1, 999, _settings.work_chb))
vfob_grp.append(rs)
rs = RadioSetting("vfob.rxfreq", "VFO B Rx Frequency",
RadioSettingValueInteger(
134000000, 520000000, _vfob.rxfreq * 10, 5000))
vfob_grp.append(rs)
rs = RadioSetting("vfob.txoffset", "VFO B Tx Offset",
RadioSettingValueInteger(
0, 520000000, _vfob.txoffset * 10, 5000))
vfob_grp.append(rs)
# u16 rxtone;
# u16 txtone;
rs = RadioSetting("vfob.power", "VFO B Power",
RadioSettingValueList(
POWER_LIST, current_index=_vfob.power))
vfob_grp.append(rs)
# shift_dir:2
rs = RadioSetting("vfob.iswide", "VFO B NBFM",
RadioSettingValueList(
BANDWIDTH_LIST, current_index=_vfob.iswide))
vfob_grp.append(rs)
rs = RadioSetting("vfob.mute_mode", "VFO B Mute",
RadioSettingValueList(
SPMUTE_LIST, current_index=_vfob.mute_mode))
vfob_grp.append(rs)
rs = RadioSetting("vfob.step", "VFO B Step (kHz)",
RadioSettingValueList(
STEP_LIST, current_index=_vfob.step))
vfob_grp.append(rs)
rs = RadioSetting("vfob.squelch", "VFO B Squelch",
RadioSettingValueList(
LIST_10, current_index=_vfob.squelch))
vfob_grp.append(rs)
rs = RadioSetting("bcl_b", "Busy Channel Lock-out B",
RadioSettingValueBoolean(_settings.bcl_b))
vfob_grp.append(rs)
# Key Settings
#
_msg = str(_settings.dispstr).split("\0")[0]
val = RadioSettingValueString(0, 15, _msg)
val.set_mutable(True)
rs = RadioSetting("dispstr", "Display Message", val)
key_grp.append(rs)
dtmfchars = "0123456789"
_codeobj = _settings.ani_code
_code = "".join([dtmfchars[x] for x in _codeobj if int(x) < 0x0A])
val = RadioSettingValueString(3, 6, _code, False)
val.set_charset(dtmfchars)
rs = RadioSetting("ani_code", "ANI Code", val)
def apply_ani_id(setting, obj):
value = []
for j in range(0, 6):
try:
value.append(dtmfchars.index(str(setting.value)[j]))
except IndexError:
value.append(0xFF)
obj.ani_code = value
rs.set_apply_callback(apply_ani_id, _settings)
key_grp.append(rs)
rs = RadioSetting("pf1_func", "PF1 Key function",
RadioSettingValueList(
PF1KEY_LIST,
current_index=_settings.pf1_func))
key_grp.append(rs)
rs = RadioSetting("pf3_func", "PF3 Key function",
RadioSettingValueList(
PF3KEY_LIST,
current_index=_settings.pf3_func))
key_grp.append(rs)
#
# Limits settings
#
rs = RadioSetting("uhf_limits.rx_start", "UHF RX Lower Limit",
RadioSettingValueInteger(
300000000, 520000000,
self._memobj.uhf_limits.rx_start * 10, 5000))
uhf_lmt_grp.append(rs)
rs = RadioSetting("uhf_limits.rx_stop", "UHF RX Upper Limit",
RadioSettingValueInteger(
300000000, 520000000,
self._memobj.uhf_limits.rx_stop * 10, 5000))
uhf_lmt_grp.append(rs)
rs = RadioSetting("uhf_limits.tx_start", "UHF TX Lower Limit",
RadioSettingValueInteger(
400000000, 520000000,
self._memobj.uhf_limits.tx_start * 10, 5000))
uhf_lmt_grp.append(rs)
rs = RadioSetting("uhf_limits.tx_stop", "UHF TX Upper Limit",
RadioSettingValueInteger(
400000000, 520000000,
self._memobj.uhf_limits.tx_stop * 10, 5000))
uhf_lmt_grp.append(rs)
rs = RadioSetting("vhf_limits.rx_start", "VHF RX Lower Limit",
RadioSettingValueInteger(
134000000, 174997500,
self._memobj.vhf_limits.rx_start * 10, 5000))
vhf_lmt_grp.append(rs)
rs = RadioSetting("vhf_limits.rx_stop", "VHF RX Upper Limit",
RadioSettingValueInteger(
134000000, 174997500,
self._memobj.vhf_limits.rx_stop * 10, 5000))
vhf_lmt_grp.append(rs)
rs = RadioSetting("vhf_limits.tx_start", "VHF TX Lower Limit",
RadioSettingValueInteger(
134000000, 174997500,
self._memobj.vhf_limits.tx_start * 10, 5000))
vhf_lmt_grp.append(rs)
rs = RadioSetting("vhf_limits.tx_stop", "VHF TX Upper Limit",
RadioSettingValueInteger(
134000000, 174997500,
self._memobj.vhf_limits.tx_stop * 10, 5000))
vhf_lmt_grp.append(rs)
#
# OEM info
#
def _decode(lst):
_str = ''.join([chr(c) for c in lst
if chr(c) in chirp_common.CHARSET_ASCII])
return _str
def do_nothing(setting, obj):
return
_str = _decode(self._memobj.oem_info.model)
val = RadioSettingValueString(0, 15, _str)
val.set_mutable(False)
rs = RadioSetting("oem_info.model", "Model", val)
rs.set_apply_callback(do_nothing, _settings)
oem_grp.append(rs)
_str = _decode(self._memobj.oem_info.oem1)
val = RadioSettingValueString(0, 15, _str)
val.set_mutable(False)
rs = RadioSetting("oem_info.oem1", "OEM String 1", val)
rs.set_apply_callback(do_nothing, _settings)
oem_grp.append(rs)
_str = _decode(self._memobj.oem_info.oem2)
val = RadioSettingValueString(0, 15, _str)
val.set_mutable(False)
rs = RadioSetting("oem_info.oem2", "OEM String 2", val)
rs.set_apply_callback(do_nothing, _settings)
oem_grp.append(rs)
_str = _decode(self._memobj.oem_info.version)
val = RadioSettingValueString(0, 15, _str)
val.set_mutable(False)
rs = RadioSetting("oem_info.version", "Software Version", val)
rs.set_apply_callback(do_nothing, _settings)
oem_grp.append(rs)
_str = _decode(self._memobj.oem_info.date)
val = RadioSettingValueString(0, 15, _str)
val.set_mutable(False)
rs = RadioSetting("oem_info.date", "OEM Date", val)
rs.set_apply_callback(do_nothing, _settings)
oem_grp.append(rs)
return group
def get_settings(self):
try:
return self._get_settings()
except Exception:
import traceback
LOG.error("Failed to parse settings: %s", traceback.format_exc())
return None
def set_settings(self, settings):
for element in settings:
if not isinstance(element, RadioSetting):
self.set_settings(element)
continue
else:
try:
if "." in element.get_name():
bits = element.get_name().split(".")
obj = self._memobj
for bit in bits[:-1]:
obj = getattr(obj, bit)
setting = bits[-1]
else:
obj = self._memobj.settings
setting = element.get_name()
if element.has_apply_callback():
LOG.debug("Using apply callback")
element.run_apply_callback()
else:
LOG.debug("Setting %s = %s" % (setting, element.value))
if self._is_freq(element):
setattr(obj, setting, int(element.value)/10)
else:
setattr(obj, setting, element.value)
except Exception:
LOG.debug(element.get_name())
raise
def _is_freq(self, element):
return "rxfreq" in element.get_name() \
or "txoffset" in element.get_name() \
or "rx_start" in element.get_name() \
or "rx_stop" in element.get_name() \
or "tx_start" in element.get_name() \
or "tx_stop" in element.get_name()
chirp-20261030/chirp/drivers/kguv8e.py 0000644 0001750 0001750 00000116075 15257672455 016434 0 ustar hibby hibby # Copyright 2019 Pavel Milanes CO7WT
#
# Based on the work of Krystian Struzik
# who figured out the crypt used and made possible the
# Wuoxun KG-UV8D Plus driver, in which this work is based.
#
# 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 .
"""Wouxun KG-UV8E radio management module"""
import struct
import time
import logging
from chirp import checksum, util, chirp_common, bitwise, memmap, errors, directory
from chirp.settings import RadioSetting, RadioSettingGroup, \
RadioSettingValueBoolean, RadioSettingValueList, \
RadioSettingValueInteger, RadioSettingValueString, \
RadioSettings
from chirp.kenwood_tone import KenwoodToneModel
LOG = logging.getLogger(__name__)
CMD_ID = 128
CMD_END = 129
CMD_RD = 130
CMD_WR = 131
MEM_VALID = 158
AB_LIST = ["A", "B"]
STEPS = [2.5, 5.0, 6.25, 10.0, 12.5, 25.0, 50.0, 100.0]
STEP_LIST = [str(x) for x in STEPS]
ROGER_LIST = ["Off", "Begin", "End", "Both"]
TIMEOUT_LIST = ["Off"] + [str(x) + "s" for x in range(15, 901, 15)]
VOX_LIST = ["Off"] + ["%s" % x for x in range(1, 10)]
BANDWIDTH_LIST = ["Narrow", "Wide"]
VOICE_LIST = ["Off", "On"]
LANGUAGE_LIST = ["Chinese", "English"]
SCANMODE_LIST = ["TO", "CO", "SE"]
PF1KEY_LIST = ["Call", "VFTX"]
PF3KEY_LIST = ["Disable", "Scan", "Lamp", "Tele Alarm", "SOS-CH", "Radio"]
WORKMODE_LIST = ["VFO", "Channel No.", "Ch. No.+Freq.", "Ch. No.+Name"]
BACKLIGHT_LIST = ["Always On"] + [str(x) + "s" for x in range(1, 21)] + \
["Always Off"]
OFFSET_LIST = ["+", "-"]
PONMSG_LIST = ["Bitmap", "Battery Volts"]
SPMUTE_LIST = ["QT", "QT+DTMF", "QT*DTMF"]
DTMFST_LIST = ["DT-ST", "ANI-ST", "DT-ANI", "Off"]
DTMF_TIMES = ["%s" % x for x in range(50, 501, 10)]
RPTSET_LIST = ["", "X-DIRRPT", "X-TWRPT"] # TODO < what is index 0?
ALERTS = [1750, 2100, 1000, 1450]
ALERTS_LIST = [str(x) for x in ALERTS]
PTTID_LIST = ["Begin", "End", "Both"]
LIST_10 = ["Off"] + ["%s" % x for x in range(1, 11)]
SCANGRP_LIST = ["All"] + ["%s" % x for x in range(1, 11)]
SCQT_LIST = ["Decoder", "Encoder", "All"]
SMUTESET_LIST = ["Off", "Tx", "Rx", "Tx/Rx"]
POWER_LIST = ["Lo", "Hi"]
HOLD_TIMES = ["Off"] + ["%s" % x for x in range(100, 5001, 100)]
RPTMODE_LIST = ["Radio", "Repeater"]
# memory slot 0 is not used, start at 1 (so need 1000 slots, not 999)
# structure elements whose name starts with x are currently unidentified
_MEM_FORMAT = """
#seekto 0x0030;
struct {
u32 rx_start;
u32 rx_stop;
u32 tx_start;
u32 tx_stop;
} vhf1_limits;
#seekto 0x0044;
struct {
u32 rx_start;
u32 rx_stop;
u32 tx_start;
u32 tx_stop;
} uhf_limits;
#seekto 0x0054;
struct {
u32 rx_start;
u32 rx_stop;
u32 tx_start;
u32 tx_stop;
} vhf_limits;
#seekto 0x0400;
struct {
u8 oem1[8];
u8 unknown[2];
u8 unknown2[10];
u8 unknown3[10];
u8 unknown4[8];
u8 model[10];
u8 version[6];
u8 date[8];
u8 unknown5[1];
u8 oem2[8];
} oem_info;
#seekto 0x0480;
struct {
u16 lower;
u16 upper;
} scan_groups[10];
#seekto 0x0500;
struct {
u8 call_code[6];
} call_groups[20];
#seekto 0x0580;
struct {
char call_name[6];
} call_group_name[20];
#seekto 0x0800;
struct {
u8 ponmsg;
char dispstr[15];
u8 x0810;
u8 x0811;
u8 x0812;
u8 x0813;
u8 x0814;
u8 voice;
u8 timeout;
u8 toalarm;
u8 channel_menu;
u8 power_save;
u8 autolock;
u8 keylock;
u8 beep;
u8 stopwatch;
u8 vox;
u8 scan_rev;
u8 backlight;
u8 roger_beep;
u8 x0822[6];
u8 x0823[6];
u16 pri_ch;
u8 ani_sw;
u8 ptt_delay;
u8 ani_code[6];
u8 dtmf_st;
u8 bcl_a;
u8 bcl_b;
u8 ptt_id;
u8 prich_sw;
u8 rpt_set;
u8 rpt_spk;
u8 rpt_ptt;
u8 alert;
u8 pf1_func;
u8 pf3_func;
u8 x0843;
u8 workmode_a;
u8 workmode_b;
u8 dtmf_tx_time;
u8 dtmf_interval;
u8 main_ab;
u16 work_cha;
u16 work_chb;
u8 x084d;
u8 x084e;
u8 x084f;
u8 x0850;
u8 x0851;
u8 x0852;
u8 x0853;
u8 x0854;
u8 rpt_mode;
u8 language;
u8 x0857;
u8 x0858;
u8 x0859;
u8 x085a;
u8 x085b;
u8 x085c;
u8 x085d;
u8 x085e;
u8 single_display;
u8 ring_time;
u8 scg_a;
u8 scg_b;
u8 x0863;
u8 rpt_tone;
u8 rpt_hold;
u8 scan_det;
u8 sc_qt;
u8 x0868;
u8 smuteset;
u8 callcode;
} settings;
#seekto 0x0880;
struct {
u32 rxfreq;
u32 txoffset;
u16 rxtone;
u16 txtone;
u8 scrambler:4,
unknown1:2,
power:1,
unknown2:1;
u8 unknown3:1,
shift_dir:2,
unknown4:1,
compander:1,
mute_mode:2,
iswide:1;
u8 step;
u8 squelch;
} vfoa;
#seekto 0x08c0;
struct {
u32 rxfreq;
u32 txoffset;
u16 rxtone;
u16 txtone;
u8 scrambler:4,
unknown1:2,
power:1,
unknown2:1;
u8 unknown3:1,
shift_dir:2,
unknown4:1,
compander:1,
mute_mode:2,
iswide:1;
u8 step;
u8 squelch;
} vfob;
#seekto 0x0900;
struct {
u32 rxfreq;
u32 txfreq;
u16 rxtone;
u16 txtone;
u8 scrambler:4,
unknown1:2,
power:1,
unknown2:1;
u8 unknown3:2,
scan_add:1,
unknown4:1,
compander:1,
mute_mode:2,
iswide:1;
u16 padding;
} memory[1000];
#seekto 0x4780;
struct {
u8 name[8];
u8 unknown[4];
} names[1000];
#seekto 0x7670;
u8 valid[1000];
"""
# Support for the Wouxun KG-UV8E radio
# Serial coms are at 19200 baud
# The data is passed in variable length records
# Record structure:
# Offset Usage
# 0 start of record (\x7a)
# 1 Command (\x80 Identify \x81 End/Reboot \x82 Read \x83 Write)
# 2 direction (\xff PC-> Radio, \x00 Radio -> PC)
# 3 length of payload (excluding header/checksum) (n)
# 4 payload (n bytes)
# 4+n+1 checksum - byte sum (% 256) of bytes 1 -> 4+n
#
# Memory Read Records:
# the payload is 3 bytes, first 2 are offset (big endian),
# 3rd is number of bytes to read
# Memory Write Records:
# the maximum payload size (from the Wouxun software) seems to be 66 bytes
# (2 bytes location + 64 bytes data).
@directory.register
class KGUV8ERadio(chirp_common.CloneModeRadio,
chirp_common.ExperimentalRadio):
"""Wouxun KG-UV8E"""
VENDOR = "Wouxun"
MODEL = "KG-UV8E"
def __init__(self, pipe):
super().__init__(pipe)
self.tone_model = KenwoodToneModel(
dcs_base=0x2800,
pol_mask=0x8000,
tone_init=0x0000
)
_model = b"KG-UV8D-A"
_file_ident = b"kguv8e" # lowercase
BAUD_RATE = 19200
@classmethod
def match_model(cls, filedata, filename):
ident = (b'kg' + filedata[0x426:0x430].replace(b'(', b'').replace(
b')', b'').lower())
return cls._file_ident in ident
POWER_LEVELS = [chirp_common.PowerLevel("L", watts=1),
chirp_common.PowerLevel("H", watts=5)]
_record_start = 0x7B
def _write_record(self, cmd, payload=b''):
_packet = struct.pack('BBBB', self._record_start, cmd, 0xFF,
len(payload))
cs = bytes([checksum.checksum_8bit(_packet[1:] + payload)])
_packet += self.encrypt(payload + cs)
LOG.debug("Sent:\n%s" % util.hexprint(_packet))
self.pipe.write(_packet)
def _read_record(self):
# read 4 chars for the header
_header = self.pipe.read(4)
if not _header:
raise errors.RadioNoResponse()
if len(_header) != 4:
raise errors.RadioError('Radio sent short header')
_length = struct.unpack('xxxB', _header)[0]
_packet = self.pipe.read(_length)
_rcs_xor = _packet[-1]
_packet = self.decrypt(_packet)
_cs = checksum.checksum_8bit(_header[1:])
_cs += checksum.checksum_8bit(_packet)
_cs %= 256
_rcs = self.strxor(self.pipe.read(1)[0], _rcs_xor)[0]
LOG.debug("_cs =%x", _cs)
LOG.debug("_rcs=%x", _rcs)
return (_rcs != _cs, _packet)
def decrypt(self, data):
result = b''
for i in range(len(data)-1, 0, -1):
result += self.strxor(data[i], data[i - 1])
result += self.strxor(data[0], 0x57)
return result[::-1]
def encrypt(self, data):
result = self.strxor(0x57, data[0])
for i in range(1, len(data), 1):
result += self.strxor(result[i - 1], data[i])
return result
def strxor(self, xora, xorb):
return bytes([xora ^ xorb])
# Identify the radio
#
# A Gotcha: the first identify packet returns a bad checksum, subsequent
# attempts return the correct checksum... (well it does on my radio!)
#
# The ID record returned by the radio also includes the current frequency range
# as 4 bytes big-endian in 10 Hz increments
#
# Offset
# 0:10 Model, zero padded (Use first 7 chars for 'KG-UV8D')
# 11:14 UHF rx lower limit (in units of 10 Hz)
# 15:18 UHF rx upper limit
# 19:22 UHF tx lower limit
# 23:26 UHF tx upper limit
# 27:30 VHF rx lower limit
# 31:34 VHF rx upper limit
# 35:38 VHF tx lower limit
# 39:42 VHF tx upper limit
def _identify(self):
"""Do the identification dance"""
for _i in range(0, 10):
self._write_record(CMD_ID)
_chksum_err, _resp = self._read_record()
LOG.debug("Got:\n%s" % util.hexprint(_resp))
if _chksum_err:
LOG.error("Checksum error: retrying ident...")
time.sleep(0.100)
continue
LOG.debug("Model %s" % util.hexprint(_resp[0:9]))
if _resp[0:9] == self._model:
return
if len(_resp) == 0:
raise errors.RadioNoResponse()
else:
raise Exception("Unable to identify radio")
def _finish(self):
self._write_record(CMD_END)
def process_mmap(self):
self._memobj = bitwise.parse(_MEM_FORMAT, self._mmap)
def sync_in(self):
try:
self._mmap = self._download()
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):
self._upload()
# TODO: Load all memory.
# It would be smarter to only load the active areas and none of
# the padding/unused areas. Padding still need to be investigated.
def _download(self):
"""Talk to a Wouxun KG-UV8E and do a download"""
try:
self._identify()
return self._do_download(0, 32768, 64)
except errors.RadioError:
raise
except Exception as e:
LOG.exception('Unknown error during download process')
raise errors.RadioError("Failed to communicate with radio: %s" % e)
def _do_download(self, start, end, blocksize):
# allocate & fill memory
image = b""
for i in range(start, end, blocksize):
req = struct.pack('>HB', i, blocksize)
self._write_record(CMD_RD, req)
cs_error, resp = self._read_record()
if cs_error:
LOG.debug(util.hexprint(resp))
raise Exception("Checksum error on read")
LOG.debug("Got:\n%s" % util.hexprint(resp))
image += resp[2:]
if self.status_fn:
status = chirp_common.Status()
status.cur = i
status.max = end
status.msg = "Cloning from radio"
self.status_fn(status)
self._finish()
return memmap.MemoryMapBytes(image)
def _upload(self):
"""Talk to a Wouxun KG-UV8E and do a upload"""
try:
self._identify()
self._do_upload(0, 32768, 64)
except errors.RadioError:
raise
except Exception as e:
raise errors.RadioError("Failed to communicate with radio: %s" % e)
return
def _do_upload(self, start, end, blocksize):
ptr = start
for i in range(start, end, blocksize):
req = struct.pack('>H', i)
chunk = self.get_mmap()[ptr:ptr + blocksize]
self._write_record(CMD_WR, req + chunk)
# ~ LOG.debug(util.hexprint(req + chunk))
cserr, ack = self._read_record()
# ~ LOG.debug(util.hexprint(ack))
j = struct.unpack('>H', ack)[0]
if cserr or j != ptr:
raise Exception("Radio did not ack block %i" % ptr)
ptr += blocksize
if self.status_fn:
status = chirp_common.Status()
status.cur = i
status.max = end
status.msg = "Cloning to radio"
self.status_fn(status)
self._finish()
def get_features(self):
rf = chirp_common.RadioFeatures()
rf.has_settings = True
rf.has_ctone = True
rf.has_rx_dtcs = True
rf.has_cross = True
rf.has_tuning_step = False
rf.has_bank = False
rf.can_odd_split = True
rf.valid_skips = ["", "S"]
rf.valid_tmodes = ["", "Tone", "TSQL", "DTCS", "Cross"]
rf.valid_cross_modes = [
"Tone->Tone",
"Tone->DTCS",
"DTCS->Tone",
"DTCS->",
"->Tone",
"->DTCS",
"DTCS->DTCS",
]
rf.valid_modes = ["FM", "NFM"]
rf.valid_power_levels = self.POWER_LEVELS
rf.valid_name_length = 8
rf.valid_duplexes = ["", "-", "+", "split", "off"]
rf.valid_bands = [(134000000, 175000000), # supports 2m
(220000000, 260000000), # supports 1.25m
(350000000, 520000000)] # supports 70cm
rf.valid_characters = chirp_common.CHARSET_ASCII
rf.memory_bounds = (1, 999) # 999 memories
rf.valid_tuning_steps = STEPS
return rf
@classmethod
def get_prompts(cls):
rp = chirp_common.RadioPrompts()
rp.experimental = \
('This driver is experimental.\n'
'\n'
'Please keep a copy of your memories with the original software '
'if you treasure them, this driver is new and may contain'
' bugs.\n'
'\n'
)
return rp
def get_raw_memory(self, number):
return repr(self._memobj.memory[number])
def get_memory(self, number):
_mem = self._memobj.memory[number]
_nam = self._memobj.names[number]
mem = chirp_common.Memory()
mem.number = number
_valid = self._memobj.valid[mem.number]
LOG.debug("%d %s", number, _valid == MEM_VALID)
if _valid != MEM_VALID:
mem.empty = True
return mem
else:
mem.empty = False
mem.freq = int(_mem.rxfreq) * 10
if _mem.txfreq == 0xFFFFFFFF:
# TX freq not set
mem.duplex = "off"
mem.offset = 0
elif int(_mem.rxfreq) == int(_mem.txfreq):
mem.duplex = ""
mem.offset = 0
elif abs(int(_mem.rxfreq) * 10 - int(_mem.txfreq) * 10) > 70000000:
mem.duplex = "split"
mem.offset = int(_mem.txfreq) * 10
else:
mem.duplex = int(_mem.rxfreq) > int(_mem.txfreq) and "-" or "+"
mem.offset = abs(int(_mem.rxfreq) - int(_mem.txfreq)) * 10
for char in _nam.name:
if char != 0:
mem.name += chr(char)
mem.name = mem.name.rstrip()
self.tone_model.get_tone(_mem, mem)
mem.skip = "" if bool(_mem.scan_add) else "S"
mem.power = self.POWER_LEVELS[_mem.power]
mem.mode = _mem.iswide and "FM" or "NFM"
return mem
def set_memory(self, mem):
number = mem.number
_mem = self._memobj.memory[number]
_nam = self._memobj.names[number]
if mem.empty:
_mem.set_raw("\x00" * (_mem.size() // 8))
self._memobj.valid[number] = 0
self._memobj.names[number].set_raw("\x00" * (_nam.size() // 8))
return
_mem.rxfreq = int(mem.freq / 10)
if mem.duplex == "off":
_mem.txfreq = 0xFFFFFFFF
elif mem.duplex == "split":
_mem.txfreq = int(mem.offset / 10)
elif mem.duplex == "off":
for i in range(0, 4):
_mem.txfreq[i].set_raw("\xFF")
elif mem.duplex == "+":
_mem.txfreq = int(mem.freq / 10) + int(mem.offset / 10)
elif mem.duplex == "-":
_mem.txfreq = int(mem.freq / 10) - int(mem.offset / 10)
else:
_mem.txfreq = int(mem.freq / 10)
_mem.scan_add = int(mem.skip != "S")
_mem.iswide = int(mem.mode == "FM")
# set the tone
self.tone_model.set_tone(mem, _mem)
# set the scrambler and compander to off by default
_mem.scrambler = 0
_mem.compander = 0
# set the power
if mem.power:
_mem.power = self.POWER_LEVELS.index(mem.power)
else:
_mem.power = True
# set to mute mode to QT (not QT+DTMF or QT*DTMF) by default
_mem.mute_mode = 0
for i in range(0, len(_nam.name)):
if i < len(mem.name) and mem.name[i]:
_nam.name[i] = ord(mem.name[i])
else:
_nam.name[i] = 0x0
self._memobj.valid[mem.number] = MEM_VALID
def _get_settings(self):
_settings = self._memobj.settings
_vfoa = self._memobj.vfoa
_vfob = self._memobj.vfob
cfg_grp = RadioSettingGroup("cfg_grp", "Configuration")
vfoa_grp = RadioSettingGroup("vfoa_grp", "VFO A Settings")
vfob_grp = RadioSettingGroup("vfob_grp", "VFO B Settings")
key_grp = RadioSettingGroup("key_grp", "Key Settings")
lmt_grp = RadioSettingGroup("lmt_grp", "Frequency Limits")
uhf_lmt_grp = RadioSettingGroup("uhf_lmt_grp", "UHF")
vhf_lmt_grp = RadioSettingGroup("vhf_lmt_grp", "VHF")
vhf1_lmt_grp = RadioSettingGroup("vhf1_lmt_grp", "VHF1")
oem_grp = RadioSettingGroup("oem_grp", "OEM Info")
lmt_grp.append(vhf_lmt_grp)
lmt_grp.append(vhf1_lmt_grp)
lmt_grp.append(uhf_lmt_grp)
group = RadioSettings(cfg_grp, vfoa_grp, vfob_grp,
key_grp, lmt_grp, oem_grp)
#
# Configuration Settings
#
rs = RadioSetting("channel_menu", "Menu available in channel mode",
RadioSettingValueBoolean(_settings.channel_menu))
cfg_grp.append(rs)
rs = RadioSetting("ponmsg", "Poweron message",
RadioSettingValueList(
PONMSG_LIST, current_index=_settings.ponmsg))
cfg_grp.append(rs)
rs = RadioSetting("voice", "Voice Guide",
RadioSettingValueBoolean(_settings.voice))
cfg_grp.append(rs)
rs = RadioSetting("language", "Language",
RadioSettingValueList(LANGUAGE_LIST,
current_index=_settings.
language))
cfg_grp.append(rs)
rs = RadioSetting("timeout", "Timeout Timer",
RadioSettingValueList(
TIMEOUT_LIST, current_index=_settings.timeout))
cfg_grp.append(rs)
rs = RadioSetting("toalarm", "Timeout Alarm",
RadioSettingValueInteger(0, 10, _settings.toalarm))
cfg_grp.append(rs)
rs = RadioSetting("roger_beep", "Roger Beep",
RadioSettingValueList(ROGER_LIST,
current_index=_settings.roger_beep))
cfg_grp.append(rs)
rs = RadioSetting("power_save", "Power save",
RadioSettingValueBoolean(_settings.power_save))
cfg_grp.append(rs)
rs = RadioSetting("autolock", "Autolock",
RadioSettingValueBoolean(_settings.autolock))
cfg_grp.append(rs)
rs = RadioSetting("keylock", "Keypad Lock",
RadioSettingValueBoolean(_settings.keylock))
cfg_grp.append(rs)
rs = RadioSetting("beep", "Keypad Beep",
RadioSettingValueBoolean(_settings.beep))
cfg_grp.append(rs)
rs = RadioSetting("stopwatch", "Stopwatch",
RadioSettingValueBoolean(_settings.stopwatch))
cfg_grp.append(rs)
rs = RadioSetting("backlight", "Backlight",
RadioSettingValueList(BACKLIGHT_LIST,
current_index=_settings.
backlight))
cfg_grp.append(rs)
rs = RadioSetting("dtmf_st", "DTMF Sidetone",
RadioSettingValueList(DTMFST_LIST,
current_index=_settings.
dtmf_st))
cfg_grp.append(rs)
rs = RadioSetting("ani_sw", "ANI-ID Switch",
RadioSettingValueBoolean(_settings.ani_sw))
cfg_grp.append(rs)
rs = RadioSetting("ptt_id", "PTT-ID Delay",
RadioSettingValueList(PTTID_LIST,
current_index=_settings.ptt_id))
cfg_grp.append(rs)
rs = RadioSetting("ring_time", "Ring Time",
RadioSettingValueList(LIST_10,
current_index=_settings.ring_time))
cfg_grp.append(rs)
rs = RadioSetting("scan_rev", "Scan Mode",
RadioSettingValueList(SCANMODE_LIST,
current_index=_settings.
scan_rev))
cfg_grp.append(rs)
rs = RadioSetting("vox", "VOX",
RadioSettingValueList(LIST_10,
current_index=_settings.vox))
cfg_grp.append(rs)
rs = RadioSetting("prich_sw", "Priority Channel Switch",
RadioSettingValueBoolean(_settings.prich_sw))
cfg_grp.append(rs)
rs = RadioSetting("pri_ch", "Priority Channel",
RadioSettingValueInteger(1, 999, _settings.pri_ch))
cfg_grp.append(rs)
rs = RadioSetting("rpt_mode", "Radio Mode",
RadioSettingValueList(RPTMODE_LIST,
current_index=_settings.
rpt_mode))
cfg_grp.append(rs)
rs = RadioSetting("rpt_set", "Repeater Setting",
RadioSettingValueList(RPTSET_LIST,
current_index=_settings.
rpt_set))
cfg_grp.append(rs)
rs = RadioSetting("rpt_spk", "Repeater Mode Speaker",
RadioSettingValueBoolean(_settings.rpt_spk))
cfg_grp.append(rs)
rs = RadioSetting("rpt_ptt", "Repeater PTT",
RadioSettingValueBoolean(_settings.rpt_ptt))
cfg_grp.append(rs)
rs = RadioSetting("dtmf_tx_time", "DTMF Tx Duration",
RadioSettingValueList(DTMF_TIMES,
current_index=_settings.
dtmf_tx_time))
cfg_grp.append(rs)
rs = RadioSetting("dtmf_interval", "DTMF Interval",
RadioSettingValueList(DTMF_TIMES,
current_index=_settings.
dtmf_interval))
cfg_grp.append(rs)
rs = RadioSetting("alert", "Alert Tone",
RadioSettingValueList(ALERTS_LIST,
current_index=_settings.alert))
cfg_grp.append(rs)
rs = RadioSetting("rpt_tone", "Repeater Tone",
RadioSettingValueBoolean(_settings.rpt_tone))
cfg_grp.append(rs)
rs = RadioSetting("rpt_hold", "Repeater Hold Time",
RadioSettingValueList(HOLD_TIMES,
current_index=_settings.
rpt_hold))
cfg_grp.append(rs)
rs = RadioSetting("scan_det", "Scan DET",
RadioSettingValueBoolean(_settings.scan_det))
cfg_grp.append(rs)
rs = RadioSetting("sc_qt", "SC-QT",
RadioSettingValueList(SCQT_LIST,
current_index=_settings.sc_qt))
cfg_grp.append(rs)
rs = RadioSetting("smuteset", "SubFreq Mute",
RadioSettingValueList(SMUTESET_LIST,
current_index=_settings.
smuteset))
cfg_grp.append(rs)
#
# VFO A Settings
#
rs = RadioSetting("workmode_a", "VFO A Workmode",
RadioSettingValueList(WORKMODE_LIST, current_index=_settings.workmode_a))
vfoa_grp.append(rs)
rs = RadioSetting("work_cha", "VFO A Channel",
RadioSettingValueInteger(1, 999, _settings.work_cha))
vfoa_grp.append(rs)
rs = RadioSetting("vfoa.rxfreq", "VFO A Rx Frequency",
RadioSettingValueInteger(
134000000, 520000000, _vfoa.rxfreq * 10, 5000))
vfoa_grp.append(rs)
rs = RadioSetting("vfoa.txoffset", "VFO A Tx Offset",
RadioSettingValueInteger(
0, 520000000, _vfoa.txoffset * 10, 5000))
vfoa_grp.append(rs)
# u16 rxtone;
# u16 txtone;
rs = RadioSetting("vfoa.power", "VFO A Power",
RadioSettingValueList(
POWER_LIST, current_index=_vfoa.power))
vfoa_grp.append(rs)
# shift_dir:2
rs = RadioSetting("vfoa.iswide", "VFO A NBFM",
RadioSettingValueList(
BANDWIDTH_LIST, current_index=_vfoa.iswide))
vfoa_grp.append(rs)
rs = RadioSetting("vfoa.mute_mode", "VFO A Mute",
RadioSettingValueList(
SPMUTE_LIST, current_index=_vfoa.mute_mode))
vfoa_grp.append(rs)
rs = RadioSetting("vfoa.step", "VFO A Step (kHz)",
RadioSettingValueList(
STEP_LIST, current_index=_vfoa.step))
vfoa_grp.append(rs)
rs = RadioSetting("vfoa.squelch", "VFO A Squelch",
RadioSettingValueList(
LIST_10, current_index=_vfoa.squelch))
vfoa_grp.append(rs)
rs = RadioSetting("bcl_a", "Busy Channel Lock-out A",
RadioSettingValueBoolean(_settings.bcl_a))
vfoa_grp.append(rs)
#
# VFO B Settings
#
rs = RadioSetting("workmode_b", "VFO B Workmode",
RadioSettingValueList(WORKMODE_LIST, current_index=_settings.workmode_b))
vfob_grp.append(rs)
rs = RadioSetting("work_chb", "VFO B Channel",
RadioSettingValueInteger(1, 999, _settings.work_chb))
vfob_grp.append(rs)
rs = RadioSetting("vfob.rxfreq", "VFO B Rx Frequency",
RadioSettingValueInteger(
134000000, 520000000, _vfob.rxfreq * 10, 5000))
vfob_grp.append(rs)
rs = RadioSetting("vfob.txoffset", "VFO B Tx Offset",
RadioSettingValueInteger(
0, 520000000, _vfob.txoffset * 10, 5000))
vfob_grp.append(rs)
# u16 rxtone;
# u16 txtone;
rs = RadioSetting("vfob.power", "VFO B Power",
RadioSettingValueList(
POWER_LIST, current_index=_vfob.power))
vfob_grp.append(rs)
# shift_dir:2
rs = RadioSetting("vfob.iswide", "VFO B NBFM",
RadioSettingValueList(
BANDWIDTH_LIST, current_index=_vfob.iswide))
vfob_grp.append(rs)
rs = RadioSetting("vfob.mute_mode", "VFO B Mute",
RadioSettingValueList(
SPMUTE_LIST, current_index=_vfob.mute_mode))
vfob_grp.append(rs)
rs = RadioSetting("vfob.step", "VFO B Step (kHz)",
RadioSettingValueList(
STEP_LIST, current_index=_vfob.step))
vfob_grp.append(rs)
rs = RadioSetting("vfob.squelch", "VFO B Squelch",
RadioSettingValueList(
LIST_10, current_index=_vfob.squelch))
vfob_grp.append(rs)
rs = RadioSetting("bcl_b", "Busy Channel Lock-out B",
RadioSettingValueBoolean(_settings.bcl_b))
vfob_grp.append(rs)
#
# Key Settings
#
_msg = str(_settings.dispstr).split("\0")[0]
val = RadioSettingValueString(0, 15, _msg)
val.set_mutable(True)
rs = RadioSetting("dispstr", "Display Message", val)
key_grp.append(rs)
dtmfchars = "0123456789"
_codeobj = _settings.ani_code
_code = "".join([dtmfchars[x] for x in _codeobj if int(x) < 0x0A])
val = RadioSettingValueString(3, 6, _code, False)
val.set_charset(dtmfchars)
rs = RadioSetting("ani_code", "ANI Code", val)
def apply_ani_id(setting, obj):
value = []
for j in range(0, 6):
try:
value.append(dtmfchars.index(str(setting.value)[j]))
except IndexError:
value.append(0xFF)
obj.ani_code = value
rs.set_apply_callback(apply_ani_id, _settings)
key_grp.append(rs)
rs = RadioSetting("pf1_func", "PF1 Key function",
RadioSettingValueList(
PF1KEY_LIST,
current_index=_settings.pf1_func))
key_grp.append(rs)
rs = RadioSetting("pf3_func", "PF3 Key function",
RadioSettingValueList(
PF3KEY_LIST,
current_index=_settings.pf3_func))
key_grp.append(rs)
#
# Limits settings
#
rs = RadioSetting("vhf_limits.rx_start", "VHF RX Lower Limit",
RadioSettingValueInteger(
134000000, 174997500,
self._memobj.vhf_limits.rx_start * 10, 5000))
vhf_lmt_grp.append(rs)
rs = RadioSetting("vhf_limits.rx_stop", "VHF RX Upper Limit",
RadioSettingValueInteger(
134000000, 174997500,
self._memobj.vhf_limits.rx_stop * 10, 5000))
vhf_lmt_grp.append(rs)
rs = RadioSetting("vhf_limits.tx_start", "VHF TX Lower Limit",
RadioSettingValueInteger(
134000000, 174997500,
self._memobj.vhf_limits.tx_start * 10, 5000))
vhf_lmt_grp.append(rs)
rs = RadioSetting("vhf_limits.tx_stop", "VHF TX Upper Limit",
RadioSettingValueInteger(
134000000, 174997500,
self._memobj.vhf_limits.tx_stop * 10, 5000))
vhf_lmt_grp.append(rs)
rs = RadioSetting("vhf1_limits.rx_start", "VHF1 RX Lower Limit",
RadioSettingValueInteger(
220000000, 265000000,
self._memobj.vhf1_limits.rx_start * 10, 5000))
vhf1_lmt_grp.append(rs)
rs = RadioSetting("vhf1_limits.rx_stop", "VHF1 RX Upper Limit",
RadioSettingValueInteger(
220000000, 265000000,
self._memobj.vhf1_limits.rx_stop * 10, 5000))
vhf1_lmt_grp.append(rs)
rs = RadioSetting("vhf1_limits.tx_start", "VHF1 TX Lower Limit",
RadioSettingValueInteger(
220000000, 265000000,
self._memobj.vhf1_limits.tx_start * 10, 5000))
vhf1_lmt_grp.append(rs)
rs = RadioSetting("vhf1_limits.tx_stop", "VHF1 TX Upper Limit",
RadioSettingValueInteger(
220000000, 265000000,
self._memobj.vhf1_limits.tx_stop * 10, 5000))
vhf1_lmt_grp.append(rs)
rs = RadioSetting("uhf_limits.rx_start", "UHF RX Lower Limit",
RadioSettingValueInteger(
350000000, 520000000,
self._memobj.uhf_limits.rx_start * 10, 5000))
uhf_lmt_grp.append(rs)
rs = RadioSetting("uhf_limits.rx_stop", "UHF RX Upper Limit",
RadioSettingValueInteger(
350000000, 520000000,
self._memobj.uhf_limits.rx_stop * 10, 5000))
uhf_lmt_grp.append(rs)
rs = RadioSetting("uhf_limits.tx_start", "UHF TX Lower Limit",
RadioSettingValueInteger(
400000000, 520000000,
self._memobj.uhf_limits.tx_start * 10, 5000))
uhf_lmt_grp.append(rs)
rs = RadioSetting("uhf_limits.tx_stop", "UHF TX Upper Limit",
RadioSettingValueInteger(
400000000, 520000000,
self._memobj.uhf_limits.tx_stop * 10, 5000))
uhf_lmt_grp.append(rs)
#
# OEM info
#
def _decode(lst):
_str = ''.join([chr(c) for c in lst
if chr(c) in chirp_common.CHARSET_ASCII])
return _str
def do_nothing(setting, obj):
return
_str = _decode(self._memobj.oem_info.model)
val = RadioSettingValueString(0, 15, _str)
val.set_mutable(False)
rs = RadioSetting("oem_info.model", "Model", val)
rs.set_apply_callback(do_nothing, _settings)
oem_grp.append(rs)
_str = _decode(self._memobj.oem_info.oem1)
val = RadioSettingValueString(0, 15, _str)
val.set_mutable(False)
rs = RadioSetting("oem_info.oem1", "OEM String 1", val)
rs.set_apply_callback(do_nothing, _settings)
oem_grp.append(rs)
_str = _decode(self._memobj.oem_info.oem2)
val = RadioSettingValueString(0, 15, _str)
val.set_mutable(False)
rs = RadioSetting("oem_info.oem2", "OEM String 2", val)
rs.set_apply_callback(do_nothing, _settings)
oem_grp.append(rs)
_str = _decode(self._memobj.oem_info.version)
val = RadioSettingValueString(0, 15, _str)
val.set_mutable(False)
rs = RadioSetting("oem_info.version", "Software Version", val)
rs.set_apply_callback(do_nothing, _settings)
oem_grp.append(rs)
_str = _decode(self._memobj.oem_info.date)
val = RadioSettingValueString(0, 15, _str)
val.set_mutable(False)
rs = RadioSetting("oem_info.date", "OEM Date", val)
rs.set_apply_callback(do_nothing, _settings)
oem_grp.append(rs)
return group
def get_settings(self):
try:
return self._get_settings()
except:
import traceback
LOG.error("Failed to parse settings: %s", traceback.format_exc())
return None
def set_settings(self, settings):
for element in settings:
if not isinstance(element, RadioSetting):
self.set_settings(element)
continue
else:
try:
if "." in element.get_name():
bits = element.get_name().split(".")
obj = self._memobj
for bit in bits[:-1]:
obj = getattr(obj, bit)
setting = bits[-1]
else:
obj = self._memobj.settings
setting = element.get_name()
if element.has_apply_callback():
LOG.debug("Using apply callback")
element.run_apply_callback()
else:
LOG.debug("Setting %s = %s" % (setting, element.value))
if self._is_freq(element):
setattr(obj, setting, int(element.value)/10)
else:
setattr(obj, setting, element.value)
except Exception:
LOG.debug(element.get_name())
raise
def _is_freq(self, element):
return "rxfreq" in element.get_name() or "txoffset" in element.get_name() or "rx_start" in element.get_name() or "rx_stop" in element.get_name() or "tx_start" in element.get_name() or "tx_stop" in element.get_name()
chirp-20261030/chirp/drivers/kguv920pa.py 0000644 0001750 0001750 00000140507 15257672455 016750 0 ustar hibby hibby # Copyright 2022 Matthew Handley
#
# 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 .
"""Wouxun KG-UV920P-A radio management module based"""
import time
import logging
from chirp import util, chirp_common, bitwise, memmap, errors, directory
from chirp import checksum
from chirp.settings import RadioSetting, RadioSettingGroup, \
RadioSettingValueBoolean, RadioSettingValueList, \
RadioSettingValueInteger, RadioSettingValueString, \
RadioSettingValueMap, RadioSettingValueFloat, RadioSettings
import struct
from chirp.kenwood_tone import KenwoodToneModel
LOG = logging.getLogger(__name__)
CMD_ID = 0x80
CMD_END = 0x81
CMD_RD = 0x82
CMD_WR = 0x83
CHARSET_NUMERIC = "0123456789"
CHARSET = "0123456789" + \
":;<=>?@" + \
"ABCDEFGHIJKLMNOPQRSTUVWXYZ" + \
"[\\]^_`" + \
"abcdefghijklmnopqrstuvwxyz" + \
"{|}~\x4E" + \
" !\"#$%&'()*+,-./"
MUTE_MODE_MAP = [('QT', 0b01),
('QT*DTMF', 0b10),
('QT+DTMF', 0b11)]
STEPS = [2.5, 5.0, 6.25, 10.0, 12.5, 20.0, 25.0, 30.0, 50.0, 100.0]
STEP_LIST = [str(x) for x in STEPS]
M_POWER_MAP = [('10', 1),
('20', 2)]
ROGER_LIST = ["Off", "BOT", "EOT", "Both"]
VOICE_LIST = ["Off", "Chinese", "English"]
SC_REV_MAP = [('Timeout (TO)', 1),
('Carrier (CO)', 2),
('Stop (SE)', 3)]
TOT_MAP = [('%d' % i, int('%02d' % i, 16)) for i in range(1, 61)]
TOA_MAP = [('Off', 0)] + \
[('%d' % i, int('%02d' % i, 16)) for i in range(1, 11)]
RING_MAP = [('Off', 0)] + \
[('%d' % i, int('%02d' % i, 16)) for i in range(1, 11)]
DTMF_ST_LIST = ["Off", "DT-ST", "ANI-ST", "DT+ANI"]
PTT_ID_LIST = ["BOT", "EOT", "Both"]
PTT_ID_MAP = [('BOT', 1),
('EOT', 2),
('Both', 3)]
BACKLIGHT_LIST = ["Off", "White", "Blue", "Green"]
SPEAKER_MAP = [('1', 1),
('2', 2),
('1+2', 3)]
RPT_MODE_LIST = ["Off", "X-DIRPT", "X-TWRPT", "CRPT-RX", "CRPT-TX"]
APO_TIME_LIST = ["Off", "30", "60", "90", "120", "150"]
ALERT_MAP = [('1750', 1),
('2100', 2),
('1000', 3),
('1450', 4)]
FAN_MODE_LIST = ["TX", "Hi-Temp/TX", "Always"]
SCAN_GROUP_LIST = ["All", "1", "2", "3", "4"]
WORKMODE_MAP = [('VFO', 1),
('Ch. No.', 2),
('Ch. No.+Freq.', 3),
('Ch. No.+Name', 4)]
AB_LIST = ["A", "B"]
POWER_MAP = [('L', 0),
('M', 1),
('H', 3)]
BANDWIDTH_MAP = [('NFM', 0b11),
('FM', 0b00)]
SCRAMBLER_LIST = ["Off", "1", "2", "3", "4", "5", "6", "7", "8"]
ANS_LIST = ["Off", "Normal", "Strong"]
DTMF_TIMES = [str(x) for x in range(80, 501, 20)]
DTMF_INTERVALS = [str(x) for x in range(60, 501, 20)]
ROGER_TIMES = [str(x) for x in range(20, 1001, 20)]
PTT_ID_DELAY_MAP = [(str(x), x/100) for x in range(100, 1001, 100)]
ROGER_INTERVALS = ROGER_TIMES
TONE_MAP = [('Off', 0x0000)] + \
[('%.1f' % tone, int(tone * 10)) for tone in chirp_common.TONES] + \
[('DN%d' % tone, int(0x8000 + tone * 10))
for tone in chirp_common.DTCS_CODES] + \
[('DR%d' % tone, int(0xC000 + tone * 10))
for tone in chirp_common.DTCS_CODES]
DUPLEX_LIST = ["Off", "+", "-"]
# structure elements whose name starts with x are currently unidentified
_MEM_FORMAT = """
#seekto 0x0040;
struct {
u8 left[8];
u8 right[8];
} ponmsg;
// Limit changes here don't persist, so may be incorrect. Currently unused
// in driver.
#seekto 0x0070;
struct {
u16 uhf_rx_start; // 0x0070
u16 uhf_rx_stop; // 0x0072
u16 vhf_rx_start; // 0x0074
u16 vhf_rx_stop; // 0x0076
u16 uhf_tx_start; // 0x0078
u16 uhf_tx_stop; // 0x007A
u16 vhf_tx_start; // 0x007C
u16 vhf_tx_stop; // 0x007E
} limits;
#seekto 0x0068;
struct {
u8 unknown1[8]; // 0x0068
u8 x0070[368];
u8 x01E0[8];
u8 x01E8[8];
u8 model[8]; // 0x01F0
u8 date[8];
u8 oem1[8];
u8 oem2[8];
u8 unknown2[8];
u8 unknown3[8];
} oem_info;
#seekto 0x0250;
struct {
u32 rxfreq;
u32 txoffset;
u16 txtone;
u16 rxtone;
u8 unknown1_7downto5:3,
mute_mode:2,
unknown1_3downto0:3;
u8 named:1,
scan_add:1,
power:2,
unknown2_32:2,
isnarrow:2;
u8 unknown3_7downto2:6,
duplex:2;
u8 unknown4:3,
compander:1,
scrambler:4;
} vfoa;
#seekto 0x0260;
struct {
u32 rxfreq;
u32 txoffset;
u16 txtone;
u16 rxtone;
u8 unknown1_7downto5:3,
mute_mode:2,
unknown1_3downto0:3;
u8 named:1,
scan_add:1,
power:2,
unknown2_3downto2:2,
isnarrow:2;
u8 unknown3_7downto2:6,
duplex:2;
u8 unknown4:3,
compander:1,
scrambler:4;
} vfob;
#seekto 0x0270;
struct {
u8 rc_power; // 0x0270
u8 voice; // 0x0271
u8 tot; // 0x0272
u8 toa; // 0x0273
u8 roger; // 0x0274
u8 sc_rev; // 0x0275
u8 dtmf_st; // 0x0276
u8 ptt_id; // 0x0277
u8 ring; // 0x0278
u8 ani_sw; // 0x0279
u8 x027A;
u8 alert; // 0x027B
u8 bcl_a; // 0x027C
u8 pri_ch_sw; // 0x027D
u8 scan_group; // 0x027E
u8 ptt_id_delay; // 0x027F
u8 x0280;
u8 x0281;
u8 beep; // 0x0282
u8 x0283;
u8 x0284;
u8 tx_led; // 0x0285
u8 wt_led; // 0x0286
u8 rx_led; // 0x0287
u8 active_display; // 0x0288
u8 workmode_a; // 0x0289
u8 workmode_b; // 0x028A
u8 squelch_a; // 0x028B
u8 squelch_b; // 0x028C
u8 step_a; // 0x028D
u8 step_b; // 0x028E
u8 single_display; // 0x028F
u8 x0290;
u16 work_cha; // 0x0291
u8 x0293;
u16 work_chb; // 0x0294
u8 dtmf_time; // 0x0296
u8 x0297;
u8 dtmf_interval; // 0x0298
u8 x0299;
u16 pri_ch; // 0x029A
u8 x029C;
u8 x029D;
u8 speaker; // 0x029E
u8 x029F;
u8 rpt_spk; // 0x02A0
u8 rpt_ptt; // 0x02A1
u8 autolock; // 0x02A2
u8 apo_time; // 0x02A3
u8 low_v; // 0x02A4
u8 fan_mode; // 0x02A5
u8 x02A6;
u8 rc_sw; // 0x02A7
u8 x02A8;
u8 roger_time; // 0x02A9
u8 roger_int; // 0x02AA
u8 bcl_b; // 0x02AB
u8 m_power_set; // 0x02AC
u8 x02AD;
u8 ans; // 0x02AE
u8 x02AF;
u8 ani[3]; // 0x02B0
u8 x02B3;
u8 ani_mcc[3]; // 0x02B4
u8 x02B7;
u8 ani_scc[3]; // 0x02B8
u8 x02BB;
u8 ani_ctrl[3]; // 0x02BC
u8 x02BF;
u8 roger_begin[4]; // 0x02C0
u8 roger_end[4]; // 0x02C4
u8 x02C8[8];
u16 grp1_lower; // 0x02D0
u16 grp1_upper;
u16 grp2_lower; // 0x02D4
u16 grp2_upper;
u16 grp3_lower; // 0x02D8
u16 grp3_upper;
u16 grp4_lower; // 0x02DC
u16 grp4_upper;
u8 x02E0[16];
u8 x02F0[16];
u8 x0300[16];
u8 x0310[7];
u8 rpt_tone; // 0x0317
u8 x0318[8];
u8 x0320[16];
u8 x0330[16];
u8 x0340[16];
u8 x0350[16];
u8 x0360[16];
u8 x0370[16];
u8 x0380[16];
u8 x0390[16];
u8 x03A0[16];
u8 x03B0[16];
u8 rpt_mode; // 0x03C0
u8 x03C1;
u8 x03C2;
u8 x03C3;
u8 x03C4;
u8 x03C5;
u8 scan_det; // 0x0x3C6
} settings;
#seekto 0x0400;
struct {
u16 freq;
} fm_preset[20];
#seekto 0x0800;
struct {
u32 rxfreq;
u32 txfreq;
u16 txtone;
u16 rxtone;
u8 unknown1_7downto5:3,
mute_mode:2,
unknown1_3downto0:3;
u8 named:1,
scan_add:1,
power:2,
unknown2_32:2,
isnarrow:2;
u8 unknown3_7downto2:6,
shift_dir:2;
u8 unknown4:3,
compander:1,
scrambler:4;
} memory[999];
#seekto 0x4700;
struct {
u8 name[8];
} names[999];
"""
def _str_decode(in_str):
out_str = ''
for c in in_str:
if c < len(CHARSET):
out_str += CHARSET[c]
return out_str.rstrip()
def _str_encode(in_str):
out_str = []
for c in in_str:
try:
out_str.append(CHARSET.index(c))
except ValueError:
pass
while len(out_str) < 8:
out_str.append(CHARSET.index(' '))
return bytes(out_str)
def _freq_decode(in_freq, bytes=4):
out_freq = 0
for i in range(bytes*2):
out_freq += (in_freq & 0xF) * (10 ** i)
in_freq = in_freq >> 4
if bytes == 4:
return out_freq * 10
elif bytes == 2:
return out_freq * 100000
def _freq_encode(in_freq, bytes=4):
if bytes == 4:
return int('%08d' % (in_freq / 10), 16)
elif bytes == 2:
return int('%04d' % (in_freq / 100000), 16)
def _ani_decode(in_ani):
return ''.join(['%02x' % b for b in in_ani]).strip('c')
def _ani_encode(in_ani):
ani_str = ''
while len(in_ani) < 6:
in_ani += 'c'
for i in range(3):
ani_str += chr(int(in_ani[i*2:i*2+2], 16))
return ani_str
def _chnum_decode(in_ch):
return int(('%04x' % in_ch)[0:3])
def _chnum_encode(in_ch):
return int('%03d0' % in_ch, 16)
def _roger_decode(in_roger):
return ''.join(['%02x' % b for b in in_roger]).strip('ef')
def _roger_encode(in_roger):
roger_str = ''
in_roger = 'e' + in_roger
while len(in_roger) < 8:
in_roger += 'f'
for i in range(4):
roger_str += chr(int(in_roger[i*2:i*2+2], 16))
return roger_str
# Support for the Wouxun KG-UV920P-A radio
# Serial coms are at 19200 baud
# The data is passed in variable length records
# Record structure:
# Offset Usage
# 0 start of record (\x7e)
# 1 Command (\x80 Identify \x81 End/Reboot \x82 Read \x83 Write)
# 2 direction (\xff PC-> Radio, \x00 Radio -> PC)
# 3 length of payload (excluding header/checksum) (n)
# 4 payload (n bytes)
# 4+n+1 checksum - byte sum (% 16) of bytes 1 -> 4+n
#
# Memory Read Records:
# the payload is 3 bytes, first 2 are offset (big endian),
# 3rd is number of bytes to read
# Memory Write Records:
# 2 bytes location + data.
@directory.register
class KGUV920PARadio(chirp_common.CloneModeRadio,
chirp_common.ExperimentalRadio):
"""Wouxun KG-UV920P-A"""
VENDOR = "Wouxun"
MODEL = "KG-UV920P-A"
_model = "KG-UV920Rr" # what the radio responds to CMD_ID with
_file_ident = b"KGUV920PA"
BAUD_RATE = 19200
@classmethod
def match_model(cls, filedata, filename):
return cls._file_ident in filedata[0x400:0x408]
POWER_LEVELS = [chirp_common.PowerLevel("L", watts=5),
chirp_common.PowerLevel("M", watts=20),
chirp_common.PowerLevel("H", watts=50)]
def __init__(self, pipe):
super().__init__(pipe)
self.tone_model = KenwoodToneModel(
dcs_base=0x8000,
pol_mask=0x4000,
tone_init=0x0000,
tone_flag=0x0000,
dcs_enc_base=10)
def _write_record(self, cmd, payload=None):
_length = 0
if payload:
_length = len(payload)
_packet = struct.pack('BBBB', 0x7E, cmd, 0xFF, _length)
if payload:
# add the chars to the packet
_packet += payload
# calculate and add the checksum to the packet
_packet += bytes([checksum.checksum_8bit(_packet[1:], 16)])
LOG.debug("Sent:\n%s" % util.hexprint(_packet))
self.pipe.write(_packet)
def _read_record(self):
# read 4 chars for the header
_header = self.pipe.read(4)
if not _header:
raise errors.RadioNoResponse()
if len(_header) != 4:
raise errors.RadioError('Radio sent short header')
_length = _header[3]
_packet = self.pipe.read(_length)
_cs = checksum.checksum_8bit(_header[1:], 16)
_cs += checksum.checksum_8bit(_packet, 16)
_cs %= 16
try:
_rcs = self.pipe.read(1)[0]
except (TypeError, IndexError):
raise errors.RadioNoResponse()
if _rcs != _cs:
LOG.error("_cs =%x", _cs)
LOG.error("_rcs=%x", _rcs)
return (_rcs != _cs, _packet)
def _identify(self):
"""
Offset
0:10 Model 'KG-UV920Rr'
11: Not used (limits)
"""
self._write_record(CMD_ID)
time.sleep(0.6)
_chksum_err, _resp = self._read_record()
LOG.debug("Got:\n%s" % util.hexprint(_resp))
if _chksum_err:
raise Exception("Checksum error")
if len(_resp) == 0:
raise errors.RadioNoResponse()
reported_model = _str_decode(_resp[0:10])
if reported_model != self._model:
raise Exception("Unable to identify radio (Got %s, Expected)" %
reported_model, self._model)
def _finish(self):
self._write_record(CMD_END)
def process_mmap(self):
self._memobj = bitwise.parse(_MEM_FORMAT, self._mmap)
def sync_in(self):
try:
self._mmap = self._download()
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):
self._upload()
# TODO: This is a dumb, brute force method of downloading the memory.
# it would be smarter to only load the active areas and none of
# the padding/unused areas.
def _download(self):
"""Talk to a Wouxun KG-UV920P-A and do a download"""
try:
self._identify()
return self._do_download(0, 0x6640, 0x40)
except errors.RadioError:
raise
except Exception as e:
LOG.exception('Unknown error during download process')
raise errors.RadioError("Failed to communicate with radio: %s" % e)
def _do_download(self, start, end, blocksize):
# allocate & fill memory
image = b""
for i in range(start, end, blocksize):
req = struct.pack('>HB', i, blocksize)
self._write_record(CMD_RD, req)
cs_error, resp = self._read_record()
if cs_error:
LOG.debug(util.hexprint(resp))
raise Exception("Checksum error on read")
LOG.debug("Got:\n%s" % util.hexprint(resp))
image += resp[2:]
if self.status_fn:
status = chirp_common.Status()
status.cur = i
status.max = end
status.msg = "Cloning from radio"
self.status_fn(status)
self._finish()
return memmap.MemoryMapBytes(image)
def _upload(self):
"""Talk to a Wouxun KG-UV920P-A and do a upload"""
try:
self._identify()
self._do_upload(0x0000, 0x6640, 0x40)
except errors.RadioError:
raise
except Exception as e:
raise errors.RadioError("Failed to communicate with radio: %s" % e)
return
def _do_upload(self, start, end, blocksize):
ptr = start
for i in range(start, end, blocksize):
req = struct.pack('>H', i)
chunk = self.get_mmap()[ptr:ptr + blocksize]
self._write_record(CMD_WR, req + chunk)
LOG.debug(util.hexprint(req + chunk))
cserr, ack = self._read_record()
LOG.debug(util.hexprint(ack))
j = struct.unpack('>H', ack[0:2])[0]
if cserr or j != ptr:
print(cserr)
print(j)
print(ptr)
raise Exception("Radio did not ack block %i" % ptr)
ptr += blocksize
if self.status_fn:
status = chirp_common.Status()
status.cur = i
status.max = end
status.msg = "Cloning to radio"
self.status_fn(status)
self._finish()
def get_features(self):
rf = chirp_common.RadioFeatures()
rf.has_settings = True
rf.has_ctone = True
rf.has_rx_dtcs = True
rf.has_cross = True
rf.has_tuning_step = False
rf.has_bank = False
rf.can_odd_split = True
rf.valid_skips = ["", "S"]
rf.valid_tmodes = ["", "Tone", "TSQL", "DTCS", "Cross"]
rf.valid_tuning_steps = STEPS
rf.valid_cross_modes = [
"Tone->Tone",
"Tone->DTCS",
"DTCS->Tone",
"DTCS->",
"->Tone",
"->DTCS",
"DTCS->DTCS",
]
rf.valid_modes = ["FM", "NFM"]
rf.valid_power_levels = self.POWER_LEVELS
rf.valid_name_length = 8
rf.valid_duplexes = ["", "-", "+", "split", "off"]
rf.valid_bands = [(136000000, 175000000), # supports 2m
(420000000, 520000000)] # supports 70cm
rf.valid_characters = CHARSET
rf.memory_bounds = (1, 999) # 999 memories
return rf
@classmethod
def get_prompts(cls):
rp = chirp_common.RadioPrompts()
rp.experimental = ("This radio driver is a beta version. "
"There are no known issues with it, but you should "
"proceed with caution.")
return rp
def get_raw_memory(self, number):
return repr(self._memobj.memory[number-1])
def get_memory(self, number):
mem = chirp_common.Memory()
mem.number = number
_mem = self._memobj.memory[mem.number-1]
if _mem.rxfreq & 0xFF000000 == 0xFF000000:
mem.empty = True
return mem
else:
mem.empty = False
_rxfreq = _freq_decode(_mem.rxfreq)
_txfreq = _freq_decode(_mem.txfreq)
mem.freq = _rxfreq
if _mem.txfreq == 0xFFFFFFFF:
# TX freq not set
mem.duplex = "off"
mem.offset = 0
elif int(_rxfreq) == int(_txfreq):
mem.duplex = ""
mem.offset = 0
elif abs(_rxfreq - _txfreq) > 70000000:
mem.duplex = "split"
mem.offset = _txfreq
else:
mem.duplex = _rxfreq > _txfreq and "-" or "+"
mem.offset = abs(_rxfreq - _txfreq)
if _mem.named:
mem.name = _str_decode(self._memobj.names[number-1].name)
else:
mem.name = ''
self.tone_model.get_tone(_mem, mem)
mem.skip = "" if bool(_mem.scan_add) else "S"
if _mem.power == 3:
mem.power = self.POWER_LEVELS[2]
else:
mem.power = self.POWER_LEVELS[_mem.power]
mem.mode = _mem.isnarrow and "NFM" or "FM"
mem.extra = RadioSettingGroup("Extra", "extra")
_scram = _mem.scrambler
if _mem.scrambler > 8:
_scram = 0
rs = RadioSetting("scrambler", "Scrambler",
RadioSettingValueList(SCRAMBLER_LIST,
current_index=_scram))
mem.extra.append(rs)
rs = RadioSetting("compander", "Compander",
RadioSettingValueBoolean(_mem.compander))
mem.extra.append(rs)
rs = RadioSetting("mute_mode", "Mute",
RadioSettingValueMap(MUTE_MODE_MAP, _mem.mute_mode))
mem.extra.append(rs)
return mem
def set_memory(self, mem):
number = mem.number
_mem = self._memobj.memory[number-1]
_nam = self._memobj.names[number-1]
if mem.empty:
_mem.set_raw("\xFF" * (_mem.size() // 8))
_nam.set_raw(_str_encode(""))
_mem.rxfreq = 0xFFFFFFFF
return
# Zero the entire structure and set these fields to default values
# instead of leaving unused fields as FF
_mem.fill_raw(b'\x00')
_mem.unknown2_32 = 0x02
_mem.mute_mode = 0x01
_mem.rxfreq = _freq_encode(mem.freq)
if mem.duplex == "off":
_mem.txfreq = 0xFFFFFFFF
elif mem.duplex == "split":
_mem.txfreq = _freq_encode(mem.offset)
elif mem.duplex == "+":
_mem.txfreq = _freq_encode(mem.freq + mem.offset)
elif mem.duplex == "-":
_mem.txfreq = _freq_encode(mem.freq - mem.offset)
else:
_mem.txfreq = _freq_encode(mem.freq)
_mem.scan_add = int(mem.skip != "S")
_mem.isnarrow = 0b11 * int(mem.mode == "NFM")
# set the tone
self.tone_model.set_tone(mem, _mem)
# set the power
if mem.power:
idx = self.POWER_LEVELS.index(mem.power)
if idx == 2:
_mem.power = 3
else:
_mem.power = idx
else:
_mem.power = True
if len(mem.name) > 0:
_mem.named = True
name_encoded = _str_encode(mem.name)
for i in range(0, 8):
_nam.name[i] = name_encoded[i]
else:
_mem.named = False
for setting in mem.extra:
setattr(_mem, setting.get_name(), setting.value)
def _get_settings(self):
_settings = self._memobj.settings
_vfoa = self._memobj.vfoa
_vfob = self._memobj.vfob
_fm_preset = self._memobj.fm_preset
cfg_grp = RadioSettingGroup("cfg_grp", "Configuration")
ui_grp = RadioSettingGroup("ui_grp", "User Interface")
vfoa_grp = RadioSettingGroup("vfoa_grp", "VFO A")
vfob_grp = RadioSettingGroup("vfob_grp", "VFO B")
scn_grp = RadioSettingGroup("scn_grp", "Scan")
rpt_grp = RadioSettingGroup("rmt_grp", "Repeater")
rmt_grp = RadioSettingGroup("rmt_grp", "Remote Control")
fmp_grp = RadioSettingGroup("fmp_grp", "FM Radio Presets")
oem_grp = RadioSettingGroup("oem_grp", "OEM Info")
group = RadioSettings(cfg_grp, ui_grp, vfoa_grp, vfob_grp, scn_grp,
rpt_grp, rmt_grp, fmp_grp, oem_grp)
#
# Configuration Settings
#
rs = RadioSetting("m_power_set", "Medium Power Level (W)",
RadioSettingValueMap(M_POWER_MAP,
_settings.m_power_set))
cfg_grp.append(rs)
rs = RadioSetting("roger", "Roger Beep",
RadioSettingValueList(ROGER_LIST,
current_index=_settings.roger))
cfg_grp.append(rs)
rs = RadioSetting("roger_time", "Roger Tx Duration (ms)",
RadioSettingValueList(
ROGER_TIMES,
current_index=_settings.roger_time))
cfg_grp.append(rs)
rs = RadioSetting("roger_int", "Roger Interval (ms)",
RadioSettingValueList(
ROGER_INTERVALS,
current_index=_settings.roger_int))
cfg_grp.append(rs)
val = RadioSettingValueString(1, 6,
_roger_decode(_settings.roger_begin),
False, CHARSET_NUMERIC)
rs = RadioSetting("roger_begin", "Roger Begin", val)
cfg_grp.append(rs)
val = RadioSettingValueString(1, 6,
_roger_decode(_settings.roger_end),
False, CHARSET_NUMERIC)
rs = RadioSetting("roger_end", "Roger End", val)
cfg_grp.append(rs)
rs = RadioSetting("tot", "Time-Out Timer (TOT) (Min)",
RadioSettingValueMap(TOT_MAP, _settings.tot))
cfg_grp.append(rs)
rs = RadioSetting("toa", "Time-Out Alarm (TOA) (Sec)",
RadioSettingValueMap(TOA_MAP, _settings.toa))
cfg_grp.append(rs)
rs = RadioSetting("ani_sw", "Caller ID Tx",
RadioSettingValueBoolean(_settings.ani_sw))
cfg_grp.append(rs)
rs = RadioSetting("ptt_id", "Caller ID Tx Mode",
RadioSettingValueMap(PTT_ID_MAP, _settings.ptt_id))
cfg_grp.append(rs)
rs = RadioSetting("ptt_id_delay", "Caller ID Tx Delay (ms)",
RadioSettingValueMap(PTT_ID_DELAY_MAP,
_settings.ptt_id_delay))
cfg_grp.append(rs)
rs = RadioSetting("ring", "Ring Time (Sec)",
RadioSettingValueMap(RING_MAP, _settings.ring))
cfg_grp.append(rs)
rs = RadioSetting("dtmf_st", "DTMF Sidetone",
RadioSettingValueList(
DTMF_ST_LIST,
current_index=_settings.dtmf_st))
cfg_grp.append(rs)
rs = RadioSetting("pri_ch_sw", "Priority Channel Switch",
RadioSettingValueBoolean(_settings.pri_ch_sw))
cfg_grp.append(rs)
rs = RadioSetting("pri_ch", "Priority Channel",
RadioSettingValueInteger(
1, 999, _chnum_decode(_settings.pri_ch)))
cfg_grp.append(rs)
rs = RadioSetting("apo_time", "Auto Power-Off (Min)",
RadioSettingValueList(
APO_TIME_LIST,
current_index=_settings.apo_time))
cfg_grp.append(rs)
rs = RadioSetting("alert", "Alert Pulse (Hz)",
RadioSettingValueMap(ALERT_MAP, _settings.alert))
cfg_grp.append(rs)
rs = RadioSetting("fan_mode", "Fan Mode",
RadioSettingValueList(
FAN_MODE_LIST,
current_index=_settings.fan_mode))
cfg_grp.append(rs)
rs = RadioSetting("low_v", "Low Voltage Shutoff",
RadioSettingValueBoolean(_settings.low_v))
cfg_grp.append(rs)
rs = RadioSetting("ans", "Noise Reduction",
RadioSettingValueList(ANS_LIST,
current_index=_settings.ans))
cfg_grp.append(rs)
rs = RadioSetting("dtmf_time", "DTMF Tx Duration (ms)",
RadioSettingValueList(
DTMF_TIMES,
current_index=_settings.dtmf_time))
cfg_grp.append(rs)
rs = RadioSetting("dtmf_interval", "DTMF Interval (ms)",
RadioSettingValueList(
DTMF_INTERVALS,
current_index=_settings.dtmf_interval))
cfg_grp.append(rs)
#
# UI Settings
#
rs = RadioSetting("ponmsg.left",
"Power On Message Left & \n Single Band Message",
RadioSettingValueString(
0, 8, _str_decode(self._memobj.ponmsg.left)))
ui_grp.append(rs)
rs = RadioSetting("ponmsg.right", "Power On Message Right",
RadioSettingValueString(
0, 8, _str_decode(self._memobj.ponmsg.right)))
ui_grp.append(rs)
rs = RadioSetting("autolock", "Autolock",
RadioSettingValueBoolean(_settings.autolock))
ui_grp.append(rs)
rs = RadioSetting("speaker", "Speaker Select",
RadioSettingValueMap(SPEAKER_MAP, _settings.speaker))
ui_grp.append(rs)
rs = RadioSetting("voice", "Voice Guide",
RadioSettingValueList(VOICE_LIST,
current_index=_settings.voice))
ui_grp.append(rs)
rs = RadioSetting("beep", "Beep",
RadioSettingValueBoolean(_settings.beep))
ui_grp.append(rs)
rs = RadioSetting("active_display", "Active Display",
RadioSettingValueList(
AB_LIST,
current_index=_settings.active_display))
ui_grp.append(rs)
rs = RadioSetting("single_display", "Single Display",
RadioSettingValueBoolean(_settings.single_display))
ui_grp.append(rs)
rs = RadioSetting("tx_led", "TX Backlight",
RadioSettingValueList(
BACKLIGHT_LIST,
current_index=_settings.tx_led))
ui_grp.append(rs)
rs = RadioSetting("wt_led", "Standby Backlight",
RadioSettingValueList(
BACKLIGHT_LIST,
current_index=_settings.wt_led))
ui_grp.append(rs)
rs = RadioSetting("rx_led", "Rx Backlight",
RadioSettingValueList(
BACKLIGHT_LIST,
current_index=_settings.rx_led))
ui_grp.append(rs)
#
# VFO A Settings
#
rs = RadioSetting("workmode_a", "Workmode",
RadioSettingValueMap(
WORKMODE_MAP, _settings.workmode_a))
vfoa_grp.append(rs)
rs = RadioSetting("work_cha", "Channel",
RadioSettingValueInteger(
1, 999, _chnum_decode(_settings.work_cha)))
vfoa_grp.append(rs)
rs = RadioSetting("vfoa.rxfreq", "Rx Frequency",
RadioSettingValueInteger(
137000000, 470000000,
_freq_decode(_vfoa.rxfreq), 2500))
vfoa_grp.append(rs)
rs = RadioSetting("vfoa.txoffset", "Tx Offset",
RadioSettingValueInteger(
0, 470000000,
_freq_decode(_vfoa.txoffset), 2500))
vfoa_grp.append(rs)
rs = RadioSetting("vfoa.rxtone", "Rx Tone",
RadioSettingValueMap(TONE_MAP, _vfoa.rxtone))
vfoa_grp.append(rs)
rs = RadioSetting("vfoa.txtone", "Tx Tone",
RadioSettingValueMap(TONE_MAP, _vfoa.txtone))
vfoa_grp.append(rs)
rs = RadioSetting("vfoa.power", "Power",
RadioSettingValueMap(POWER_MAP, _vfoa.power))
vfoa_grp.append(rs)
rs = RadioSetting("vfoa.duplex", "Duplex",
RadioSettingValueList(DUPLEX_LIST,
current_index=_vfoa.duplex))
vfoa_grp.append(rs)
rs = RadioSetting("vfoa.isnarrow", "Mode",
RadioSettingValueMap(BANDWIDTH_MAP, _vfoa.isnarrow))
vfoa_grp.append(rs)
_vfoa_scram = _vfoa.scrambler
if _vfoa_scram > 8:
_vfoa_scram = 0
rs = RadioSetting("vfoa.scrambler", "Scrambler",
RadioSettingValueList(SCRAMBLER_LIST,
current_index=_vfoa_scram))
vfoa_grp.append(rs)
rs = RadioSetting("vfoa.compander", "Compander",
RadioSettingValueBoolean(_vfoa.compander))
vfoa_grp.append(rs)
rs = RadioSetting("vfoa.mute_mode", "Mute",
RadioSettingValueMap(MUTE_MODE_MAP,
_vfoa.mute_mode))
vfoa_grp.append(rs)
rs = RadioSetting("step_a", "Step (kHz)",
RadioSettingValueList(
STEP_LIST, current_index=_settings.step_a))
vfoa_grp.append(rs)
rs = RadioSetting("squelch_a", "Squelch",
RadioSettingValueInteger(
0, 9, _settings.squelch_a))
vfoa_grp.append(rs)
rs = RadioSetting("bcl_a", "Busy Channel Lock-out",
RadioSettingValueBoolean(_settings.bcl_a))
vfoa_grp.append(rs)
#
# VFO B Settings
#
rs = RadioSetting("workmode_b", "Workmode",
RadioSettingValueMap(
WORKMODE_MAP, _settings.workmode_b))
vfob_grp.append(rs)
rs = RadioSetting("work_chb", "Channel",
RadioSettingValueInteger(
1, 999, _chnum_decode(_settings.work_chb)))
vfob_grp.append(rs)
rs = RadioSetting("vfob.rxfreq", "Rx Frequency",
RadioSettingValueInteger(
137000000, 470000000,
_freq_decode(_vfob.rxfreq), 2500))
vfob_grp.append(rs)
rs = RadioSetting("vfob.txoffset", "Tx Offset",
RadioSettingValueInteger(
0, 470000000,
_freq_decode(_vfob.txoffset), 2500))
vfob_grp.append(rs)
rs = RadioSetting("vfob.rxtone", "Rx Tone",
RadioSettingValueMap(TONE_MAP, _vfob.rxtone))
vfob_grp.append(rs)
rs = RadioSetting("vfob.txtone", "Tx Tone",
RadioSettingValueMap(TONE_MAP, _vfob.txtone))
vfob_grp.append(rs)
rs = RadioSetting("vfob.power", "Power",
RadioSettingValueMap(POWER_MAP, _vfob.power))
vfob_grp.append(rs)
rs = RadioSetting("vfob.duplex", "Duplex",
RadioSettingValueList(DUPLEX_LIST,
current_index=_vfob.duplex))
vfob_grp.append(rs)
rs = RadioSetting("vfob.isnarrow", "Mode",
RadioSettingValueMap(BANDWIDTH_MAP, _vfob.isnarrow))
vfob_grp.append(rs)
_vfob_scram = _vfob.scrambler
if _vfob_scram > 8:
_vfob_scram = 0
rs = RadioSetting("vfob.scrambler", "Scrambler",
RadioSettingValueList(SCRAMBLER_LIST,
current_index=_vfob_scram))
vfob_grp.append(rs)
rs = RadioSetting("vfob.compander", "Compander",
RadioSettingValueBoolean(_vfob.compander))
vfob_grp.append(rs)
rs = RadioSetting("vfob.mute_mode", "Mute",
RadioSettingValueMap(MUTE_MODE_MAP, _vfob.mute_mode))
vfob_grp.append(rs)
rs = RadioSetting("step_b", "Step (kHz)",
RadioSettingValueList(
STEP_LIST, current_index=_settings.step_b))
vfob_grp.append(rs)
rs = RadioSetting("squelch_b", "Squelch",
RadioSettingValueInteger(
0, 9, _settings.squelch_b))
vfob_grp.append(rs)
rs = RadioSetting("bcl_b", "Busy Channel Lock-out",
RadioSettingValueBoolean(_settings.bcl_b))
vfob_grp.append(rs)
#
# Scan Settings
#
rs = RadioSetting("sc_rev", "Scan Resume Mode",
RadioSettingValueMap(SC_REV_MAP, _settings.sc_rev))
scn_grp.append(rs)
rs = RadioSetting("scan_group", "Scan Group", RadioSettingValueList(
SCAN_GROUP_LIST, current_index=_settings.scan_group))
scn_grp.append(rs)
rs = RadioSetting("grp1_lower", "Group 1 Lower",
RadioSettingValueInteger(1, 999,
_settings.grp1_lower))
scn_grp.append(rs)
rs = RadioSetting("grp1_upper", "Group 1 Upper",
RadioSettingValueInteger(1, 999,
_settings.grp1_upper))
scn_grp.append(rs)
rs = RadioSetting("grp2_lower", "Group 2 Lower",
RadioSettingValueInteger(1, 999,
_settings.grp2_lower))
scn_grp.append(rs)
rs = RadioSetting("grp2_upper", "Group 2 Upper",
RadioSettingValueInteger(1, 999,
_settings.grp2_upper))
scn_grp.append(rs)
rs = RadioSetting("grp3_lower", "Group 3 Lower",
RadioSettingValueInteger(1, 999,
_settings.grp3_lower))
scn_grp.append(rs)
rs = RadioSetting("grp3_upper", "Group 3 Upper",
RadioSettingValueInteger(1, 999,
_settings.grp3_upper))
scn_grp.append(rs)
rs = RadioSetting("grp4_lower", "Group 4 Lower",
RadioSettingValueInteger(1, 999,
_settings.grp4_lower))
scn_grp.append(rs)
rs = RadioSetting("grp4_upper", "Group 4 Upper",
RadioSettingValueInteger(1, 999,
_settings.grp4_upper))
scn_grp.append(rs)
rs = RadioSetting("scan_det", "Scan DET",
RadioSettingValueBoolean(_settings.scan_det))
scn_grp.append(rs)
#
# Repeater Settings
#
rs = RadioSetting("rpt_spk", "Speaker",
RadioSettingValueBoolean(_settings.rpt_spk))
rpt_grp.append(rs)
rs = RadioSetting("rpt_ptt", "PTT",
RadioSettingValueBoolean(_settings.rpt_ptt))
rpt_grp.append(rs)
rs = RadioSetting("rpt_mode", "Mode", RadioSettingValueList(
RPT_MODE_LIST, current_index=_settings.rpt_mode))
rpt_grp.append(rs)
rs = RadioSetting("rpt_tone", "Tone",
RadioSettingValueBoolean(_settings.rpt_tone))
rpt_grp.append(rs)
#
# Remote Settings
#
rs = RadioSetting("rc_sw", "Remote Control",
RadioSettingValueBoolean(_settings.rc_sw))
rmt_grp.append(rs)
rs = RadioSetting("rc_power", "Remote Control Power",
RadioSettingValueBoolean(_settings.rc_power))
rmt_grp.append(rs)
val = RadioSettingValueString(3, 6, _ani_decode(_settings.ani),
False, CHARSET_NUMERIC)
rs = RadioSetting("ani", "Caller ID (ANI)", val)
rmt_grp.append(rs)
val = RadioSettingValueString(3, 6, _ani_decode(_settings.ani_mcc),
False, CHARSET_NUMERIC)
rs = RadioSetting("ani_mcc", "MCC-Edit", val)
rmt_grp.append(rs)
val = RadioSettingValueString(3, 6, _ani_decode(_settings.ani_scc),
False, CHARSET_NUMERIC)
rs = RadioSetting("ani_scc", "SCC-Edit", val)
rmt_grp.append(rs)
val = RadioSettingValueString(3, 6, _ani_decode(_settings.ani_ctrl),
False, CHARSET_NUMERIC)
rs = RadioSetting("ani_ctrl", "Control", val)
rmt_grp.append(rs)
#
# FM Radio Presets Settings
#
for i in range(1, 21):
val = _fm_preset[i-1].freq / 100.0
rs = RadioSetting(str(i), str(i),
RadioSettingValueFloat(65.00, 108.00,
val, precision=2))
fmp_grp.append(rs)
#
# OEM info
#
_str = _str_decode(self._memobj.oem_info.model)
val = RadioSettingValueString(0, 15, _str)
val.set_mutable(False)
rs = RadioSetting("model", "Model", val)
oem_grp.append(rs)
_str = _str_decode(self._memobj.oem_info.date)
val = RadioSettingValueString(0, 15, _str)
val.set_mutable(False)
rs = RadioSetting("date", "OEM Date", val)
oem_grp.append(rs)
_str = _str_decode(self._memobj.oem_info.oem1)
val = RadioSettingValueString(0, 15, _str)
val.set_mutable(False)
rs = RadioSetting("oem1", "OEM String 1", val)
oem_grp.append(rs)
_str = _str_decode(self._memobj.oem_info.oem2)
val = RadioSettingValueString(0, 15, _str)
val.set_mutable(False)
rs = RadioSetting("oem2", "OEM String 2", val)
oem_grp.append(rs)
_str = _str_decode(self._memobj.oem_info.unknown1)
val = RadioSettingValueString(0, 15, _str)
val.set_mutable(False)
rs = RadioSetting("unknown1", "Unknown String 1", val)
oem_grp.append(rs)
_str = _str_decode(self._memobj.oem_info.unknown2)
val = RadioSettingValueString(0, 15, _str)
val.set_mutable(False)
rs = RadioSetting("unknown2", "Unknown String 2", val)
oem_grp.append(rs)
_str = _str_decode(self._memobj.oem_info.unknown3)
val = RadioSettingValueString(0, 15, _str)
val.set_mutable(False)
rs = RadioSetting("unknown3", "Unknown String 3", val)
oem_grp.append(rs)
return group
def get_settings(self):
try:
return self._get_settings()
except:
import traceback
LOG.error("Failed to parse settings: %s", traceback.format_exc())
return None
def set_settings(self, settings):
for group in settings:
for element in group:
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 = self._memobj.settings
setting = element.get_name()
#
# Special Configuration Settings
#
if group.get_name() == 'cfg_grp':
if name == 'roger_begin':
value = _roger_encode(element[0].get_value())
for i in range(0, 4):
obj.roger_begin[i] = ord(value[i])
continue
elif name == 'roger_end':
value = _roger_encode(element[0].get_value())
for i in range(0, 4):
obj.roger_end[i] = ord(value[i])
continue
elif name == 'pri_ch':
value = _chnum_encode(element[0].get_value())
setattr(obj, setting, value)
continue
#
# Special UI Settings
#
if group.get_name() == 'ui_grp':
if name == 'ponmsg.left':
value = _str_encode(element[0].get_value())
for i in range(0, 8):
self._memobj.ponmsg.left[i] = value[i]
continue
elif name == 'ponmsg.right':
value = _str_encode(element[0].get_value())
for i in range(0, 8):
self._memobj.ponmsg.right[i] = value[i]
continue
#
# Special VFO A Settings
#
if (group.get_name() == 'vfoa_grp') or \
(group.get_name() == 'vfob_grp'):
if (setting == 'rxfreq') or \
(setting == 'txoffset'):
value = _freq_encode(element[0].get_value())
setattr(obj, setting, value)
continue
elif (setting == 'work_cha') or \
(setting == 'work_chb'):
value = _chnum_encode(element[0].get_value())
setattr(obj, setting, value)
continue
#
# Special VFO B Settings
#
if group.get_name() == 'vfob_grp':
pass
#
# Special Scan settings
#
if group.get_name() == 'scn_grp':
pass
#
# Special Repeater settings
#
if group.get_name() == 'rpt_grp':
pass
#
# Special Remote settings
#
if group.get_name() == 'rmt_grp':
if name == 'ani':
value = _ani_encode(element[0].get_value())
for i in range(0, 3):
obj.ani[i] = ord(value[i])
continue
elif name == 'ani_mcc':
value = _ani_encode(element[0].get_value())
for i in range(0, 3):
obj.ani_mcc[i] = ord(value[i])
continue
elif name == 'ani_scc':
value = _ani_encode(element[0].get_value())
for i in range(0, 3):
obj.ani_scc[i] = ord(value[i])
continue
elif name == 'ani_ctrl':
value = _ani_encode(element[0].get_value())
for i in range(0, 3):
obj.ani_ctrl[i] = ord(value[i])
continue
#
# FM Radio Presets Settings
#
if group.get_name() == 'fmp_grp':
value = int(element[0].get_value() * 100)
self._memobj.fm_preset[int(name)-1].freq = value
continue
#
# Generic Settings
#
if element[0].__class__ is RadioSettingValueList:
value = element[0].get_options().index(
element[0].get_value())
setattr(obj, setting, value)
elif element[0].__class__ is RadioSettingValueMap:
value = element[0].get_mem_val()
setattr(obj, setting, value)
elif element[0].__class__ is RadioSettingValueBoolean:
value = element[0].get_value()
setattr(obj, setting, value)
elif element[0].__class__ is RadioSettingValueInteger:
value = element[0].get_value()
setattr(obj, setting, value)
else:
LOG.debug("Unable to set_setting %s" % name)
chirp-20261030/chirp/drivers/kguv980p.py 0000644 0001750 0001750 00000266624 15172631605 016612 0 ustar hibby hibby # Copyright 2022 Mel Terechenok
# KG-UV980P , KG-1000G, KG-1000G Plus unified driver using
# KG-935G, KG-UV8H, KG-UV920P, and KG-UV9D Plus drivers as resources
#
# Based on the previous work of Pavel Milanes CO7WT
# and Krystian Struzik
# who figured out the crypt used and made possible the
# Wuoxun KG-UV8D Plus driver.
#
# 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 .
"""Wouxun KG-UV980P radio management module"""
import time
import logging
import struct
from chirp import util, chirp_common, bitwise, memmap, errors, directory
from chirp.kenwood_tone import KenwoodToneModel
from chirp.settings import RadioSetting, RadioSettingGroup, \
RadioSettingValueBoolean, RadioSettingValueList, \
RadioSettingValueInteger, RadioSettingValueString, \
RadioSettingValueFloat, RadioSettingValueMap, RadioSettings
LOG = logging.getLogger(__name__)
CMD_ID = 0x80
CMD_END = 0x81
CMD_RD = 0x82
CMD_WR = 0x83
DIR_TO = 0xFF
DIR_FROM = 0x00
# This is used to write the configuration of the radio base on info
# gleaned from the downloaded app. There are empty spaces and we honor
# them because we don't know what they are (yet) although we read the
# whole of memory.
# Max Write Size = 64 for comm protocol
config_map = ( # map address, write size, write count
# (0x0, 64, 512), # 0 to 8000 - full write use only
(0x4c, 12, 1), # Mode PSW -- Display name
(0x60, 16, 3), # Freq Limits
(0x740, 40, 1), # FM chan 1-20
(0x830, 16, 13), # General settings
(0x900, 8, 1), # General settings
(0x940, 64, 2), # Scan groups and names
(0xa00, 64, 249), # Memory Channels 1-996
(0x4840, 48, 1), # Memory Channels 997-999
(0x4900, 64, 124), # Memory Names 1-992
(0x6800, 8, 7), # Memory Names 997-999
(0x7000, 64, 15), # mem valid 1 -960
(0x73c0, 39, 1), # mem valid 961 - 999
)
MEM_VALID = 0x00
MEM_INVALID = [0x80]
TX_BLANK = 0x40
RX_BLANK = 0x80
CHARSET_NUMERIC = "0123456789"
CHARSET = "0123456789" + \
":;<=>?@" + \
"ABCDEFGHIJKLMNOPQRSTUVWXYZ" + \
"[\\]^_`" + \
"abcdefghijklmnopqrstuvwxyz" + \
"{|}~\x4E" + \
" !\"#$%&'()*+,-./"
SCANNAME_CHARSET = "0123456789" + \
":;<=>?@" + \
"ABCDEFGHIJKLMNOPQRSTUVWXYZ" + \
"[\\]^_`" + \
"abcdefghijklmnopqrstuvwxyz" + \
"{|}~\x4E" + \
" !\"#$%&'()*+,-./"
MUTE_MODE_MAP = [('QT', 0b01),
('QT*DTMF', 0b10),
('QT+DTMF', 0b11)]
STEPS = [2.5, 5.0, 6.25, 10.0, 12.5, 20.0, 25.0, 30.0, 50.0, 100.0]
STEP_LIST = [str(x) for x in STEPS]
SQL_LIST = [int(i) for i in range(0, 10)]
M_POWER_MAP = [('1 = 20W', 1),
('2 = 10W', 2)]
ROGER_LIST = ["Off", "BOT", "EOT", "Both"]
VOICE_MAP = [('Off', 0),
('Chinese', 1),
('On / English', 2)]
VOICE_MAP_1000GPLUS = [('Off', 0),
('On', 2)]
SC_REV_MAP = [('Timeout (TO)', 1),
('Carrier (CO)', 2),
('Stop (SE)', 3)]
TOT_MAP = [('%d min' % i, int('%02d' % i, 10)) for i in range(1, 61)]
TOT_MAP_1000GPLUS = [('%d min' % i, int('%02d' % i, 16)) for i in range(1, 61)]
TOA_LIST = ["Off", "1s", "2s", "3s", "4s", "5s", "6s", "7s", "8s", "9s", "10s"]
TOA_MAP = [('Off', 0)] + \
[('%ds' % i, int('%02d' % i, 16)) for i in range(1, 11)]
RING_LIST = ["Off", "1s", "2s", "3s", "4s", "5s", "6s", "7s",
"8s", "9s", "10s"]
RING_MAP = [('Off', 0)] + \
[('%ds' % i, int('%02d' % i, 10)) for i in range(1, 11)]
DTMF_ST_LIST = ["Off", "DT-ST", "ANI-ST", "DT+ANI"]
DTMF_ST_LIST_1000GPLUS = ["Off", "DTMF", "ID", "DTMF+ID"]
PTT_ID_MAP = [('BOT', 1),
('EOT', 2),
('Both', 3)]
PTT_ID_MAP_1000GPLUS = [('Off', 0),
('BOT', 1),
('EOT', 2),
('Both', 3)]
BACKLIGHT_LIST = ["Off", "Red", "Orange", "Green"]
SPEAKER_MAP = [('SPK_1', 1),
('SPK_2', 2),
('SPK_1+2', 3)]
SPEAKER_MAP_1000GPLUS = [('RADIO', 1),
('MIC', 2),
('BOTH', 3)]
RPT_MODE_LIST = ["Radio", "X-DIRPT", "X-TWRPT", "RPT-RX", "T-W RPT"]
RPT_MODE_MAP = [("OFF", 0),
("RPT-RX", 3),
("RPT-TX", 4)]
APO_TIME_LIST = ["Off", "30", "60", "90", "120", "150"]
ALERT_MAP = [('1750', 1),
('2100', 2),
('1000', 3),
('1450', 4)]
FAN_MODE_LIST = ["TX", "Hi-Temp/TX", "Always"]
SCAN_GROUP_LIST = ["All"] + ["%s" % x for x in range(1, 11)]
WORKMODE_MAP = [('VFO', 1),
('Ch. No.', 2),
('Ch. No.+Freq.', 3),
('Ch. No.+Name', 4)]
WORKMODE_MAP_1000GPLUS = [('Freq', 1),
('Ch-Num', 2),
('Ch-Freq', 3),
('Ch-Name', 4)]
VFOBAND_MAP = [("150M", 0),
("450M", 1),
("20M", 2),
("50M", 3),
("350M", 4),
("850M", 5)]
AB_LIST = ["A", "B"]
POWER_MAP = [('Low', 0),
('Med', 1),
('Med2', 2),
('High', 3)]
BANDWIDTH_MAP = [('Narrow', 1),
('Wide', 0)]
SCRAMBLER_LIST = ["Off", "1", "2", "3", "4", "5", "6", "7", "8"]
ANS_LIST = ["Off", "Normal", "Strong"]
DTMF_TIMES = [str(x) for x in range(80, 501, 20)]
DTMF_INTERVALS = [str(x) for x in range(60, 501, 20)]
ROGER_TIMES = [str(x) for x in range(20, 1001, 20)]
# For py3 compliance x/100 must be changed to x//100
PTT_ID_DELAY_MAP = [(str(x), x//100) for x in range(100, 1001, 100)]
ROGER_INTERVALS = ROGER_TIMES
TONE_MAP = [('Off', 0x0000)] + \
[('%.1f' % tone, int(tone * 10)) for tone in chirp_common.TONES] + \
[('DN%d' % tone, int(0x8000 + tone))
for tone in chirp_common.DTCS_CODES] + \
[('DI%d' % tone, int(0xC000 + tone))
for tone in chirp_common.DTCS_CODES]
DUPLEX_LIST = ["Off", "Plus", "Minus"]
SC_QT_MAP = [("Decoder - Rx QT/DT MEM", 1), ("Encoder- Tx QT/DT MEM", 2),
("All- RxTx QT/DT MEM", 3)]
SC_QT_MAP_1000GPLUS = [("Rx", 1), ("Tx", 2),
("Tx/Rx", 3)]
HOLD_TIMES = ["Off"] + ["%s" % x for x in range(100, 5001, 100)]
PF1_SETTINGS = ["Off", "Stun", "Kill", "Monitor", "Inspection"]
PF1_SETTINGS_1000GPLUS = ["OFF", "Reverse", "Pri-Sel", "Pri-Scan", "Squelch",
"TX PWR", "Scan", "Scan CTCSS",
"Scan DCS", "FM Radio", "Weather", "Ch-Add", "W-N",
"TDR", "WORKMODE", "Band", "Repeater", "Lock",
"Monitor"]
ABR_LIST = ["Always", "1s", "2s", "3s", "4s", "5s", "6s", "7s", "8s",
"9s", "10s", "11s", "12s", "13s", "14s", "15s", "16s", "17s",
"18s", "19s", "20s", "Off"]
KEY_LIST = ["Off", "B/SW", "MENCH", "H-M-L", "VFO/MR", "SET-D", "TDR",
"SQL", "SCAN", "FM-Radio", "Scan CTCSS", "Scan DCS"]
KEY_LIST_1000GPLUS = ["OFF", "Reverse", "Pri-Sel", "Pri-Scan", "Squelch",
"TX PWR", "Scan", "Scan CTCSS", "Scan DCS",
"FM Radio", "Weather", "Ch-Add", "W-N", "TDR",
"WORKMODE", "Band", "Repeater", "Lock", "Monitor"]
RC_POWER_LIST = ["RC Stop", "RC Open"]
ACTIVE_AREA_LIST = ["Area A - Left", "Area B - Right"]
TDR_LIST = ["TDR ON", "TDR OFF"]
PRI_CH_SCAN_LIST = ["Off", "ON-Stby", "On-Always"]
# memory slot 0 is not used, start at 1 (so need 1000 slots, not 999)
# structure elements whose name starts with x are currently unidentified
_MEM_FORMAT = """
#seekto 0x004c;
struct {
u24 mode_psw;
u8 xunk04F;
u8 display_name[8];
} oem;
#seekto 0x0060;
struct {
bbcd limit_144M_ChA_rx_start[2];
bbcd limit_144M_ChA_rx_stop[2];
bbcd limit_70cm_rx_start[2];
bbcd limit_70cm_rx_stop[2];
bbcd limit_10m_rx_start[2];
bbcd limit_10m_rx_stop[2];
bbcd limit_6m_rx_start[2];
bbcd limit_6m_rx_stop[2];
bbcd limit_350M_rx_start[2];
bbcd limit_350M_rx_stop[2];
bbcd limit_850M_rx_start[2];
bbcd limit_850M_rx_stop[2];
bbcd limit_144M_tx_start[2];
bbcd limit_144M_tx_stop[2];
bbcd limit_70cm_tx_start[2];
bbcd limit_70cm_tx_stop[2];
bbcd limit_10m_tx_start[2];
bbcd limit_10m_tx_stop[2];
bbcd limit_6m_tx_start[2];
bbcd limit_6m_tx_stop[2];
bbcd limit_144M_ChB_rx_start[2];
bbcd limit_144M_ChB_rx_stop[2];
} bandlimits;
#seekto 0x0740;
struct {
u16 FM_radio1;
u16 FM_radio2;
u16 FM_radio3;
u16 FM_radio4;
u16 FM_radio5;
u16 FM_radio6;
u16 FM_radio7;
u16 FM_radio8;
u16 FM_radio9;
u16 FM_radio10;
u16 FM_radio11;
u16 FM_radio12;
u16 FM_radio13;
u16 FM_radio14;
u16 FM_radio15;
u16 FM_radio16;
u16 FM_radio17;
u16 FM_radio18;
u16 FM_radio19;
u16 FM_radio20;
u16 FM_radio21;
u16 FM_radio22;
u8 x76c_pad[196];
u32 vfofreq1; // 0x0830
u32 vfoofst1;
u16 txtone1;
u16 rxtone1;
u8 xunk83C_1:3,
mute1:2,
xunk83C_2:3;
u8 xunk83d_1:1,
xunk83d_2:1,
xunk83d_3:1,
power1:2,
am_mode1:1,
xunk83d_7:1,
narrow1:1;
u8 xunk83e:6,
shft_dir1:2;
u8 xunk83F:3,
compander1:1,
scrambler1:4;
u32 vfofreq2;
u32 vfoofst2;
u16 txtone2;
u16 rxtone2;
u8 xunk84C_1:3,
mute2:2,
xunk84C_2:3;
u8 xunk84d_1:1,
xunk84d_2:1,
xunk84d_3:1,
power2:2,
am_mode2:1,
xunk84d_7:1,
narrow2:1;
u8 xunk84e:6,
shft_dir2:2;
u8 xunk84F:3,
compander2:1,
scrambler2:4;
u32 vfofreq3;
u32 vfoofst3;
u16 txtone3;
u16 rxtone3;
u8 xunk85C_1:3,
mute3:2,
xunk85C_2:3;
u8 xunk85d_1:1,
xunk85d_2:1,
xunk85d_3:1,
power3:2,
am_mode3:1,
xunk85d_7:1,
narrow3:1;
u8 xunk85e:6,
shft_dir3:2;
u8 xunk85F:3,
compander3:1,
scrambler3:4;
u32 vfofreq4;
u32 vfoofst4;
u16 txtone4;
u16 rxtone4;
u8 xunk86C_1:3,
mute4:2,
xunk86C_2:3;
u8 xunk86d_1:1,
xunk86d_2:1,
xunk86d_3:1,
power4:2,
am_mode4:1,
xunk86d_7:1,
narrow4:1;
u8 xunk86e:6,
shft_dir4:2;
u8 xunk86F:3,
compander4:1,
scrambler4:4;
u32 vfofreq5;
u32 vfoofst5;
u16 txtone5;
u16 rxtone5;
u8 xunk87C_1:3,
mute5:2,
xunk87C_2:3;
u8 xunk87d_1:1,
xunk87d_2:1,
xunk87d_3:1,
power5:2,
am_mode5:1,
xunk87d_7:1,
narrow5:1;
u8 xunk87e:6,
shft_dir5:2;
u8 xunk87F:3,
compander5:1,
scrambler5:4;
u32 vfofreq6;
u32 vfoofst6;
u16 txtone6;
u16 rxtone6;
u8 xunk88C_1:3,
mute6:2,
xunk88C_2:3;
u8 xunk88d_1:1,
xunk88d_2:1,
xunk88d_3:1,
power6:2,
am_mode6:1,
xunk88d_7:1,
narrow6:1;
u8 xunk88e:6,
shft_dir6:2;
u8 xunk8F:3,
compander6:1,
scrambler6:4;
u32 vfofreq7;
u32 vfoofst7;
u16 txtone7;
u16 rxtone7;
u8 xunk89C_1:3,
mute7:2,
xunk89C_2:3;
u8 xunk89d_1:1,
xunk89d_2:1,
xunk89d_3:1,
power7:2,
am_mode7:1,
xunk89d_7:1,
narrow7:1;
u8 xunk89e:6,
shft_dir7:2;
u8 xunk89F:3,
compander7:1,
scrambler7:4;
u8 x8a0;
u16 vfochan_a;
u8 x8a3;
u16 vfochan_b;
u16 pri_ch;
u8 x8a8;
u8 x8a9;
u8 scan_a_act;
u8 scan_b_act;
u8 m_pwr;
u8 hold_time_rpt;
u8 spk_cont;
u8 x8af;
u8 rc_power;
u8 voice;
u8 tot;
u8 toa;
u8 roger;
u8 sc_rev;
u8 dtmfsf;
u8 ptt_id;
u8 ring;
u8 ani_sw;
u8 rc_sw;
u8 alert;
u8 bcl_a;
u8 prich_sw;
u8 x8bE;
u8 ptt_id_dly;
u8 menu;
u8 x8c1;
u8 beep;
u8 key_lock;
u8 x8c4;
u8 tx_led;
u8 wt_led;
u8 rx_led;
u8 act_area;
u8 vfomode_a;
u8 vfomode_b;
u8 vfosquelch_a;
u8 vfosquelch_b;
u8 vfostep_a;
u8 vfostep_b;
u8 tdr_off;
u8 rpt_spk; //x8d0
u8 rpt_ptt;
u8 autolock;
u8 apo_time;
u8 low_v;
u8 fan;
u8 rpt_set_model;
u8 pf1_set;
u8 auto_am;
u8 dtmf_time;
u8 dtmf_int;
u8 bcl_b;
u8 rpt_tone;
u8 sc_qt;
u8 vfoband_a;
u8 x8dF;
u24 ani_edit;
u8 x8e3;
u24 mcc_edit;
u8 x8e7;
u24 scc_edit;
u8 x8eB;
u24 ctrl_edit;
u8 x8eF;
u8 KeyA;
u8 KeyB;
u8 KeyC;
u8 ABR;
u8 x8f4;
u8 x8f5;
u8 KeyD; // KG-1000G Plus ONLY
u8 x8f7;
u8 x8f8;
u8 x8f9;
u8 x8fA;
u8 x8fB;
u8 x8fC;
u8 x8fD;
u8 x8fE;
u8 x8fF;
u16 FM_radio_cur_freq;
u8 x902;
u8 scan_det;
u8 x904;
u8 thr_vol_tx;
u16 thr_vol_lvl;
u8 x908;
u8 x909;
u8 x90A;
u8 x90B;
u8 x90C;
u8 x90D;
u8 x90E;
u8 x90F;
u8 x910pad[48];
u16 scanlower1; // x940
u16 scanupper1;
u16 scanlower2;
u16 scanupper2;
u16 scanlower3;
u16 scanupper3;
u16 scanlower4;
u16 scanupper4;
u16 scanlower5;
u16 scanupper5;
u16 scanlower6;
u16 scanupper6;
u16 scanlower7;
u16 scanupper7;
u16 scanlower8;
u16 scanupper8;
u16 scanlower9;
u16 scanupper9;
u16 scanlower10;
u16 scanupper10;
u8 scanname0[8]; // x968
u8 scanname1[8];
u8 scanname2[8];
u8 scanname3[8];
u8 scanname4[8];
u8 scanname5[8];
u8 scanname6[8];
u8 scanname7[8];
u8 scanname8[8];
u8 scanname9[8];
u8 scanname10[8];
} settings;
#seekto 0x09f0;
struct {
u32 rxfreq;
u32 txfreq;
u16 txtone;
u16 rxtone;
u8 unknown1:3,
mute_mode:2,
unknown2:3;
u8 named:1,
scan_add:1,
extra_power_bit:1,
power:2,
am_mode:1,
unknownbit2:1,
isnarrow:1;
u8 unknown3:6,
Unknown4_shft_dir:2;
u8 unknown5:3,
compander:1,
scrambler:4;
} memory[1000];
#seekto 0x48f8;
struct {
u8 name[8];
} names[1000];
#seekto 0x6fff;
u8 valid[1000];
"""
def _freq_decode(in_freq, bytes=4):
out_freq = 0
for i in range(bytes*2):
out_freq += (in_freq & 0xF) * (10 ** i)
in_freq = in_freq >> 4
if bytes == 4:
return out_freq * 10
elif bytes == 2:
return out_freq * 100000
def _freq_encode(in_freq, bytes=4):
if bytes == 4:
return int('%08d' % (in_freq / 10), 16)
elif bytes == 2:
return int('%04d' % (in_freq / 100000), 16)
def _oem_str_decode(in_str):
out_str = ''
stopchar = False
for c in in_str:
if c != 0x50 and stopchar is False:
if chr(c+48) in chirp_common.CHARSET_ASCII:
out_str += chr(c+48)
else:
out_str += ''
stopchar = True
return out_str
def _oem_str_encode(in_str):
out_str = ''
LOG.debug("OEM Input String = %s", in_str)
for c in in_str:
try:
out_str += chr(int(ord(c))-48)
except ValueError:
pass
while len(out_str) < 8:
out_str += chr(0x50)
LOG.debug("OEM Output String = %s", out_str)
return out_str
# OEM String Encode for KG-1000G Plus
def _oem_str_decode_1000GPLUS(in_str):
LOG.debug("decode OEM Input String = %s", in_str)
out_str = ''
for c in in_str:
# 1000G+ character mapping starts with P = 32 and O = 127
if 127 >= c >= 80:
out_str += chr(c - 48)
elif 32 <= c < 80:
out_str += chr(c+48)
else:
out_str += ''
LOG.debug("decode OEM Output String = %s", out_str)
return out_str
# OEM String Encode for KG-1000G Plus
def _oem_str_encode_1000GPLUS(in_str):
out_str = ''
LOG.debug("encode OEM Input String = %s", in_str)
for c in in_str:
if 32 <= ord(c) < 80:
out_str += chr(int(ord(c)) + 48)
elif 127 >= ord(c) >= 80:
out_str += chr(int(ord(c)) - 48)
while len(out_str) < 8:
out_str += chr(0x50)
LOG.debug("encode OEM Output String = %s", out_str)
return out_str
def _str_decode(in_str):
out_str = ''
stopchar = False
for c in in_str:
if c != 0x00 and stopchar is False:
if chr(c) in chirp_common.CHARSET_ASCII:
out_str += chr(c)
else:
out_str += ''
stopchar = True
return out_str
def _str_encode(in_str):
out_str = ''
for c in in_str:
try:
out_str += chr(ord(c))
except ValueError:
pass
while len(out_str) < 8:
out_str += chr(0x00)
if out_str == " " or out_str == "":
out_str = "\x00\x00\x00\x00\x00\x00\x00\x00"
return out_str
def _chnum_decode(in_ch):
return int(('%04x' % in_ch)[0:3])
def _chnum_encode(in_ch):
return int('%03d0' % in_ch, 16)
# Support for the Wouxun KG-UV980P radio
# Serial coms are at 19200 baud
# The data is passed in variable length records
# Record structure:
# Offset Usage
# 0 start of record (\x7c)
# 1 Command (\x80 Identify \x81 End/Reboot \x82 Read \x83 Write)
# 2 direction (\xff PC-> Radio, \x00 Radio -> PC)
# 3 length of payload (excluding header/checksum) (n)
# 4 payload (n bytes)
# 4+n+1 checksum - only lower 4 bits of byte sum (% 256) of bytes 1 -> 4+n
#
# Memory Read Records:
# the payload is 3 bytes, first 2 are offset (big endian),
# 3rd is number of bytes to read
# Memory Write Records:
# the maximum payload size (from the Wouxun software) seems to be 66 bytes
# (2 bytes location + 64 bytes data).
@directory.register
class KG980PRadio(chirp_common.CloneModeRadio,
chirp_common.ExperimentalRadio):
"""Wouxun KG-UV980P"""
VENDOR = "Wouxun"
MODEL = "KG-UV980P"
_model = b"KG-UV950R2"
_file_ident = b"980P"
BAUD_RATE = 19200
# Start Byte for Communication messages
_record_start = 0xDA
# _cs_size = 0x0F for 4-bit checksum, 0xFF for 8-Bit checksum
_cs_size = 0x0F
# _valxor = value needed to encrypt/decrypt the bytes to/from the radio
_valxor = 0x57
POWER_LEVELS = [chirp_common.PowerLevel("L", watts=1.0),
chirp_common.PowerLevel("M", watts=20.0),
chirp_common.PowerLevel("H", watts=50.0)]
def __init__(self, pipe):
super().__init__(pipe)
self.tone_model = KenwoodToneModel(
dcs_base=0x8000,
pol_mask=0x4000,
tone_init=0x0000,
tone_flag=0x0000,
dcs_enc_base=10)
def _checksum(self, data):
cs = 0
for byte in data:
cs += byte
return ((cs % 256) & self._cs_size)
def _write_record(self, cmd, payload=b''):
_packet = struct.pack('BBBB', self._record_start, cmd, 0xFF,
len(payload))
checksum = bytes([self._checksum(_packet[1:] + payload)])
_packet += self.encrypt(payload + checksum)
LOG.debug("Sent:\n%s" % util.hexprint(_packet))
self.pipe.write(_packet)
def _read_record(self):
# read 4 chars for the header
_header = self.pipe.read(4)
if not _header:
raise errors.RadioNoResponse()
if len(_header) != 4:
raise errors.RadioError('Radio sent short header')
_length = struct.unpack('xxxB', _header)[0]
_packet = self.pipe.read(_length)
_rcs_xor = _packet[-1]
_packet = self.decrypt(_packet)
_cs = self._checksum(_header[1:])
_cs += self._checksum(_packet)
_cs %= 256
_cs = _cs & self._cs_size
_rcs = self.strxor(self.pipe.read(1)[0], _rcs_xor)[0]
return (_rcs != _cs, _packet)
def decrypt(self, data):
result = b''
for i in range(len(data)-1, 0, -1):
result += self.strxor(data[i], data[i - 1])
result += self.strxor(data[0], self._valxor)
return result[::-1]
def encrypt(self, data):
result = self.strxor(self._valxor, data[0])
for i in range(1, len(data), 1):
result += self.strxor(result[i - 1], data[i])
return result
def strxor(self, xora, xorb):
return bytes([xora ^ xorb])
# Identify the radio
#
# A Gotcha: the first identify packet returns a bad checksum, subsequent
# attempts return the correct checksum... (well it does on my radio!)
#
# The ID record returned by the radio also includes the
# current frequency range
# as 4 bytes big-endian in 10 Hz increments
#
# Offset
# 0:10 Model, zero padded
def _identify(self):
"""Do the identification dance"""
for _i in range(0, 3):
LOG.debug("ID try #"+str(_i))
self._write_record(CMD_ID)
_chksum_err, _resp = self._read_record()
if len(_resp) == 0:
raise errors.RadioNoResponse()
else:
LOG.debug("Got:\n%s" % util.hexprint(_resp))
LOG.debug("Model received is %s" % _resp[0:10])
LOG.debug("Model expected is %s" % self._model)
if _chksum_err:
LOG.error("Checksum error: retrying ident...")
time.sleep(0.100)
continue
else:
LOG.debug("checksum passed")
if _resp[0:8] == self._model[0:8]:
LOG.debug("Passed identify")
break
else:
LOG.debug("FAILED to identify")
raise errors.RadioError("Failed Identification")
def _finish(self):
self._write_record(CMD_END)
def process_mmap(self):
self._memobj = bitwise.parse(_MEM_FORMAT, self._mmap)
def sync_in(self):
try:
self._mmap = self._download()
except errors.RadioError:
raise
except Exception:
raise errors.RadioError("Failed to communicate with radio: %s")
self.process_mmap()
def sync_out(self):
self._upload()
# TODO: Load all memory.
# It would be smarter to only load the active areas and none of
# the padding/unused areas. Padding still need to be investigated.
def _download(self):
"""Talk to a Wouxun KG-UV980P and do a download"""
try:
self._identify()
return self._do_download(0, 32768, 64)
except errors.RadioError:
raise
except Exception:
LOG.exception('Unknown error during download process')
raise errors.RadioError("Failed to communicate with radio: %s")
def _do_download(self, start, end, size):
# allocate & fill memory
LOG.debug("Start Download")
image = b""
for i in range(start, end, size):
req = struct.pack("BBB", int(i / 256), int(i % 256), int(size))
self._write_record(CMD_RD, req)
cs_error, resp = self._read_record()
if cs_error:
LOG.debug(util.hexprint(resp))
raise errors.RadioError("Checksum error on read")
image += resp[2:]
if self.status_fn:
status = chirp_common.Status()
status.cur = i
status.max = end
status.msg = "Cloning from radio"
self.status_fn(status)
self._finish()
LOG.debug("Download Completed")
return memmap.MemoryMapBytes(image)
def _upload(self):
"""Talk to a Wouxun KG-UV980P and do a upload"""
try:
self._identify()
LOG.debug("Done with Upload Identify")
self._do_upload()
LOG.debug("Done with Mem and Settings Upload")
self._finish()
except errors.RadioError:
raise
except Exception:
raise errors.RadioError("Failed to communicate with radio: %s")
return
def _do_upload(self):
LOG.debug("Start of _do_upload")
cfg_map = config_map
endwrite = 0x73E7
for start, blocksize, count in cfg_map:
end = start + (blocksize * count)
LOG.debug("start = " + str(start))
LOG.debug("end = " + str(end))
LOG.debug("blksize = " + str(blocksize))
for addr in range(start, end, blocksize):
ptr = addr
req = struct.pack('>H', addr)
chunk = self.get_mmap()[ptr:ptr + blocksize]
self._write_record(CMD_WR, req + chunk)
LOG.debug(util.hexprint(req + chunk))
cserr, ack = self._read_record()
LOG.debug(util.hexprint(ack))
j = struct.unpack('>H', ack)[0]
if cserr or j != ptr:
raise errors.RadioError("Radio did not ack block %i" % ptr)
ptr += blocksize
if self.status_fn:
status = chirp_common.Status()
status.cur = ptr
status.max = endwrite
status.msg = "Cloning to radio"
self.status_fn(status)
def get_features(self):
rf = chirp_common.RadioFeatures()
rf.has_settings = True
rf.has_ctone = True
rf.has_rx_dtcs = True
rf.has_cross = True
rf.has_tuning_step = False
rf.has_bank = False
rf.can_odd_split = True
rf.valid_skips = ["", "S"]
rf.valid_tmodes = ["", "Tone", "TSQL", "DTCS", "Cross"]
rf.valid_cross_modes = [
"Tone->Tone",
"Tone->DTCS",
"DTCS->Tone",
"DTCS->",
"->Tone",
"->DTCS",
"DTCS->DTCS",
]
rf.valid_modes = ["FM", "NFM", "AM", "NAM"]
rf.valid_power_levels = self.POWER_LEVELS
rf.valid_name_length = 8
rf.valid_duplexes = ["", "-", "+", "split", "off"]
rf.valid_bands = [(26000000, 299999990), # supports VHF
(300000000, 999999990)] # supports UHF
rf.valid_characters = chirp_common.CHARSET_ASCII
rf.memory_bounds = (1, 999) # 999 memories
rf.valid_tuning_steps = STEPS
return rf
@classmethod
def get_prompts(cls):
rp = chirp_common.RadioPrompts()
rp.experimental = \
('This driver is experimental and may contain bugs. \n'
'USE AT YOUR OWN RISK - '
'SAVE A COPY OF DOWNLOAD FROM YOUR RADIO BEFORE MAKING CHANGES\n'
'Modification of Freq Limit Interfaces is done '
'AT YOUR OWN RISK and may affect performance or certification'
)
return rp
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
_valid = self._memobj.valid[mem.number]
_nam = self._memobj.names[number]
# Handle deleted channel quirks due to radio firmware
# not always clearing the valid channel indicators
# _valid == 0x80 indicates channel was deleted thru radio menu and
# only part of the channel structure data is cleared by firmware
# _valid == 0x00 indicates the channel is Valid
# all other values the firmware uses for _valid: look for valid
# Rx and Tx freq values to determine validity
if (_valid in MEM_INVALID or
((_valid != MEM_VALID) & ((_mem.rxfreq == 0xFFFFFFFF) or
_mem.rxfreq == 0x00000000))):
mem.empty = True
if _valid == 0x80:
LOG.debug("Ch %s was deleted by using radio menu" % number)
LOG.debug("Current Memory: \n%s" % _mem)
LOG.debug("Clearing Ch %s memory" % number)
_mem.set_raw(b"\xFF" * (_mem.size() // 8))
_nam.set_raw(b"\xFF" * (_nam.size() // 8))
self._memobj.valid[mem.number] = 0xFF
return mem
elif (_valid != MEM_VALID) & ((_mem.rxfreq != 0xFFFFFFFF) and
(_mem.rxfreq != 0x00000000)):
mem.empty = False
self._memobj.valid[mem.number] = MEM_VALID
else:
mem.empty = False
mem.freq = int(_mem.rxfreq) * 10
_rxfreq = _freq_decode(_mem.rxfreq)
_txfreq = _freq_decode(_mem.txfreq)
mem.freq = _rxfreq
if _mem.txfreq == 0xFFFFFFFF:
# TX freq not set
mem.duplex = "off"
mem.offset = 0
elif int(_rxfreq) == int(_txfreq):
mem.duplex = ""
mem.offset = 0
elif abs(_rxfreq - _txfreq) > 70000000:
mem.duplex = "split"
mem.offset = _txfreq
else:
mem.duplex = _rxfreq > _txfreq and "-" or "+"
mem.offset = abs(_rxfreq - _txfreq)
if _mem.named:
mem.name = _str_decode(self._memobj.names[number].name)
# Remove trailing whitespaces from the name
mem.name = mem.name.rstrip()
else:
mem.name = ''
self.tone_model.get_tone(_mem, mem)
mem.skip = "" if bool(_mem.scan_add) else "S"
pwr_index = _mem.power
if _mem.power == 3:
pwr_index = 2
if _mem.power:
mem.power = self.POWER_LEVELS[pwr_index]
else:
mem.power = self.POWER_LEVELS[0]
if _mem.am_mode:
if _mem.isnarrow:
mem.mode = "NAM"
else:
mem.mode = "AM"
else:
mem.mode = _mem.isnarrow and "NFM" or "FM"
mem.extra = RadioSettingGroup("Extra", "extra")
# Scrambler >8 is invalid default to off
if _mem.scrambler > 8:
_mem.scrambler = 0
return mem
def set_memory(self, mem):
# _mem = Stored Memory value
# mem = New value from user entry
number = mem.number
_mem = self._memobj.memory[number]
_nam = self._memobj.names[number]
if mem.empty:
self._memobj.valid[number] = 0xFF
_mem.set_raw(b"\xFF" * (_mem.size() // 8))
self._memobj.names[number].set_raw(b"\xFF" * (_nam.size() // 8))
else:
if len(mem.name) > 0:
_mem.named = True
name_encoded = _str_encode(mem.name)
for i in range(0, 8):
_nam.name[i] = ord(name_encoded[i])
else:
_mem.named = False
_mem.rxfreq = _freq_encode(mem.freq)
if mem.duplex == "off":
_mem.txfreq = 0xFFFFFFFF
elif mem.duplex == "split":
_mem.txfreq = _freq_encode(mem.offset)
elif mem.duplex == "+":
_mem.txfreq = _freq_encode(mem.freq + mem.offset)
elif mem.duplex == "-":
_mem.txfreq = _freq_encode(mem.freq - mem.offset)
else:
_mem.txfreq = _freq_encode(mem.freq)
_mem.scan_add = int(mem.skip != "S")
if mem.mode == "AM":
_mem.am_mode = True
_mem.isnarrow = False
elif mem.mode == "NAM":
_mem.am_mode = True
_mem.isnarrow = True
else:
_mem.am_mode = False
if mem.mode == "NFM":
_mem.isnarrow = True
else:
_mem.isnarrow = False
# set the tone
self.tone_model.set_tone(mem, _mem)
# set the scrambler and compander to off by default
# This changes them in the channel memory
_mem.scrambler = 0
_mem.compander = 0
# set the power
if mem.power is None:
_mem.power = 0 # Default to Low power
else:
index = self.POWER_LEVELS.index(mem.power)
if index == 2:
_mem.power = 0b11 # Force H to value of binary 3
else:
_mem.power = index
# Not sure what this bit does yet but it causes
# the radio to display
# MED power when the CPS shows Low Power.
# Forcing it to 0 to keep them
# consistent
_mem.extra_power_bit = 0
# Set other unknowns to 0 to match default CPS values
_mem.unknown1 = 0
_mem.unknown2 = 0
_mem.unknownbit2 = 0
_mem.unknown3 = 0
_mem.Unknown4_shft_dir = 0
_mem.unknown5 = 0
# set to mute mode to QT (not QT+DTMF or QT*DTMF) by default
# This changes them in the channel memory
_mem.mute_mode = 1
self._memobj.valid[number] = MEM_VALID
def _get_settings(self):
_settings = self._memobj.settings
_oem = self._memobj.oem
cfg_grp = RadioSettingGroup("cfg_grp", "Config Settings")
cfg1_grp = RadioSettingGroup("cfg1_grp", "Config Settings 1")
cfg2_grp = RadioSettingGroup("cfg2_grp", "Config Settings 2")
if self.MODEL == "KG-1000G Plus":
vfoaname = "Area A Settings"
else:
vfoaname = "VFO A Settings"
vfoa_grp = RadioSettingGroup("vfoa_grp", vfoaname)
vfo150_grp = RadioSettingGroup("vfo150_grp", "150M Settings")
vfo450_grp = RadioSettingGroup("vfo450_grp", "450M Settings")
vfo20_grp = RadioSettingGroup("vfo20_grp", "20M Settings")
vfo50_grp = RadioSettingGroup("vfo50_grp", "50M Settings")
vfo350_grp = RadioSettingGroup("vfo350_grp", "350M Settings")
vfo850_grp = RadioSettingGroup("vfo850_grp", "850M Settings")
if self.MODEL == "KG-1000G Plus":
vfobname = "Area B Settings"
else:
vfobname = "VFO B Settings"
vfob_grp = RadioSettingGroup("vfob_grp", vfobname)
fmradio_grp = RadioSettingGroup("fmradio_grp", "FM Broadcast Memory")
lmt_grp = RadioSettingGroup("lmt_grp", "Frequency Limits")
lmwrn_grp = RadioSettingGroup("lmwrn_grp", "USE AT YOUR OWN RISK")
rxlim_grp = RadioSettingGroup("rxlim_grp", "Rx Limits")
txlim_grp = RadioSettingGroup("txlim_grp", "Tx Limits")
oem_grp = RadioSettingGroup("oem_grp", "OEM Info")
scan_grp = RadioSettingGroup("scan_grp", "Scan Group")
scanname_grp = RadioSettingGroup("scanname_grp", "Scan Names")
remote_grp = RadioSettingGroup("remote_grp", "Remote Settings")
extra_grp = RadioSettingGroup("extra_grp",
"Extra Settings")
if self.MODEL == "KG-1000G Plus":
vfoname = "Freq Mode Settings"
else:
vfoname = "VFO Settings"
vfo_grp = RadioSettingGroup("vfo_grp",
vfoname)
extra_grp.append(oem_grp)
cfg_grp.append(cfg1_grp)
cfg_grp.append(cfg2_grp)
lmt_grp.append(lmwrn_grp)
lmt_grp.append(rxlim_grp)
extra_grp.append(lmt_grp)
vfo_grp.append(vfoa_grp)
vfo_grp.append(vfob_grp)
vfoa_grp.append(vfo150_grp)
vfoa_grp.append(vfo450_grp)
if self.MODEL == "KG-UV980P":
vfoa_grp.append(vfo20_grp)
lmt_grp.append(txlim_grp)
vfoa_grp.append(vfo50_grp)
vfoa_grp.append(vfo350_grp)
vfoa_grp.append(vfo850_grp)
scan_grp.append(scanname_grp)
group = RadioSettings(cfg_grp, vfo_grp, fmradio_grp,
remote_grp, scan_grp, extra_grp)
# Configuration Settings
if self.MODEL == "KG-1000G Plus":
voicemap = VOICE_MAP_1000GPLUS
pttidmap = PTT_ID_MAP_1000GPLUS
pttidlabel = "DTMF ID"
dtmflist = DTMF_ST_LIST_1000GPLUS
dtmflabel = "Sidetone"
spkmap = SPEAKER_MAP_1000GPLUS
key_l = KEY_LIST_1000GPLUS
scqt = SC_QT_MAP_1000GPLUS
pf1set = PF1_SETTINGS_1000GPLUS
wmmap = WORKMODE_MAP_1000GPLUS
totmap = TOT_MAP_1000GPLUS
lowvlabel = "Voltage Alert"
fanlabel = "Fan Setting"
alerttonelabel = "Alert Tone(Hz)"
tonescanlabel = "Tone Save"
pttdelaylabel = "DTMF ID Delay (ms)"
scandetlabel = "Tone Scan"
txvoltlabel = "Tx Voltage Limit"
holdtimrptlabel = "Repeater Hold Time (ms)"
thrvollvllabel = "Tx Voltage Min"
modepswlabel = "Freq Mode Password"
narrow1label = "150M W/N"
mute1label = "150M SP Mute"
scram1label = "150M Descrambler"
narrow2label = "450M W/N"
mute2label = "450M SP Mute"
scram2label = "450M Descrambler"
narrow3label = "20M W/N"
mute3label = "20M SP Mute"
scram3label = "20M Descrambler"
narrow4label = "50M W/N"
mute4label = "50M SP Mute"
scram4label = "50M Descrambler"
narrow5label = "350M W/N"
mute5label = "350M SP Mute"
scram5label = "350M Descrambler"
narrow6label = "850M W/N"
mute6label = "850M SP Mute"
scram6label = "850M Descrambler"
narrow7label = "W/N"
mute7label = "SP Mute"
scram7label = "Descrambler"
else: # 980P or 1000G radios
voicemap = VOICE_MAP
pttidmap = PTT_ID_MAP
pttidlabel = "Caller ID Tx Mode (PTT_ID)"
dtmflist = DTMF_ST_LIST
dtmflabel = "DTMF Sidetone"
spkmap = SPEAKER_MAP
key_l = KEY_LIST
scqt = SC_QT_MAP
pf1set = PF1_SETTINGS
wmmap = WORKMODE_MAP
totmap = TOT_MAP
lowvlabel = "Low Voltage Shutoff"
fanlabel = "Fan Mode"
alerttonelabel = "Alert Pulse (Hz)"
tonescanlabel = "CTCSS/DCS Scan"
pttdelaylabel = "Caller ID Tx Delay PTT-ID-DLY (ms)"
scandetlabel = "Scan DET"
txvoltlabel = "Threshold Voltage Tx"
holdtimrptlabel = "Hold Time of Repeat (ms)"
thrvollvllabel = "Threshold Voltage Level"
modepswlabel = "MODE PSW"
narrow1label = "150M Bandwidth"
mute1label = "150M Mute Mode"
scram1label = "150M Scrambler"
narrow2label = "450M Bandwidth"
mute2label = "450M Mute Mode"
scram2label = "450M Scrambler"
narrow3label = "20M Bandwidth"
mute3label = "20M Mute Mode"
scram3label = "20M Scrambler"
narrow4label = "50M Bandwidth"
mute4label = "50M Mute Mode"
scram4label = "50M Scrambler"
narrow5label = "350M Bandwidth"
mute5label = "350M Mute Mode"
scram5label = "350M Scrambler"
narrow6label = "850M Bandwidth"
mute6label = "850M Mute Mode"
scram6label = "850M Scrambler"
narrow7label = "Bandwidth"
mute7label = "Mute Mode"
scram7label = "Scrambler"
rs = RadioSetting("roger", "Roger Beep",
RadioSettingValueList(ROGER_LIST,
current_index=_settings.
roger))
cfg1_grp.append(rs)
rs = RadioSetting("beep", "Keypad Beep",
RadioSettingValueBoolean(_settings.beep))
cfg1_grp.append(rs)
rs = RadioSetting("voice", "Voice Guide",
RadioSettingValueMap(voicemap,
_settings.voice))
cfg1_grp.append(rs)
rs = RadioSetting("bcl_a", "Busy Channel Lock-out A",
RadioSettingValueBoolean(_settings.bcl_a))
cfg1_grp.append(rs)
rs = RadioSetting("bcl_b", "Busy Channel Lock-out B",
RadioSettingValueBoolean(_settings.bcl_b))
cfg1_grp.append(rs)
rs = RadioSetting("sc_rev", "Scan Mode",
RadioSettingValueMap(SC_REV_MAP, _settings.sc_rev))
cfg1_grp.append(rs)
rs = RadioSetting("tot", "Timeout Timer (TOT)",
RadioSettingValueMap(
totmap, _settings.tot))
cfg1_grp.append(rs)
if self.MODEL != "KG-1000G Plus":
rs = RadioSetting("toa", "Timeout Alarm (TOA)",
RadioSettingValueList(
TOA_LIST, current_index=_settings.toa))
else:
rs = RadioSetting("toa", "Overtime Alarm (TOA)",
RadioSettingValueMap(
TOA_MAP, _settings.toa))
cfg1_grp.append(rs)
if self.MODEL != "KG-1000G Plus":
rs = RadioSetting("ani_sw", "Caller ID Tx - ANI-SW",
RadioSettingValueBoolean(_settings.ani_sw))
cfg1_grp.append(rs)
if self.MODEL != "KG-1000G Plus":
rs = RadioSetting("ring", "Ring Time (Sec)",
RadioSettingValueList(
RING_LIST,
current_index=_settings.ring))
else:
rs = RadioSetting("ring", "Ring Time (Sec)",
RadioSettingValueMap(
RING_MAP, _settings.ring))
cfg1_grp.append(rs)
rs = RadioSetting("dtmfsf", dtmflabel,
RadioSettingValueList(
dtmflist,
current_index=_settings.dtmfsf))
cfg1_grp.append(rs)
rs = RadioSetting("ptt_id", pttidlabel,
RadioSettingValueMap(pttidmap, _settings.ptt_id))
cfg1_grp.append(rs)
rs = RadioSetting("wt_led", "Standby / WT LED",
RadioSettingValueList(
BACKLIGHT_LIST,
current_index=_settings.wt_led))
cfg1_grp.append(rs)
rs = RadioSetting("tx_led", "TX LED",
RadioSettingValueList(
BACKLIGHT_LIST,
current_index=_settings.tx_led))
cfg1_grp.append(rs)
rs = RadioSetting("rx_led", "Rx LED",
RadioSettingValueList(
BACKLIGHT_LIST,
current_index=_settings.rx_led))
cfg1_grp.append(rs)
if self.MODEL == "KG-1000G Plus":
rs = RadioSetting("prich_sw", "Priority Scan",
RadioSettingValueList(
PRI_CH_SCAN_LIST,
current_index=_settings.prich_sw))
cfg1_grp.append(rs)
else:
rs = RadioSetting("prich_sw", "Priority Channel Scan",
RadioSettingValueBoolean(_settings.prich_sw))
cfg1_grp.append(rs)
rs = RadioSetting("spk_cont", "Speaker Control",
RadioSettingValueMap(
spkmap,
_settings.spk_cont))
cfg1_grp.append(rs)
rs = RadioSetting("autolock", "Autolock",
RadioSettingValueBoolean(_settings.autolock))
cfg1_grp.append(rs)
rs = RadioSetting("low_v", lowvlabel,
RadioSettingValueBoolean(_settings.low_v))
cfg1_grp.append(rs)
rs = RadioSetting("fan", fanlabel,
RadioSettingValueList(
FAN_MODE_LIST,
current_index=_settings.fan))
cfg1_grp.append(rs)
rs = RadioSetting("apo_time", "Auto Power-Off (Min)",
RadioSettingValueList(
APO_TIME_LIST,
current_index=_settings.apo_time))
cfg1_grp.append(rs)
rs = RadioSetting("alert", alerttonelabel,
RadioSettingValueMap(ALERT_MAP, _settings.alert))
cfg1_grp.append(rs)
rs = RadioSetting("m_pwr", "Medium Power Level (W)",
RadioSettingValueMap(M_POWER_MAP,
_settings.m_pwr))
cfg1_grp.append(rs)
if self.MODEL != "KG-1000G Plus":
rs = RadioSetting("rpt_set_model", "Model (RPT-SET)",
RadioSettingValueList(
RPT_MODE_LIST,
current_index=_settings.rpt_set_model))
else:
rs = RadioSetting("rpt_set_model", "Repeater Mode",
RadioSettingValueMap(
RPT_MODE_MAP,
_settings.rpt_set_model))
cfg2_grp.append(rs)
rs = RadioSetting("rpt_spk", "Repeater Speaker Switch (RPT-SPK)",
RadioSettingValueBoolean(_settings.rpt_spk))
cfg2_grp.append(rs)
rs = RadioSetting("rpt_ptt", "Repeater PTT (RPT-PTT)",
RadioSettingValueBoolean(_settings.rpt_ptt))
cfg2_grp.append(rs)
rs = RadioSetting("dtmf_time", "DTMF Tx Duration (ms)",
RadioSettingValueList(
DTMF_TIMES,
current_index=_settings.dtmf_time))
cfg2_grp.append(rs)
rs = RadioSetting("dtmf_int", "DTMF Interval (ms)",
RadioSettingValueList(
DTMF_INTERVALS,
current_index=_settings.dtmf_int))
cfg2_grp.append(rs)
rs = RadioSetting("sc_qt", tonescanlabel,
RadioSettingValueMap(
scqt, _settings.sc_qt))
cfg2_grp.append(rs)
rs = RadioSetting("pri_ch", "Priority Channel",
RadioSettingValueInteger(
1, 999, _chnum_decode(_settings.pri_ch)))
cfg2_grp.append(rs)
rs = RadioSetting("ptt_id_dly", pttdelaylabel,
RadioSettingValueMap(PTT_ID_DELAY_MAP,
_settings.ptt_id_dly))
cfg2_grp.append(rs)
rs = RadioSetting("rc_sw", "Remote Control RC-SW",
RadioSettingValueBoolean(_settings.rc_sw))
cfg2_grp.append(rs)
rs = RadioSetting("scan_det", scandetlabel,
RadioSettingValueBoolean(_settings.scan_det))
cfg2_grp.append(rs)
rs = RadioSetting("menu", "Menu Available",
RadioSettingValueBoolean(_settings.menu))
cfg2_grp.append(rs)
rs = RadioSetting("thr_vol_tx", txvoltlabel,
RadioSettingValueBoolean(_settings.thr_vol_tx))
cfg2_grp.append(rs)
rs = RadioSetting("hold_time_rpt", holdtimrptlabel,
RadioSettingValueList(
HOLD_TIMES,
current_index=_settings.hold_time_rpt))
cfg2_grp.append(rs)
rs = RadioSetting("auto_am", "Auto AM",
RadioSettingValueBoolean(_settings.auto_am))
cfg2_grp.append(rs)
rs = RadioSetting("rpt_tone", "Repeat Tone",
RadioSettingValueBoolean(_settings.rpt_tone))
cfg2_grp.append(rs)
rs = RadioSetting("pf1_set", "PF1 setting",
RadioSettingValueList(
pf1set,
current_index=_settings.pf1_set))
cfg2_grp.append(rs)
rs = RadioSetting("settings.thr_vol_lvl", thrvollvllabel,
RadioSettingValueFloat(
9.5, 10.5, _settings.thr_vol_lvl / 100.0, 0.1, 1))
cfg2_grp.append(rs)
dtmfchars = "0123456789"
_code = ''
test = int(_oem.mode_psw)
_codeobj = '0x{0:0{1}X}'.format(test, 6)
LOG.debug("codeobj = %s" % _codeobj)
_psw = str(_codeobj)
for i in range(2, 8):
LOG.debug("psw[i] = %s" % _psw[i])
if _psw[i] in dtmfchars:
_code += _psw[i]
val_psw = RadioSettingValueString(6, 6, _code, False)
val_psw.set_charset(dtmfchars)
rs = RadioSetting("oem.mode_psw", modepswlabel, val_psw)
def apply_psw_id(setting, obj):
val2 = hex(int(str(val_psw), 16))
if (int(val2, 16) != 0):
while len(val2) < 8:
val2 += '0'
psw = int(str(val2), 16)
obj.mode_psw = psw
rs.set_apply_callback(apply_psw_id, _oem)
cfg2_grp.append(rs)
rs = RadioSetting("ABR", "Backlight On Time (ABR)",
RadioSettingValueList(
ABR_LIST,
current_index=_settings.ABR))
cfg2_grp.append(rs)
rs = RadioSetting("KeyA", "Key A",
RadioSettingValueList(
key_l,
current_index=_settings.KeyA))
cfg2_grp.append(rs)
rs = RadioSetting("KeyB", "Key B",
RadioSettingValueList(
key_l,
current_index=_settings.KeyB))
cfg2_grp.append(rs)
rs = RadioSetting("KeyC", "Key C",
RadioSettingValueList(
key_l,
current_index=_settings.KeyC))
cfg2_grp.append(rs)
if self.MODEL == "KG-1000G Plus":
rs = RadioSetting("KeyD", "Key D",
RadioSettingValueList(
KEY_LIST_1000GPLUS,
current_index=_settings.KeyD))
cfg2_grp.append(rs)
rs = RadioSetting("key_lock", "Key Lock Active",
RadioSettingValueBoolean(_settings.key_lock))
cfg2_grp.append(rs)
rs = RadioSetting("act_area", "Active Area (BAND)",
RadioSettingValueList(
ACTIVE_AREA_LIST,
current_index=_settings.act_area))
cfg2_grp.append(rs)
rs = RadioSetting("tdr_off", "TDR",
RadioSettingValueList(
TDR_LIST,
current_index=_settings.tdr_off))
cfg2_grp.append(rs)
# Freq Limits settings
# Convert Integer back to correct limit HEX value:
s = self._memobj
_temp = int(s.bandlimits.limit_144M_ChA_rx_start) // 10
val = RadioSettingValueInteger(0, 999, _temp)
rs = RadioSetting("bandlimits.limit_144M_ChA_rx_start",
"144M Area A Rx Lower Limit (MHz)",
val)
rxlim_grp.append(rs)
_temp = int(s.bandlimits.limit_144M_ChA_rx_stop) // 10
val = RadioSettingValueInteger(0, 999, _temp)
rs = RadioSetting("bandlimits.limit_144M_ChA_rx_stop",
"144M Area A Rx Upper Limit (+ .9975 MHz)",
val)
rxlim_grp.append(rs)
_temp = int(s.bandlimits.limit_144M_ChB_rx_start) // 10
val = RadioSettingValueInteger(0, 999, _temp)
rs = RadioSetting("bandlimits.limit_144M_ChB_rx_start",
"144M Area B Rx Lower Limit (MHz)",
val)
rxlim_grp.append(rs)
_temp = int(s.bandlimits.limit_144M_ChB_rx_stop) // 10
val = RadioSettingValueInteger(0, 999, _temp)
rs = RadioSetting("bandlimits.limit_144M_ChB_rx_stop",
"144M Area B Rx Upper Limit (+ .9975 MHz)",
val)
rxlim_grp.append(rs)
_temp = int(s.bandlimits.limit_70cm_rx_start) // 10
val = RadioSettingValueInteger(0, 999, _temp)
rs = RadioSetting("bandlimits.limit_70cm_rx_start",
"450M Rx Lower Limit (MHz)",
val)
rxlim_grp.append(rs)
_temp = int(s.bandlimits.limit_70cm_rx_stop) // 10
val = RadioSettingValueInteger(0, 999, _temp)
rs = RadioSetting("bandlimits.limit_70cm_rx_stop",
"450M Rx Upper Limit (+ .9975 MHz)",
val)
rxlim_grp.append(rs)
if self.MODEL == "KG-UV980P":
_temp = int(s.bandlimits.limit_10m_rx_start) // 10
val = RadioSettingValueInteger(0, 999, _temp)
rs = RadioSetting("bandlimits.limit_10m_rx_start",
"20M Rx Lower Limit (MHz)",
val)
rxlim_grp.append(rs)
_temp = int(s.bandlimits.limit_10m_rx_stop) // 10
val = RadioSettingValueInteger(0, 999, _temp)
rs = RadioSetting("bandlimits.limit_10m_rx_stop",
"20M Rx Upper Limit (+ .9975 MHz)",
val)
rxlim_grp.append(rs)
_temp = int(s.bandlimits.limit_6m_rx_start) // 10
val = RadioSettingValueInteger(0, 999, _temp)
rs = RadioSetting("bandlimits.limit_6m_rx_start",
"50M Rx Lower Limit (MHz)",
val)
rxlim_grp.append(rs)
_temp = int(s.bandlimits.limit_6m_rx_stop) // 10
val = RadioSettingValueInteger(0, 999, _temp)
rs = RadioSetting("bandlimits.limit_6m_rx_stop",
"50M Rx Upper Limit (+ .9975 MHz)",
val)
rxlim_grp.append(rs)
_temp = int(s.bandlimits.limit_350M_rx_start) // 10
val = RadioSettingValueInteger(0, 999, _temp)
rs = RadioSetting("bandlimits.limit_350M_rx_start",
"350M Rx Lower Limit (MHz)",
val)
rxlim_grp.append(rs)
_temp = int(s.bandlimits.limit_350M_rx_stop) // 10
val = RadioSettingValueInteger(0, 999, _temp)
rs = RadioSetting("bandlimits.limit_350M_rx_stop",
"350M Rx Upper Limit (+ .9975 MHz)",
val)
rxlim_grp.append(rs)
_temp = int(s.bandlimits.limit_850M_rx_start) // 10
val = RadioSettingValueInteger(0, 999, _temp)
rs = RadioSetting("bandlimits.limit_850M_rx_start",
"850M Rx Lower Limit (MHz)",
val)
rxlim_grp.append(rs)
_temp = int(s.bandlimits.limit_850M_rx_stop) // 10
val = RadioSettingValueInteger(0, 999, _temp)
rs = RadioSetting("bandlimits.limit_850M_rx_stop",
"850M Rx Upper Limit (+ .9975 MHz)",
val)
rxlim_grp.append(rs)
if self.MODEL == "KG-UV980P":
_temp = int(s.bandlimits.limit_144M_tx_start) // 10
val = RadioSettingValueInteger(0, 999, _temp)
rs = RadioSetting("bandlimits.limit_144M_tx_start",
"144M Tx Lower Limit (MHz)",
val)
txlim_grp.append(rs)
_temp = int(s.bandlimits.limit_144M_tx_stop) // 10
val = RadioSettingValueInteger(0, 999, _temp)
rs = RadioSetting("bandlimits.limit_144M_tx_stop",
"144M Tx Upper Limit (+ .9975 MHz)",
val)
txlim_grp.append(rs)
_temp = int(s.bandlimits.limit_70cm_tx_start) // 10
val = RadioSettingValueInteger(0, 999, _temp)
rs = RadioSetting("bandlimits.limit_70cm_tx_start",
"450M Tx Lower Limit (MHz)",
val)
txlim_grp.append(rs)
_temp = int(s.bandlimits.limit_70cm_tx_stop) // 10
val = RadioSettingValueInteger(0, 999, _temp)
rs = RadioSetting("bandlimits.limit_70cm_tx_stop",
"450M tx Upper Limit (+ .9975 MHz)",
val)
txlim_grp.append(rs)
if self.MODEL == "KG-UV980P":
_temp = int(s.bandlimits.limit_10m_tx_start) // 10
val = RadioSettingValueInteger(0, 999, _temp)
rs = RadioSetting("bandlimits.limit_10m_tx_start",
"20M tx Lower Limit (MHz)",
val)
txlim_grp.append(rs)
_temp = int(s.bandlimits.limit_10m_tx_stop) // 10
val = RadioSettingValueInteger(0, 999, _temp)
rs = RadioSetting("bandlimits.limit_10m_tx_stop",
"20M tx Upper Limit (+ .9975 MHz)",
val)
txlim_grp.append(rs)
_temp = int(s.bandlimits.limit_6m_tx_start) // 10
val = RadioSettingValueInteger(0, 999, _temp)
rs = RadioSetting("bandlimits.limit_6m_tx_start",
"50M tx Lower Limit (MHz)",
val)
txlim_grp.append(rs)
_temp = int(s.bandlimits.limit_6m_tx_stop) // 10
val = RadioSettingValueInteger(0, 999, _temp)
rs = RadioSetting("bandlimits.limit_6m_tx_stop",
"50M tx Upper Limit (+ .9975 MHz)",
val)
txlim_grp.append(rs)
# VFO Settings
rs = RadioSetting("vfomode_a", "Working Mode",
RadioSettingValueMap(wmmap,
_settings.vfomode_a))
vfoa_grp.append(rs)
rs = RadioSetting("vfoband_a", "Current Band",
RadioSettingValueMap(VFOBAND_MAP,
_settings.vfoband_a))
vfoa_grp.append(rs)
rs = RadioSetting("vfochan_a", "Active/Work Channel",
RadioSettingValueInteger(1, 999,
_chnum_decode(
_settings.vfochan_a)))
vfoa_grp.append(rs)
rs = RadioSetting("vfosquelch_a", "Squelch",
RadioSettingValueInteger(0, 9,
_settings.vfosquelch_a))
vfoa_grp.append(rs)
rs = RadioSetting("vfostep_a", "Step",
RadioSettingValueList(
STEP_LIST,
current_index=_settings.vfostep_a))
vfoa_grp.append(rs)
# #####################
rs = RadioSetting("vfofreq1", "150M Freq",
RadioSettingValueFloat(
0, 999.999999, (_freq_decode
(_settings.vfofreq1) /
1000000.0), 0.000001, 6))
vfo150_grp.append(rs)
if self.MODEL != "KG-1000G Plus":
rs = RadioSetting("vfoofst1", "150M Offset",
RadioSettingValueFloat(
0, 999.999999, (_freq_decode
(_settings.vfoofst1) /
1000000.0), 0.000001, 6))
vfo150_grp.append(rs)
rs = RadioSetting("rxtone1", "150M Rx tone",
RadioSettingValueMap(
TONE_MAP, _settings.rxtone1))
vfo150_grp.append(rs)
rs = RadioSetting("txtone1", "150M Tx tone",
RadioSettingValueMap(
TONE_MAP, _settings.txtone1))
vfo150_grp.append(rs)
rs = RadioSetting("power1", "150M Power",
RadioSettingValueMap(
POWER_MAP, _settings.power1))
vfo150_grp.append(rs)
rs = RadioSetting("narrow1", narrow1label,
RadioSettingValueMap(
BANDWIDTH_MAP, _settings.narrow1))
vfo150_grp.append(rs)
rs = RadioSetting("mute1", mute1label,
RadioSettingValueMap(
MUTE_MODE_MAP, _settings.mute1))
vfo150_grp.append(rs)
if self.MODEL == "KG-1000G Plus":
rs = RadioSetting("shft_dir1", "150M Repeater",
RadioSettingValueBoolean(
_settings.shft_dir1))
else:
rs = RadioSetting("shft_dir1", "150M Shift Direction",
RadioSettingValueList(
DUPLEX_LIST,
current_index=_settings.shft_dir1))
vfo150_grp.append(rs)
rs = RadioSetting("compander1", "150M Compander",
RadioSettingValueBoolean(
_settings.compander1))
vfo150_grp.append(rs)
rs = RadioSetting("scrambler1", scram1label,
RadioSettingValueList(
SCRAMBLER_LIST,
current_index=_settings.scrambler1))
vfo150_grp.append(rs)
rs = RadioSetting("am_mode1", "150M AM Mode",
RadioSettingValueBoolean(
_settings.am_mode1))
vfo150_grp.append(rs)
# ###########################
rs = RadioSetting("vfofreq2", "450M Freq",
RadioSettingValueFloat(
0, 999.999999, (_freq_decode(
_settings.vfofreq2) /
1000000.0), 0.000001, 6))
vfo450_grp.append(rs)
if self.MODEL != "KG-1000G Plus":
rs = RadioSetting("vfoofst2", "450M Offset",
RadioSettingValueFloat(
0, 999.999999, (_freq_decode(
_settings.vfoofst2) /
1000000.0), 0.000001, 6))
vfo450_grp.append(rs)
rs = RadioSetting("rxtone2", "450M Rx tone",
RadioSettingValueMap(
TONE_MAP, _settings.rxtone2))
vfo450_grp.append(rs)
rs = RadioSetting("txtone2", "450M Tx tone",
RadioSettingValueMap(
TONE_MAP, _settings.txtone2))
vfo450_grp.append(rs)
rs = RadioSetting("power2", "450M Power",
RadioSettingValueMap(
POWER_MAP, _settings.power2))
vfo450_grp.append(rs)
rs = RadioSetting("narrow2", narrow2label,
RadioSettingValueMap(
BANDWIDTH_MAP, _settings.narrow2))
vfo450_grp.append(rs)
rs = RadioSetting("mute2", mute2label,
RadioSettingValueMap(
MUTE_MODE_MAP, _settings.mute2))
vfo450_grp.append(rs)
if self.MODEL == "KG-1000G Plus":
rs = RadioSetting("shft_dir2", "450M Repeater",
RadioSettingValueBoolean(
_settings.shft_dir2))
else:
rs = RadioSetting("shft_dir2", "450M Shift Direction",
RadioSettingValueList(
DUPLEX_LIST,
current_index=_settings.shft_dir2))
vfo450_grp.append(rs)
rs = RadioSetting("compander2", "450M Compander",
RadioSettingValueBoolean(
_settings.compander2))
vfo450_grp.append(rs)
rs = RadioSetting("scrambler2", scram2label,
RadioSettingValueList(
SCRAMBLER_LIST,
current_index=_settings.scrambler2))
vfo450_grp.append(rs)
rs = RadioSetting("am_mode2", "450M AM Mode",
RadioSettingValueBoolean(
_settings.am_mode2))
vfo450_grp.append(rs)
# ###########################
if self.MODEL == "KG-UV980P":
rs = RadioSetting("vfofreq3", "20M Freq",
RadioSettingValueFloat(
0, 999.999999, (_freq_decode(
_settings.vfofreq3) /
1000000.0), 0.000001, 6))
vfo20_grp.append(rs)
if self.MODEL == "KG-UV980P":
rs = RadioSetting("vfoofst3", "20M Offset",
RadioSettingValueFloat(
0, 999.999999, (_freq_decode(
_settings.vfoofst3) /
1000000.0), 0.000001, 6))
vfo20_grp.append(rs)
rs = RadioSetting("rxtone3", "20M Rx tone",
RadioSettingValueMap(
TONE_MAP, _settings.rxtone3))
vfo20_grp.append(rs)
rs = RadioSetting("txtone3", "20M Tx tone",
RadioSettingValueMap(
TONE_MAP, _settings.txtone3))
vfo20_grp.append(rs)
rs = RadioSetting("power3", "20M Power",
RadioSettingValueMap(
POWER_MAP, _settings.power3))
vfo20_grp.append(rs)
rs = RadioSetting("narrow3", narrow3label,
RadioSettingValueMap(
BANDWIDTH_MAP, _settings.narrow3))
vfo20_grp.append(rs)
rs = RadioSetting("mute3", mute3label,
RadioSettingValueMap(
MUTE_MODE_MAP, _settings.mute3))
vfo20_grp.append(rs)
if self.MODEL == "KG-1000G Plus":
rs = RadioSetting("shft_dir3", "20M Repeater",
RadioSettingValueBoolean(
_settings.shft_dir3))
else:
rs = RadioSetting("shft_dir3", "20M Shift Direction",
RadioSettingValueList(
DUPLEX_LIST,
current_index=_settings.shft_dir3))
vfo20_grp.append(rs)
rs = RadioSetting("compander3", "20M Compander",
RadioSettingValueBoolean(
_settings.compander3))
vfo20_grp.append(rs)
rs = RadioSetting("scrambler3", scram3label,
RadioSettingValueList(
SCRAMBLER_LIST,
current_index=_settings.scrambler3))
vfo20_grp.append(rs)
rs = RadioSetting("am_mode3", "20M AM Mode",
RadioSettingValueBoolean(
_settings.am_mode3))
vfo20_grp.append(rs)
# ###########################
rs = RadioSetting("vfofreq4", "50M Freq",
RadioSettingValueFloat(
0, 999.999999, (_freq_decode(
_settings.vfofreq4) /
1000000.0), 0.000001, 6))
vfo50_grp.append(rs)
if self.MODEL != "KG-1000G Plus":
rs = RadioSetting("vfoofst4", "50M Offset",
RadioSettingValueFloat(
0, 999.999999, (_freq_decode(
_settings.vfoofst4) /
1000000.0), 0.000001, 6))
vfo50_grp.append(rs)
rs = RadioSetting("rxtone4", "50M Rx tone",
RadioSettingValueMap(
TONE_MAP, _settings.rxtone4))
vfo50_grp.append(rs)
rs = RadioSetting("txtone4", "50M Tx tone",
RadioSettingValueMap(
TONE_MAP, _settings.txtone4))
vfo50_grp.append(rs)
rs = RadioSetting("power4", "50M Power",
RadioSettingValueMap(
POWER_MAP, _settings.power4))
vfo50_grp.append(rs)
rs = RadioSetting("narrow4", narrow4label,
RadioSettingValueMap(
BANDWIDTH_MAP, _settings.narrow4))
vfo50_grp.append(rs)
rs = RadioSetting("mute4", mute4label,
RadioSettingValueMap(
MUTE_MODE_MAP, _settings.mute4))
vfo50_grp.append(rs)
if self.MODEL == "KG-1000G Plus":
rs = RadioSetting("shft_dir4", "50M Repeater",
RadioSettingValueBoolean(
_settings.shft_dir4))
else:
rs = RadioSetting("shft_dir4", "50M Shift Direction",
RadioSettingValueList(
DUPLEX_LIST,
current_index=_settings.shft_dir4))
vfo50_grp.append(rs)
rs = RadioSetting("compander4", "50M Compander",
RadioSettingValueBoolean(
_settings.compander4))
vfo50_grp.append(rs)
rs = RadioSetting("scrambler4", scram4label,
RadioSettingValueList(
SCRAMBLER_LIST,
current_index=_settings.scrambler4))
vfo50_grp.append(rs)
rs = RadioSetting("am_mode4", "50M AM Mode",
RadioSettingValueBoolean(
_settings.am_mode4))
vfo50_grp.append(rs)
# ###########################
rs = RadioSetting("vfofreq5", "350M Freq",
RadioSettingValueFloat(
0, 999.999999, (_freq_decode(
_settings.vfofreq5) /
1000000.0), 0.000001, 6))
vfo350_grp.append(rs)
if self.MODEL != "KG-1000G Plus":
rs = RadioSetting("vfoofst5", "350M Offset",
RadioSettingValueFloat(
0, 999.999999, (_freq_decode(
_settings.vfoofst5) /
1000000.0), 0.000001, 6))
vfo350_grp.append(rs)
rs = RadioSetting("rxtone5", "350M Rx tone",
RadioSettingValueMap(
TONE_MAP, _settings.rxtone5))
vfo350_grp.append(rs)
rs = RadioSetting("txtone5", "350M Tx tone",
RadioSettingValueMap(
TONE_MAP, _settings.txtone5))
vfo350_grp.append(rs)
rs = RadioSetting("power5", "350M Power",
RadioSettingValueMap(
POWER_MAP, _settings.power5))
vfo350_grp.append(rs)
rs = RadioSetting("narrow5", narrow5label,
RadioSettingValueMap(
BANDWIDTH_MAP, _settings.narrow5))
vfo350_grp.append(rs)
rs = RadioSetting("mute5", mute5label,
RadioSettingValueMap(
MUTE_MODE_MAP, _settings.mute5))
vfo350_grp.append(rs)
if self.MODEL == "KG-1000G Plus":
rs = RadioSetting("shft_dir5", "350M Repeater",
RadioSettingValueBoolean(
_settings.shft_dir5))
else:
rs = RadioSetting("shft_dir5", "350M Shift Direction",
RadioSettingValueList(
DUPLEX_LIST,
current_index=_settings.shft_dir5))
vfo350_grp.append(rs)
rs = RadioSetting("compander5", "350M Compander",
RadioSettingValueBoolean(
_settings.compander5))
vfo350_grp.append(rs)
rs = RadioSetting("scrambler5", scram5label,
RadioSettingValueList(
SCRAMBLER_LIST,
current_index=_settings.scrambler5))
vfo350_grp.append(rs)
rs = RadioSetting("am_mode5", "350M AM Mode",
RadioSettingValueBoolean(
_settings.am_mode5))
vfo350_grp.append(rs)
# ############################
rs = RadioSetting("vfofreq6", "850M Freq",
RadioSettingValueFloat(
0, 999.999999, (_freq_decode(
_settings.vfofreq6) /
1000000.0), 0.000001, 6))
vfo850_grp.append(rs)
if self.MODEL != "KG-1000G Plus":
rs = RadioSetting("vfoofst6", "850M Offset",
RadioSettingValueFloat(
0, 999.999999, (_freq_decode(
_settings.vfoofst6) /
1000000.0), 0.000001, 6))
vfo850_grp.append(rs)
rs = RadioSetting("rxtone6", "850M Rx tone",
RadioSettingValueMap(
TONE_MAP, _settings.rxtone6))
vfo850_grp.append(rs)
rs = RadioSetting("txtone6", "850M Tx tone",
RadioSettingValueMap(
TONE_MAP, _settings.txtone6))
vfo850_grp.append(rs)
rs = RadioSetting("power6", "850M Power",
RadioSettingValueMap(
POWER_MAP, _settings.power6))
vfo850_grp.append(rs)
rs = RadioSetting("narrow6", narrow6label,
RadioSettingValueMap(
BANDWIDTH_MAP, _settings.narrow6))
vfo850_grp.append(rs)
rs = RadioSetting("mute6", mute6label,
RadioSettingValueMap(
MUTE_MODE_MAP, _settings.mute6))
vfo850_grp.append(rs)
if self.MODEL == "KG-1000G Plus":
rs = RadioSetting("shft_dir6", "850M Repeater",
RadioSettingValueBoolean(
_settings.shft_dir6))
else:
rs = RadioSetting("shft_dir6", "850M Shift Direction",
RadioSettingValueList(
DUPLEX_LIST,
current_index=_settings.shft_dir6))
vfo850_grp.append(rs)
rs = RadioSetting("compander6", "850M Compander",
RadioSettingValueBoolean(
_settings.compander6))
vfo850_grp.append(rs)
rs = RadioSetting("scrambler6", scram6label,
RadioSettingValueList(
SCRAMBLER_LIST,
current_index=_settings.scrambler6))
vfo850_grp.append(rs)
rs = RadioSetting("am_mode6", "850M AM Mode",
RadioSettingValueBoolean(
_settings.am_mode6))
vfo850_grp.append(rs)
# ###########################
rs = RadioSetting("vfomode_b", "Working Mode",
RadioSettingValueMap(wmmap,
_settings.vfomode_b))
vfob_grp.append(rs)
rs = RadioSetting("vfochan_b", "Active/Work Channel",
RadioSettingValueInteger(1, 999,
_chnum_decode(
_settings.vfochan_b)))
vfob_grp.append(rs)
rs = RadioSetting("vfofreq7", "Freq",
RadioSettingValueFloat(
0, 999.999999, (_freq_decode(
_settings.vfofreq7) /
1000000.0), 0.000001, 6))
vfob_grp.append(rs)
if self.MODEL != "KG-1000G Plus":
rs = RadioSetting("vfoofst7", "Offset",
RadioSettingValueFloat(
0, 999.999999, (_freq_decode(
_settings.vfoofst7) /
1000000.0), 0.000001, 6))
vfob_grp.append(rs)
rs = RadioSetting("rxtone7", "Rx tone",
RadioSettingValueMap(
TONE_MAP, _settings.rxtone7))
vfob_grp.append(rs)
rs = RadioSetting("txtone7", "Tx tone",
RadioSettingValueMap(
TONE_MAP, _settings.txtone7))
vfob_grp.append(rs)
rs = RadioSetting("power7", "Power",
RadioSettingValueMap(
POWER_MAP, _settings.power7))
vfob_grp.append(rs)
rs = RadioSetting("narrow7", narrow7label,
RadioSettingValueMap(
BANDWIDTH_MAP, _settings.narrow7))
vfob_grp.append(rs)
rs = RadioSetting("mute7", mute7label,
RadioSettingValueMap(
MUTE_MODE_MAP, _settings.mute7))
vfob_grp.append(rs)
if self.MODEL == "KG-1000G Plus":
rs = RadioSetting("shft_dir7", "Repeater",
RadioSettingValueBoolean(
_settings.shft_dir7))
else:
rs = RadioSetting("shft_dir7", "Shift Direction",
RadioSettingValueList(
DUPLEX_LIST,
current_index=_settings.shft_dir7))
vfob_grp.append(rs)
rs = RadioSetting("compander7", "Compander",
RadioSettingValueBoolean(
_settings.compander7))
vfob_grp.append(rs)
rs = RadioSetting("scrambler7", scram7label,
RadioSettingValueList(
SCRAMBLER_LIST,
current_index=_settings.scrambler7))
vfob_grp.append(rs)
rs = RadioSetting("vfosquelch_b", "Squelch",
RadioSettingValueInteger(0, 9,
_settings.vfosquelch_b))
vfob_grp.append(rs)
rs = RadioSetting("vfostep_b", "Step",
RadioSettingValueList(
STEP_LIST,
current_index=_settings.vfostep_b))
vfob_grp.append(rs)
# Scan Group Settings
def _decode(lst):
LOG.debug("lst %s", lst)
_str = ''.join([chr(c) for c in lst
if chr(c) in SCANNAME_CHARSET])
return _str
rs = RadioSetting("scan_a_act", "Scan A Active Group",
RadioSettingValueList(
SCAN_GROUP_LIST,
current_index=_settings.scan_a_act))
scan_grp.append(rs)
rs = RadioSetting("scan_b_act", "Scan B Active Group",
RadioSettingValueList(
SCAN_GROUP_LIST,
current_index=_settings.scan_b_act))
scan_grp.append(rs)
for i in range(1, 11):
x = str(i)
_str = _decode(getattr(_settings, 'scanname%s' % x))
LOG.debug("ScanName %s", i)
LOG.debug("is %s", _str)
# CPS treats PPPPPP as a blank name as it is the factory reset
# value"
# The Radio treats PPPPPPP as a blank name and can display 8
# chars"
# Force Chirp to blank out the scan name if value is PPPPPPP
# to match the radio"
# Blank out the name if first 6 are spaces or null and the
# 7th is a P to handle
# firmware peculiarities in handling all 8 characters.
if _str[0:7] == "PPPPPPP":
_str = ""
if self.MODEL != "KG-1000G Plus":
if _str[0:7] == "\x00\x00\x00\x00\x00\x00P":
_str = ""
elif _str[0:7] == "\x20\x20\x20\x20\x20\x20P":
_str = ""
else:
if _str[0:6] == "\x00\x00\x00\x00\x00\x00":
_str = ""
elif _str[0:6] == "\x20\x20\x20\x20\x20\x20":
_str = ""
elif (len(_str) == 2) & (_str[0:2] == "PP"):
_str = ""
rs = RadioSetting("scanname" + x, "Scan Name " + x,
RadioSettingValueString(0, 8, _str))
scanname_grp.append(rs)
val = getattr(_settings, 'scanlower%i' % i)
rs = RadioSetting("scanlower%i" % i, "Scan Lower %i" % i,
RadioSettingValueInteger(1, 999, val))
scan_grp.append(rs)
val = getattr(_settings, 'scanupper%i' % i)
rs = RadioSetting("scanupper%i" % i, "Scan Upper %i" % i,
RadioSettingValueInteger(1, 999, val))
scan_grp.append(rs)
# remote settings
rs = RadioSetting("rc_power", "RC Power",
RadioSettingValueList(
RC_POWER_LIST,
current_index=_settings.rc_power))
remote_grp.append(rs)
def decode_remote_vals(self, setting):
# parse the id value and replace all C with empty
# C indicates the end of the id value
code = ('%06X' % int(setting)).replace('C', '')
return int(code), code
def apply_remote_id(setting, obj, val):
val = str(val)
value = int(val.ljust(3, '0').ljust(6, 'C'), 16)
setattr(obj, setting.get_name(), value)
val_ani, code_val = decode_remote_vals(self, _settings.ani_edit)
LOG.debug("ani = %s" % val_ani)
val_ani = RadioSettingValueString(3, 6, code_val, False)
val_ani.set_charset(dtmfchars)
rs = RadioSetting("settings.ani_edit", "ANI Edit", val_ani)
def apply_ani_id(setting, obj):
LOG.debug("val= %s" % val_ani)
if str(val_ani)[0] == "0":
raise errors.RadioError("ANI EDIT must start with \
Non-Zero Digit")
val = str(val_ani)
value = int(val.ljust(3, '0').ljust(6, 'C'), 16)
obj.ani_edit = value
rs.set_apply_callback(apply_ani_id, _settings)
remote_grp.append(rs)
val_mcc, code_val = decode_remote_vals(self, _settings.mcc_edit)
LOG.debug("mcc = %s" % val_mcc)
val_mcc = RadioSettingValueString(3, 6, code_val, False)
val_mcc.set_charset(dtmfchars)
rs = RadioSetting("mcc_edit", "MCC Edit", val_mcc)
rs.set_apply_callback(apply_remote_id, _settings, val_mcc)
remote_grp.append(rs)
val_scc, code_val = decode_remote_vals(self, _settings.scc_edit)
LOG.debug("scc = %s" % val_scc)
val_scc = RadioSettingValueString(3, 6, code_val, False)
val_scc.set_charset(dtmfchars)
rs = RadioSetting("scc_edit", "SCC Edit", val_scc)
rs.set_apply_callback(apply_remote_id, _settings, val_scc)
remote_grp.append(rs)
val_ctrl, code_val = decode_remote_vals(self, _settings.ctrl_edit)
LOG.debug("ctrl = %s" % val_ctrl)
val_ctrl = RadioSettingValueString(3, 6, code_val, False)
val_ctrl.set_charset(dtmfchars)
rs = RadioSetting("ctrl_edit", "CTRL Edit", val_ctrl)
rs.set_apply_callback(apply_remote_id, _settings, val_ctrl)
remote_grp.append(rs)
# OEM Settings
if self.MODEL != "KG-1000G Plus":
_oem_name = _oem_str_decode(self._memobj.oem.display_name)
else:
displayname = self._memobj.oem.display_name
_oem_name = _oem_str_decode_1000GPLUS(displayname)
rs = RadioSetting("oem.display_name", "Area Message",
RadioSettingValueString(1, 8, _oem_name))
oem_grp.append(rs)
# FM RADIO PRESETS
# memory stores raw integer value like 7600
# radio will divide 7600 by 100 and interpret correctly at 76.0 MHz
for i in range(1, 21):
# chan = str(i)
fmname = "FM_radio%i" % i
fmlabel = "FM Preset %i" % i
fmvalue = getattr(_settings, fmname)
# some CPS versions store values with .01 MHz in error
# eg 99.5 MHz is stored as 0x26df = 9951 dec = 99.51 MHz
# even though the radio properly displays 99.5
# this will drop the 0.01 MHz for Chirp Displayed values
fmvalue = fmvalue // 10 / 10
rs = RadioSetting(fmname, fmlabel,
RadioSettingValueFloat(76.0, 108.0,
fmvalue,
0.1, 1))
fmradio_grp.append(rs)
return group
def get_settings(self):
try:
return self._get_settings()
except Exception:
LOG.exception("Failed to parse settings: %s")
return None
def set_settings(self, settings):
for element in settings:
if not isinstance(element, RadioSetting):
self.set_settings(element)
continue
else:
try:
if "." in element.get_name():
bits = element.get_name().split(".")
obj = self._memobj
for bit in bits[:-1]:
obj = getattr(obj, bit)
setting = bits[-1]
else:
obj = self._memobj.settings
setting = element.get_name()
if element.has_apply_callback():
LOG.debug("Using apply callback")
element.run_apply_callback()
else:
LOG.debug("Setting %s = %s" % (setting, element.value))
if self._is_freq(element):
# rescale freq values to match radio
# expected values
value = _freq_encode(
element[0].get_value()*1000000.0)
setattr(obj, setting, value)
elif self._is_fmradio_or_voltage(element):
# rescale FM Radio values to match radio
# expected values
setattr(obj, setting,
int(element.values()[0]._current * 100.0))
elif self._is_limit(element):
setattr(obj, setting,
int(element[0].get_value()) * 10)
# Special VFO A Settings
#
elif self._is_chan(element):
value = _chnum_encode(element[0].get_value())
setattr(obj, setting, value)
continue
elif self._is_display_name(element):
string = element[0].get_value()
if self.MODEL != "KG-1000G Plus":
nameenc = _oem_str_encode(string)
else:
nameenc = _oem_str_encode_1000GPLUS(string)
for i in range(0, 8):
LOG.debug("nameenc %s" % (nameenc[i]))
self._memobj.oem.display_name[i] = \
ord(nameenc[i])
elif self._is_scan_name(element):
string = element[0].get_value()
LOG.debug("string %s" % (string))
value = _str_encode(string)
LOG.debug("scaname %s" % (value))
setattr(obj, setting, value.encode())
else:
setattr(obj, setting, element.value)
except Exception:
LOG.debug(element.get_name())
raise
def _is_freq(self, element):
return ("rxfreq" in element.get_name() or
"txoffset" in element.get_name() or
"vfofreq" in element.get_name() or
"vfoofst" in element.get_name())
def _is_limit(self, element):
return "limit" in element.get_name()
def _is_fmradio_or_voltage(self, element):
return ("FM_radio" in element.get_name() or
"thr_vol_lvl" in element.get_name())
def _is_chan(self, element):
return ("vfochan" in element.get_name() or
"pri_ch" in element.get_name())
def _is_display_name(self, element):
return "display_name" in element.get_name()
def _is_scan_name(self, element):
return "scanname" in element.get_name()
@directory.register
class KG1000GRadio(KG980PRadio):
# """Wouxun KG-1000G"""
VENDOR = "Wouxun"
MODEL = "KG-1000G"
@directory.register
class KG1000GPlusRadio(KG980PRadio):
# """Wouxun KG-1000G Plus"""
VENDOR = "Wouxun"
MODEL = "KG-1000G Plus"
chirp-20261030/chirp/drivers/kguv9dplus.py 0000644 0001750 0001750 00000301560 15257672455 017333 0 ustar hibby hibby # Copyright 2022 Mel Terechenok
# Updated Driver to support Wouxun KG-UV9PX
# based on prior driver for KG-UV9D Plus by
# Jim Lieb
#
# Borrowed from other chirp drivers, especially the KG-UV8D Plus
# by Krystian Struzik
#
# 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 .
"""Wouxun KG-UV9D Plus radio management module"""
import time
import logging
import struct
from chirp import chirp_common, bitwise, memmap, errors, directory
from chirp.kenwood_tone import KenwoodToneModel
from chirp.settings import RadioSetting, RadioSettingGroup, \
RadioSettingValueBoolean, \
RadioSettingValueList, RadioSettingValueInteger, \
RadioSettingValueString, RadioSettings, \
InvalidValueError
LOG = logging.getLogger(__name__)
CMD_IDENT = 0x80
CMD_HANGUP = 0x81
CMD_RCONF = 0x82
CMD_WCONF = 0x83
CMD_RCHAN = 0x84
CMD_WCHAN = 0x85
cmd_name = {
CMD_IDENT: "ident",
CMD_HANGUP: "hangup",
CMD_RCONF: "read config",
CMD_WCONF: "write config",
CMD_RCHAN: "read channel memory", # Unused
CMD_WCHAN: "write channel memory" # Unused because it is a hack.
}
# This is used to write the configuration of the radio base on info
# gleaned from the downloaded app. There are empty spaces and we honor
# them because we don't know what they are (yet) although we read the
# whole of memory.
#
# Channel memory is separate. There are 1000 (1-999) channels.
# These are read/written to the radio in 4 channel (96 byte)
# records starting at address 0xa00 and ending at
# 0x4800 (presuming the end of channel 1000 is 0x4860-1
config_map = ( # map address, write size, write count
(0x40, 16, 1), # Passwords
(0x60, 20, 1), # RX frequency limits
(0x74, 8, 1), # TX frequency limits
(0x740, 40, 1), # FM chan 1-20
(0x780, 16, 1), # vfo-b-150
(0x790, 16, 1), # vfo-b-450
(0x800, 16, 1), # vfo-a-150
(0x810, 16, 1), # vfo-a-450
(0x820, 16, 1), # vfo-a-300
(0x830, 16, 1), # vfo-a-700
(0x840, 16, 1), # vfo-a-200
(0x860, 16, 1), # area-a-conf
(0x870, 16, 1), # area-b-conf
(0x880, 16, 1), # radio conf 0
(0x890, 16, 1), # radio conf 1
(0x8a0, 16, 1), # radio conf 2
(0x8b0, 16, 1), # radio conf 3
(0x8c0, 16, 1), # PTT-ANI
(0x8d0, 16, 1), # SCC
(0x8e0, 16, 1), # power save
(0x8f0, 16, 1), # Display banner
(0x940, 64, 2), # Scan groups and names
(0xa00, 64, 249), # Memory Channels 1-996
(0x4840, 48, 1), # Memory Channels 997-999
(0x4900, 32, 249), # Memory Names 1-996
(0x6820, 24, 1), # Memory Names 997-999
(0x7000, 8, 124), # Valid Channel bytes 1-992
(0x73E0, 1, 7), # Valid Channel Bytes 993-999
(0x7400, 64, 5), # CALL-ID 1-20, names 1-20
)
config_map2 = ( # map address, write size, write count
(0x40, 16, 1), # Passwords
(0x50, 10, 1), # OEM Display Name
(0x60, 20, 1), # Rx Freq Limits Area A
(0x74, 8, 1), # TX Frequency Limits 150M and 450M
(0x7c, 4, 1), # Rx 150M Freq Limits Area B
# (0x80, 8, 1), # unknown Freq limits
(0x740, 40, 1), # FM chan 1-20
(0x780, 16, 1), # vfo-b-150
(0x790, 16, 1), # vfo-b-450
(0x800, 16, 1), # vfo-a-150
(0x810, 16, 1), # vfo-a-450
(0x820, 16, 1), # vfo-a-300
(0x830, 16, 1), # vfo-a-700
(0x840, 16, 1), # vfo-a-200
(0x860, 16, 1), # area-a-conf
(0x870, 16, 1), # area-b-conf
(0x880, 16, 1), # radio conf 0
(0x890, 16, 1), # radio conf 1
(0x8a0, 16, 1), # radio conf 2
(0x8b0, 16, 1), # radio conf 3
(0x8c0, 16, 1), # PTT-ANI
(0x8d0, 16, 1), # SCC
(0x8e0, 16, 1), # power save
(0x8f0, 16, 1), # Display banner
(0x940, 64, 2), # Scan groups and names
(0xa00, 64, 249), # Memory Channels 1-996
(0x4840, 48, 1), # Memory Channels 997-999
(0x4900, 32, 249), # Memory Names 1-996
(0x6820, 24, 1), # Memory Names 997-999
(0x7000, 8, 124), # Valid Channel bytes 1-992
(0x73E0, 1, 7), # Valid Channel Bytes 993-999
(0x7400, 64, 5), # CALL-ID 1-20, names 1-20
(0x7600, 1, 1) # Screen Mode
)
MEM_VALID = 0xfc
MEM_INVALID = 0xff
VALID_MEM_VALUES = [MEM_VALID, 0x00, 0x02, 0x40, 0x3D]
INVALID_MEM_VALUES = [MEM_INVALID]
# new CHAN_VALID/INVALID mem values to address some radios not showing
# new channels
CHAN_VALID = 0x41
CHAN_INVALID = 0xFC
# Radio memory map. This matches the reads/writes above.
# structure elements whose name starts with x are currently unidentified
_MEM_FORMAT02 = """
#seekto 0x40;
struct {
char reset[6];
char x46[2];
char mode_sw[6];
char x4e;
} passwords;
#seekto 0x60;
struct freq_limit {
u16 start;
u16 stop;
};
struct {
struct freq_limit band_150;
struct freq_limit band_450;
struct freq_limit band_300;
struct freq_limit band_700;
struct freq_limit band_200;
} rx_freq_limits;
struct {
struct freq_limit band_150;
struct freq_limit band_450;
} tx_freq_limits;
#seekto 0x740;
struct {
u16 fm_freq;
} fm_chans[20];
// each band has its own configuration, essentially its default params
struct vfo {
u32 freq;
u32 offset;
u16 encqt;
u16 decqt;
u8 bit7_4:3,
qt:3,
bit1_0:2;
u8 bit7:1,
scan:1,
bit5:1,
pwr:2,
mod:1,
fm_dev:2;
u8 pad2:6,
shift:2;
u8 zeros;
};
#seekto 0x780;
struct {
struct vfo band_150;
struct vfo band_450;
} vfo_b;
#seekto 0x800;
struct {
struct vfo band_150;
struct vfo band_450;
struct vfo band_300;
struct vfo band_700;
struct vfo band_200;
} vfo_a;
// There are two independent radios, aka areas (as described
// in the manual as the upper and lower portions of the display...
struct area_conf {
u8 w_mode;
u16 w_chan;
u8 scan_grp;
u8 bcl;
u8 sql;
u8 cset;
u8 step;
u8 scan_mode;
u8 x869;
u8 scan_range;
u8 x86b;
u8 x86c;
u8 x86d;
u8 x86e;
u8 x86f;
};
#seekto 0x860;
struct area_conf a_conf;
#seekto 0x870;
struct area_conf b_conf;
#seekto 0x880;
struct {
u8 menu_avail;
u8 reset_avail;
u8 x882;
u8 x883;
u8 lang;
u8 x885;
u8 beep;
u8 auto_am;
u8 qt_sw;
u8 lock;
u8 x88a;
u8 pf1;
u8 pf2;
u8 pf3;
u8 s_mute;
u8 type_set;
u8 tot;
u8 toa;
u8 ptt_id;
u8 x893;
u8 id_dly;
u8 x895;
u8 voice_sw;
u8 s_tone;
u8 abr_lvl;
u8 ring_time;
u8 roger;
u8 x89b;
u8 abr;
u8 save_m;
u8 lock_m;
u8 auto_lk;
u8 rpt_ptt;
u8 rpt_spk;
u8 rpt_rct;
u8 prich_sw;
u16 pri_ch;
u8 x8a6;
u8 x8a7;
u8 dtmf_st;
u8 dtmf_tx;
u8 x8aa;
u8 sc_qt;
u8 apo_tmr;
u8 vox_grd;
u8 vox_dly;
u8 rpt_kpt;
struct {
u16 scan_st;
u16 scan_end;
} a;
struct {
u16 scan_st;
u16 scan_end;
} b;
u8 x8b8;
u8 x8b9;
u8 x8ba;
u8 ponmsg;
u8 blcdsw;
u8 bledsw;
u8 x8be;
u8 x8bf;
} settings;
#seekto 0x8c0;
struct {
u8 code[6];
char x8c6[10];
} my_callid;
#seekto 0x8d0;
struct {
u8 scc[6];
char x8d6[10];
} stun;
#seekto 0x8e0;
struct {
u16 wake;
u16 sleep;
} save[4];
#seekto 0x8f0;
struct {
char banner[16];
} display;
#seekto 0x940;
struct {
struct {
i16 scan_st;
i16 scan_end;
} addrs[10];
u8 x0968[8];
struct {
char name[8];
} names[10];
} scn_grps;
// this array of structs is marshalled via the R/WCHAN commands
#seekto 0xa00;
struct {
u32 rxfreq;
u32 txfreq;
u16 txtone;
u16 rxtone;
u8 bit7_5:3, // all ones
qt:3,
bit1_0:2;
u8 bit7:1,
scan:1,
bit5:1,
pwr:2,
mod:1,
fm_dev:2;
u8 state;
u8 c3;
} chan_blk[999];
// nobody really sees this. It is marshalled with chan_blk
// in 4 entry chunks
#seekto 0x4900;
// Tracks with the index of chan_blk[]
struct {
char name[8];
} chan_name[999];
#seekto 0x7000;
struct {
u8 ch_valid;
} chan_valid[999];
#seekto 0x7400;
struct {
u8 cid[6];
u8 pad[2];
}call_ids[20];
// This array tracks with the index of call_ids[]
struct {
char name[6];
char pad[2];
} cid_names[20];
"""
_MEM_FORMAT_9PX = """
#seekto 0x40;
struct {
char reset[6];
char x46[2];
char mode_sw[6];
char x4e;
} passwords;
#seekto 0x50;
struct {
char model[10];
} oemmodel;
#seekto 0x60;
struct {
u16 lim_150M_area_a_rxlower_limit; // 0x60
u16 lim_150M_area_a_rxupper_limit;
u16 lim_450M_rxlower_limit;
u16 lim_450M_rxupper_limit;
u16 lim_300M_rxlower_limit;
u16 lim_300M_rxupper_limit;
u16 lim_800M_rxlower_limit;
u16 lim_800M_rxupper_limit;
u16 lim_210M_rxlower_limit;
u16 lim_210M_rxupper_limit;
u16 lim_150M_Txlower_limit;
u16 lim_150M_Txupper_limit;
u16 lim_450M_Txlower_limit;
u16 lim_450M_Txupper_limit;
u16 lim_150M_area_b_rxlower_limit;
u16 lim_150M_area_b_rxupper_limit;
u16 unknown_lower_limit;
u16 unknown_upper_limit;
u16 unknown2_lower_limit;
u16 unknown2_upper_limit;
} limits;
#seekto 0x740;
struct {
u16 fm_freq;
} fm_chans[20];
// each band has its own configuration, essentially its default params
struct vfo {
u32 freq;
u32 offset;
u16 encqt;
u16 decqt;
u8 bit7_4:3,
qt:3,
bit1_0:2;
u8 bit7:1,
scan:1,
bit5:1,
pwr:2,
mod:1,
fm_dev:2;
u8 pad2:6,
shift:2;
u8 zeros;
};
#seekto 0x780;
struct {
struct vfo band_150;
struct vfo band_450;
} vfo_b;
#seekto 0x800;
struct {
struct vfo band_150;
struct vfo band_450;
struct vfo band_300;
struct vfo band_700;
struct vfo band_200;
} vfo_a;
// There are two independent radios, aka areas (as described
// in the manual as the upper and lower portions of the display...
struct area_conf {
u8 w_mode;
u16 w_chan; // fix issue in 9D Plus - w_chan is 2bytes
u8 scan_grp;
u8 bcl;
u8 sql;
u8 cset;
u8 step;
u8 scan_mode;
u8 x869;
u8 scan_range;
u8 x86b;
u8 x86c;
u8 x86d;
u8 x86e;
u8 x86f;
};
#seekto 0x860;
struct area_conf a_conf;
#seekto 0x870;
struct area_conf b_conf;
#seekto 0x880;
struct {
u8 menu_avail;
u8 reset_avail;
u8 act_area;
u8 tdr;
u8 lang;
u8 x885;
u8 beep;
u8 auto_am;
u8 qt_sw;
u8 lock;
u8 x88a;
u8 pf1;
u8 pf2;
u8 pf3;
u8 s_mute;
u8 type_set;
u8 tot;
u8 toa;
u8 ptt_id;
u8 x893;
u8 id_dly;
u8 x895;
u8 voice_sw;
u8 s_tone;
u8 abr_lvl;
u8 ring_time;
u8 roger;
u8 x89b;
u8 abr;
u8 save_m;
u8 lock_m;
u8 auto_lk;
u8 rpt_ptt;
u8 rpt_spk;
u8 rpt_rct;
u8 prich_sw;
u16 pri_ch;
u8 x8a6;
u8 x8a7;
u8 dtmf_st;
u8 dtmf_tx;
u8 x8aa;
u8 sc_qt;
u8 apo_tmr;
u8 vox_grd;
u8 vox_dly;
u8 rpt_kpt;
struct {
u16 scan_st;
u16 scan_end;
} a;
struct {
u16 scan_st;
u16 scan_end;
} b;
u8 x8b8;
u8 x8b9;
u8 x8ba;
u8 ponmsg;
u8 blcdsw;
u8 bledsw;
u8 x8be;
u8 x8bf;
} settings;
#seekto 0x8c0;
struct {
u8 code[6];
char x8c6[10];
} my_callid;
#seekto 0x8d0;
struct {
u8 scc[6];
char x8d6[10];
} stun;
#seekto 0x8e0;
struct {
u16 wake;
u16 sleep;
} save[4];
#seekto 0x8f0;
struct {
char banner[16];
} display;
#seekto 0x940;
struct {
struct {
i16 scan_st;
i16 scan_end;
} addrs[10];
u8 x0968[8];
struct {
char name[8];
} names[10];
} scn_grps;
// this array of structs is marshalled via the R/WCHAN commands
#seekto 0xa00;
struct {
u32 rxfreq;
u32 txfreq;
u16 txtone;
u16 rxtone;
u8 bit7_5:3, // all ones
qt:3,
bit1_0:2;
u8 bit7:1,
scan:1,
bit5:1,
pwr:2,
mod:1,
fm_dev:2;
u8 state;
u8 c3;
} chan_blk[999];
// nobody really sees this. It is marshalled with chan_blk
// in 4 entry chunks
#seekto 0x4900;
// Tracks with the index of chan_blk[]
struct {
char name[8];
} chan_name[999];
#seekto 0x7400;
struct {
u8 cid[6];
u8 pad[2];
}call_ids[20];
// This array tracks with the index of call_ids[]
struct {
char name[6];
char pad[2];
} cid_names[20];
#seekto 0x7000;
struct {
u8 ch_valid;
} chan_valid[999];
#seekto 0x7600;
struct {
u8 screen_mode;
} screen;
"""
# Support for the Wouxun KG-UV9D Plus and KG-UV9PX radio
# Serial coms are at 19200 baud
# The data is passed in variable length records
# Record structure:
# Offset Usage
# 0 start of record (\x7d)
# 1 Command (6 commands, see above)
# 2 direction (\xff PC-> Radio, \x00 Radio -> PC)
# 3 length of payload (excluding header/checksum) (n)
# 4 payload (n bytes)
# 4+n+1 checksum - byte sum (% 256) of bytes 1 -> 4+n
#
# Memory Read Records:
# the payload is 3 bytes, first 2 are offset (big endian),
# 3rd is number of bytes to read
# Memory Write Records:
# the maximum payload size (from the Wouxun software)
# seems to be 66 bytes (2 bytes location + 64 bytes data).
def _pkt_encode(op, payload):
"""Assemble a packet for the radio and encode it for transmission.
Yes indeed, the checksum we store is only 4 bits. Why?
I suspect it's a bug in the radio firmware guys didn't want to fix,
i.e. a typo 0xff -> 0xf..."""
data = bytearray()
data.append(0x7d) # tag that marks the beginning of the packet
data.append(op)
data.append(0xff) # 0xff is from app to radio
# calc checksum from op to end
cksum = op + 0xff
if (payload):
data.append(len(payload))
cksum += len(payload)
for byte in payload:
cksum += byte
data.append(byte)
else:
data.append(0x00)
# Yea, this is a 4 bit cksum (also known as a bug)
data.append(cksum & 0xf)
# now obfuscate by an xor starting with first payload byte ^ 0x52
# including the trailing cksum.
xorbits = 0x52
for i, byte in enumerate(data[4:]):
xord = xorbits ^ byte
data[i + 4] = xord
xorbits = xord
return (data)
def _pkt_decode(data):
"""Take a packet hot off the wire and decode it into clear text
and return the fields. We say <> here because all it
turns out to be is annoying obfuscation.
This is the inverse of pkt_decode"""
# we don't care about data[0].
# It is always 0x7d and not included in checksum
op = data[1]
direction = data[2]
bytecount = data[3]
# First un-obfuscate the payload and cksum
payload = bytearray()
xorbits = 0x52
for i, byte in enumerate(data[4:]):
payload.append(xorbits ^ byte)
xorbits = byte
# Calculate the checksum starting with the 3 bytes of the header
cksum = op + direction + bytecount
for byte in payload[:-1]:
cksum += byte
# yes, a 4 bit cksum to match the encode
cksum_match = (cksum & 0xf) == payload[-1]
if (not cksum_match):
LOG.debug(
"Checksum mismatch: %x != %x; " % (cksum, payload[-1]))
return (cksum_match, op, payload[:-1])
# UI callbacks to process input for mapping UI fields to memory cells
def freq2int(val, min, max):
"""Convert a frequency as a string to a u32. Units is Hz
"""
_freq = chirp_common.parse_freq(str(val))
if _freq > max or _freq < min:
raise InvalidValueError("Frequency %s is not with in %s-%s" %
(chirp_common.format_freq(_freq),
chirp_common.format_freq(min),
chirp_common.format_freq(max)))
return _freq
def int2freq(freq):
"""
Convert a u32 frequency to a string for UI data entry/display
This is stored in the radio as units of 10 Hz which we compensate to Hz.
A value of -1 indicates , i.e. unused channel.
"""
if (int(freq) > 0):
f = chirp_common.format_freq(freq)
return f
else:
return ""
def freq2short(val, min, max):
"""Convert a frequency as a string to a u16 which is units of 10 kHz
"""
_freq = chirp_common.parse_freq(str(val))
if _freq > max or _freq < min:
raise InvalidValueError("Frequency %s is not with in %s-%s" %
(chirp_common.format_freq(_freq),
chirp_common.format_freq(min),
chirp_common.format_freq(max)))
return _freq // 100000 & 0xFFFF
def short2freq(freq):
"""
Convert a short frequency to a string for UI data entry/display
This is stored in the radio as units of 10 kHz which we
compensate to Hz.
A value of -1 indicates , i.e. unused channel.
"""
if (int(freq) > 0):
f = chirp_common.format_freq(freq * 100000)
return f
else:
return ""
def tone2short(t):
"""Convert a string tone or DCS to an encoded u16
"""
tone = str(t)
if tone == "----":
u16tone = 0x0000
elif tone[0] == 'D': # This is a DCS code
c = tone[1: -1]
code = int(c, 8)
if tone[-1] == 'I':
code |= 0x4000
u16tone = code | 0x8000
else: # This is an analog CTCSS
u16tone = int(tone[0:-2]+tone[-1]) & 0xffff # strip the '.'
return u16tone
def short2tone(tone):
""" Map a binary CTCSS/DCS to a string name for the tone
"""
if tone == 0 or tone == 0xffff:
ret = "----"
else:
code = tone & 0x3fff
if tone & 0x8000: # This is a DCS
if tone & 0x4000: # This is an inverse code
ret = "D%0.3oI" % code
else:
ret = "D%0.3oN" % code
else: # Just plain old analog CTCSS
ret = "%4.1f" % (code / 10.0)
return ret
def str2callid(val):
""" Convert caller id strings from callid2str.
"""
ascii2bin = "0123456789"
s = str(val).strip()
if len(s) < 3 or len(s) > 6:
raise InvalidValueError(
"Caller ID must be at least 3 and no more than 6 digits")
if s[0] == '0':
raise InvalidValueError(
"First digit of a Caller ID cannot be a zero '0'")
blk = bytearray()
for c in s:
if c not in ascii2bin:
raise InvalidValueError(
"Caller ID must be all digits 0x%x" % c)
b = (0xa, 0x1, 0x2, 0x3, 0x4, 0x5, 0x6, 0x7, 0x8, 0x9)[int(c)]
blk.append(b)
if len(blk) < 6:
blk.append(0xc) # EOL a short ID
if len(blk) < 6:
for i in range(0, (6 - len(blk))):
blk.append(0xf0)
return blk
def digits2str(digits, padding=' ', width=6):
"""Convert a password or SCC digit string to a string
Passwords are expanded to and must be 6 chars. Fill them with '0'
"""
bin2ascii = "0123456789"
digitsstr = ""
for i in range(0, 6):
b = digits[i].get_value()
if b == 0xc: # the digits EOL
break
if b >= 0xa:
raise InvalidValueError(
"Value has illegal byte 0x%x" % ord(b))
digitsstr += bin2ascii[b]
digitsstr = digitsstr.ljust(width, padding)
return digitsstr
def str2digits(val):
""" Callback for edited strings from digits2str.
"""
ascii2bin = " 0123456789"
s = str(val).strip()
if len(s) < 3 or len(s) > 6:
raise InvalidValueError(
"Value must be at least 3 and no more than 6 digits")
blk = bytearray()
for c in s:
if c not in ascii2bin:
raise InvalidValueError("Value must be all digits 0x%x" % c)
blk.append(int(c))
for i in range(len(blk), 6):
blk.append(0xc) # EOL a short ID
return blk
def name2str(name):
""" Convert a callid or scan group name to a string
Deal with fixed field padding (\0 or \0xff)
"""
namestr = ""
for i in range(0, len(name)):
b = ord(name[i].get_value())
if b != 0 and b != 0xff:
namestr += chr(b)
return namestr
def str2name(val, size=6, fillchar='\0', emptyfill='\0'):
""" Convert a string to a name. A name is a 6 element bytearray
with ascii chars.
"""
val = str(val).rstrip(' \t\r\n\0\xff')
if len(val) == 0:
name = "".ljust(size, emptyfill)
else:
name = val.ljust(size, fillchar)
return name
def pw2str(pw):
"""Convert a password string (6 digits) to a string
Passwords must be 6 digits. If it is shorter, pad right with '0'
"""
pwstr = ""
ascii2bin = "0123456789"
for i in range(0, len(pw)):
b = pw[i].get_value()
if b not in ascii2bin:
raise InvalidValueError("Value must be digits 0-9")
pwstr += b
pwstr = pwstr.ljust(6, '0')
return pwstr
def str2pw(val):
"""Store a password from UI to memory obj
If we clear the password (make it go away), change the
empty string to '000000' since the radio must have *something*
Also, fill a < 6 digit pw with 0's
"""
ascii2bin = "0123456789"
val = str(val).rstrip(' \t\r\n\0\0xff')
if len(val) == 0: # a null password
val = "000000"
for i in range(0, len(val)):
b = val[i]
if b not in ascii2bin:
raise InvalidValueError("Value must be digits 0-9")
if len(val) == 0:
pw = "".ljust(6, '\0')
else:
pw = val.ljust(6, '0')
return pw
# Helpers to replace python2 things like confused str/byte
def _hex_print(data, addrfmt=None):
"""Return a hexdump-like encoding of @data
We expect data to be a bytearray, not a string.
Expanded from borrowed code to use the first 2 bytes as the address
per comm packet format.
"""
if addrfmt is None:
addrfmt = '%(addr)03i'
addr = 0
else: # assume first 2 bytes are address
a = struct.unpack(">H", data[0:2])
addr = a[0]
data = data[2:]
block_size = 16
lines = (len(data) // block_size)
if (len(data) % block_size > 0):
lines += 1
out = ""
left = len(data)
for block in range(0, lines):
addr += block * block_size
try:
out += addrfmt % locals()
except (OverflowError, ValueError, TypeError, KeyError):
out += "%03i" % addr
out += ': '
if left < block_size:
limit = left
else:
limit = block_size
for j in range(0, block_size):
if (j < limit):
out += "%02x " % data[(block * block_size) + j]
else:
out += " "
out += " "
for j in range(0, block_size):
if (j < limit):
_byte = data[(block * block_size) + j]
if _byte >= 0x20 and _byte < 0x7F:
out += "%s" % chr(_byte)
else:
out += "."
else:
out += " "
out += "\n"
if (left > block_size):
left -= block_size
return out
# Useful UI lists
STEPS = [2.5, 5.0, 6.25, 10.0, 12.5, 25.0, 50.0, 100.0]
STEPS_9K = [2.5, 5.0, 6.25, 8.33, 10.0, 12.5, 25.0, 50.0, 100.0]
S_TONES = [str(x) for x in [1000, 1450, 1750, 2100]]
STEP_LIST = [str(x)+"kHz" for x in STEPS]
STEP_LIST_9K = [str(x)+"kHz" for x in STEPS_9K]
ROGER_LIST = ["Off", "Begin", "End", "Both"]
TIMEOUT_LIST = [str(x) + "s" for x in range(15, 601, 15)]
TOA_LIST = ["Off"] + ["%ds" % t for t in range(1, 11)]
BANDWIDTH_LIST = ["Wide", "Narrow"]
LANGUAGE_LIST = ["English", "Chinese"]
LANGUAGE_LIST2 = ["English", "Chinese-DISABLED"]
PF1KEY_LIST = ["OFF", "call id", "r-alarm", "SOS", "SF-TX"]
PF1KEY_LIST9GX = ["OFF", "call id", "r-alarm", "SOS", "SF-TX", "Scan",
"Second", "Lamp"]
PF2KEY_LIST = ["OFF", "Scan", "Second", "Lamp", "SDF-DIR", "K-lamp"]
PF2KEY_LIST9GX = ["OFF", "Scan", "Second", "Lamp", "K-lamp"]
PF3KEY_LIST2 = ["OFF", "Call ID", "R-ALARM", "SOS", "SF-TX", "Scan",
"Second", "Lamp"]
PF3KEY_LIST9GX = ["OFF", "call id", "r-alarm", "SOS", "SF-TX", "Scan",
"Second", "Lamp"]
PF3KEY_LIST = ["OFF", "Call ID", "R-ALARM", "SOS", "SF-TX"]
WORKMODE_LIST = ["VFO freq", "Channel No.", "Ch. No.+Freq.",
"Ch. No.+Name"]
BACKLIGHT_LIST = ["Off"] + ["%sS" % t for t in range(1, 31)] + \
["Always On"]
BACKLIGHT_BRIGHT_MIN = 1
BACKLIGHT_BRIGHT_MAX = 5
SAVE_MODES = ["Off", "1", "2", "3", "4"]
LOCK_MODES = ["key-lk", "key+pg", "key+ptt", "all"]
APO_TIMES = ["Off"] + ["%dm" % t for t in range(15, 151, 15)]
OFFSET_LIST = ["none", "+", "-"]
PONMSG_LIST = ["Battery Volts", "Bitmap"]
PONMSG_LIST2 = ["Battery Volts", "Bitmap-DISABLED"]
SPMUTE_LIST = ["QT", "QT*T", "QT&T"]
DTMFST_LIST = ["Off", "DT-ST", "ANI-ST", "DT-ANI"]
DTMF_TIMES = ["%d" % x for x in range(80, 501, 20)]
PTTID_LIST = ["Off", "Begin", "End", "Both"]
ID_DLY_LIST = ["%dms" % t for t in range(100, 3001, 100)]
VOX_GRDS = ["Off"] + ["%dlevel" % l for l in range(1, 11)]
VOX_DLYS = ["Off"] + ["%ds" % t for t in range(1, 5)]
RPT_KPTS = ["Off"] + ["%dms" % t for t in range(100, 5001, 100)]
ABR_LVL_MAP = [("1", 1), ("2", 2), ("3", 3), ("4", 4), ("5", 5)]
LIST_1_5 = ["%s" % x for x in range(1, 6)]
LIST_0_9 = ["%s" % x for x in range(0, 10)]
LIST_1_20 = ["%s" % x for x in range(1, 21)]
LIST_OFF_10 = ["Off"] + ["%s" % x for x in range(1, 11)]
SCANGRP_LIST = ["All"] + ["%s" % x for x in range(1, 11)]
SCANMODE_LIST = ["TO", "CO", "SE"]
SCANRANGE_LIST = ["Current band", "freq range", "ALL"]
SCQT_LIST = ["Decoder", "Encoder", "Both"]
S_MUTE_LIST = ["off", "rx mute", "tx mute", "r/t mute"]
POWER_LIST = ["Low", "Med", "High"]
RPTMODE_LIST = ["Radio/Talkie", "One direction Repeater",
"Two direction repeater"]
TONE_LIST = ["----"] + ["%s" % str(t) for t in chirp_common.TONES] + \
["D%0.3dN" % dts for dts in chirp_common.DTCS_CODES] + \
["D%0.3dI" % dts for dts in chirp_common.DTCS_CODES]
SCREEN_MODE_LIST = ["Classic", "Covert", "Day_1", "Day_2"]
ACTIVE_AREA_LIST = ["Receiver A - Top", "Receiver B - Bottom"]
TDR_LIST = ["TDR ON", "TDR OFF"]
@directory.register
class KGUV9DPlusRadio(chirp_common.CloneModeRadio,
chirp_common.ExperimentalRadio):
"""Wouxun KG-UV9D Plus"""
VENDOR = "Wouxun"
MODEL = "KG-UV9D Plus"
_model = b"KG-UV9D"
_rev = b"00" # default rev for the radio I know about...
_file_ident = b"kg-uv9d"
BAUD_RATE = 19200
@classmethod
def match_model(cls, filedata, filename):
return cls._file_ident in filedata[0x51:0x59].lower()
POWER_LEVELS = [chirp_common.PowerLevel("L", watts=1),
chirp_common.PowerLevel("M", watts=2),
chirp_common.PowerLevel("H", watts=5)]
_step_list = STEP_LIST
_valid_steps = STEPS
_mmap = ""
_tone_model = KenwoodToneModel(
dcs_base=0x8000, pol_mask=0x4000, tone_init=0x0000,
tone_flag=0x0000, dcs_enc_base=8)
def _read_record(self):
""" Read and validate the header of a radio reply.
A record is a formatted byte stream as follows:
0x7D All records start with this
opcode This is in the set of legal commands.
The radio reply matches the request
dir This is the direction, 0xFF to the radio,
0x00 from the radio.
cnt Count of bytes in payload
(not including the trailing checksum byte)
"""
# first get the header and validate it
data = bytearray(self.pipe.read(4))
if not data:
raise errors.RadioNoResponse()
if (len(data) < 4):
raise errors.RadioError('Radio sent short header')
if (data[0] != 0x7D):
raise errors.RadioError(
'Radio reply garbled (%02x)' % data[0])
if (data[1] not in cmd_name):
raise errors.RadioError(
"Unrecognized opcode (%02x)" % data[1])
if (data[2] != 0x00):
raise errors.RadioError(
"Direction incorrect. Got (%02x)" % data[2])
payload_len = data[3]
# don't forget to read the checksum byte
data.extend(self.pipe.read(payload_len + 1))
if (len(data) != (payload_len + 5)): # we got a short read
raise errors.RadioError(
"Radio reply wrong size. Wanted %d, got %d" %
((payload_len + 1), (len(data) - 4)))
return _pkt_decode(data)
def _write_record(self, cmd, payload=None):
""" Write a request packet to the radio.
"""
packet = _pkt_encode(cmd, payload)
self.pipe.write(packet)
def _identify(self):
""" Identify the radio
The ident block identifies the radio and its capabilities.
This block is always 78 bytes. The rev == '01' is the base
radio and '02' seems to be the '-Plus' version.
I don't really trust the content after the model and revision.
One would assume this is pretty much constant data but I have
seen differences between my radio and the dump named
KG-UV9D-Plus-OutOfBox-Read.txt from bug #3509. The first
five bands match the OEM windows
app except the 350-400 band. The OOB trace has the 700 MHz
band different. This is speculation at this point.
TODO: This could be smarter and reject a radio not actually
a UV9D...
"""
for _i in range(0, 10): # retry 10 times if we get junk
self._write_record(CMD_IDENT)
chksum_match, op, _resp = self._read_record()
if len(_resp) == 0:
raise errors.RadioNoResponse()
if len(_resp) != 74:
LOG.error(
"Expected and IDENT reply of 78 bytes. Got (%d)" %
len(_resp))
continue
if not chksum_match:
LOG.error("Checksum error: retrying ident...")
time.sleep(0.100)
continue
if op != CMD_IDENT:
LOG.error("Expected IDENT reply. Got (%02x)" % op)
continue
LOG.debug("Got:\n%s" % _hex_print(_resp))
(mod, rev) = struct.unpack(">7s2s", _resp[0:9])
LOG.debug("Model %s, rev %s" % (mod, rev))
if mod == self._model:
self._rev = rev
return
else:
raise Exception("Unable to identify radio")
raise Exception("All retries to identify failed")
def process_mmap(self):
if self._rev != b"02" and self._rev != b"00":
# new revision found - log it and assume same map and proceed
LOG.debug("Unrecognized model variation (%s) Using default Map" %
self._rev)
self._memobj = bitwise.parse(_MEM_FORMAT02, self._mmap)
def sync_in(self):
""" Public sync_in
Download contents of the radio. Throw errors back
to the core if the radio does not respond.
"""
try:
self._identify()
self._mmap = self._do_download()
self._write_record(CMD_HANGUP)
except errors.RadioError:
raise
except Exception as e:
LOG.exception('Unknown error during download process')
raise errors.RadioError(
"Failed to communicate with radio: %s" % e)
self.process_mmap()
def sync_out(self):
""" Public sync_out
Upload the modified memory image into the radio.
"""
try:
self._identify()
self._do_upload()
self._write_record(CMD_HANGUP)
except errors.RadioError:
raise
except Exception as e:
raise errors.RadioError(
"Failed to communicate with radio: %s" % e)
return
def _do_download(self):
""" Read the whole of radio memory in 64 byte chunks.
We load the config space followed by loading memory channels.
The radio seems to be a "clone" type and the memory channels
are actually within the config space. There are separate
commands (CMD_RCHAN, CMD_WCHAN) for reading channel memory but
these seem to be a hack that can only do 4 channels at a time.
Since the radio only supports 999, (can only support 3 chars
in the display UI?) although the vendors app reads 1000
channels, it hacks back to config writes (CMD_WCONF) for the
last 3 channels and names. We keep it simple and just read
the whole thing even though the vendor app doesn't. Channels
are separate in their app simply because the radio protocol
has read/write commands to access it. What they do is simply
marshal the frequency+mode bits in 4 channel chunks followed
by a separate chunk of for names. In config space, they are two
separate arrays 1..999. Given that this space is not a
multiple of 4, there is hackery on upload to do the writes to
config space. See upload for this.
"""
mem = bytearray(0x8000) # The radio's memory map is 32k
for addr in range(0, 0x8000, 64):
req = bytearray(struct.pack(">HB", addr, 64))
self._write_record(CMD_RCONF, req)
chksum_match, op, resp = self._read_record()
if not chksum_match:
LOG.debug(_hex_print(resp))
raise Exception(
"Checksum error while reading configuration (0x%x)" %
addr)
pa = struct.unpack(">H", resp[0:2])
pkt_addr = pa[0]
payload = resp[2:]
if op != CMD_RCONF or addr != pkt_addr:
raise Exception(
"Expected CMD_RCONF (%x) reply. Got (%02x: %x)" %
(addr, op, pkt_addr))
LOG.debug("Config read (0x%x):\n%s" %
(addr, _hex_print(resp, '0x%(addr)04x')))
# Orig Code from 9D Plus driver was len(Payload)-1:
# This Caused every 64th byte to = 00
for i in range(0, len(payload)):
mem[addr + i] = payload[i]
if self.status_fn:
status = chirp_common.Status()
status.cur = addr
status.max = 0x8000
status.msg = "Cloning from radio"
self.status_fn(status)
return memmap.MemoryMapBytes(bytes(mem))
def _do_upload(self):
"""Walk through the config map and write updated records to
the radio. The config map contains only the regions we know
about. We don't use the channel memory commands to avoid the
hackery of using config write commands to fill in the last
3 channel memory and names slots. As we discover other useful
goodies in the map, we can add more slots...
"""
if (self.MODEL == "KG-UV9PX" or self.MODEL == "KG-UV9GX"):
cfgmap = config_map2
else:
cfgmap = config_map
for start, blocksize, count in cfgmap:
end = start + (blocksize * count)
for addr in range(start, end, blocksize):
req = bytearray(struct.pack(">H", addr))
req.extend(self.get_mmap()[addr:addr + blocksize])
self._write_record(CMD_WCONF, req)
LOG.debug("Config write (0x%x):\n%s" %
(addr, _hex_print(req)))
chksum_match, op, ack = self._read_record()
LOG.debug("Config write ack [%x]\n%s" %
(addr, _hex_print(ack)))
a = struct.unpack(">H", ack) # big endian short...
ack = a[0]
if not chksum_match or op != CMD_WCONF or addr != ack:
msg = ""
if not chksum_match:
msg += "Checksum err, "
if op != CMD_WCONF:
msg += "cmd mismatch %x != %x, " % \
(op, CMD_WCONF)
if addr != ack:
msg += "ack error %x != %x, " % (addr, ack)
raise Exception("Radio did not ack block: %s error" % msg)
if self.status_fn:
status = chirp_common.Status()
status.cur = addr
status.max = 0x8000
status.msg = "Cloning to radio"
self.status_fn(status)
def get_features(self):
""" Public get_features
Return the features of this radio once we have identified
it and gotten its bits
"""
rf = chirp_common.RadioFeatures()
rf.has_settings = True
rf.has_ctone = True
rf.has_rx_dtcs = True
rf.has_cross = True
rf.has_tuning_step = False
rf.has_bank = False
rf.can_odd_split = True
rf.valid_skips = ["", "S"]
rf.valid_tmodes = ["", "Tone", "TSQL", "DTCS", "Cross"]
rf.valid_cross_modes = [
"Tone->Tone",
"Tone->DTCS",
"DTCS->Tone",
"DTCS->",
"->Tone",
"->DTCS",
"DTCS->DTCS",
]
rf.valid_modes = ["FM", "NFM", "AM"]
rf.valid_power_levels = self.POWER_LEVELS
rf.valid_name_length = 8
rf.valid_duplexes = ["", "-", "+", "split", "off"]
rf.valid_bands = [(108000000, 136000000), # Aircraft AM
(136000000, 180000000), # supports 2m
(230000000, 250000000),
(350000000, 400000000),
(400000000, 520000000), # supports 70cm
(700000000, 985000000)]
rf.valid_characters = chirp_common.CHARSET_ASCII
rf.valid_tuning_steps = self._valid_steps
rf.memory_bounds = (1, 999) # 999 memories
return rf
@classmethod
def get_prompts(cls):
rp = chirp_common.RadioPrompts()
rp.experimental = ("This radio driver is currently under development. "
"There are no known issues with it, but you should "
"proceed with caution.")
return rp
def get_raw_memory(self, number):
return repr(self._memobj.chan_blk[number - 1])
def get_memory(self, number):
""" Public get_memory
Return the channel memory referenced by number to the UI.
"""
_mem = self._memobj.chan_blk[number - 1]
_nam = self._memobj.chan_name[number - 1]
_val = self._memobj.chan_valid[number - 1]
mem = chirp_common.Memory()
mem.number = number
_valid = _mem.state
# This code attempts to robustly decipher what Wouxun considers valid
# memory locations on the 9 series radios and the factory CPS.
# It appears they use a combination of State and Rx Freq to determine
# validity rather than just the State value.
# It is possible the State value is not even used at all.
# Rather than continuously adding new Mem Valid values as they are
# found, assume any value other than 0xFF is likely valid and use
# Rx Freq to further assess validity
if _mem.rxfreq == 0xFFFFFFFF:
# Rx freq indicates empty channel memory
# assume empty regardless of _valid and proceed to next channel
if _valid not in INVALID_MEM_VALUES:
# only log if _valid indicates the channel is not invalid
LOG.debug("CH %s Rx Freq = 0xFFFFFFFF - "
"Treating chan as empty", mem.number)
mem.empty = True
_val.ch_valid = CHAN_INVALID
return mem
elif _valid in INVALID_MEM_VALUES:
# Check for 9PX case where CPS creates a valid channel with
# 0xFF for State - accept it as valid as long as Rx Freq is
# <= max value
if _mem.rxfreq > 99999999: # Max poss Value = 999.99999 MHz
LOG.debug("CH %s State invalid - Rx Frq > 999.99999 MHz: "
"Treating chan as empty", mem.number)
mem.empty = True
_val.ch_valid = CHAN_INVALID
return mem
else:
LOG.debug("CH %s State invalid - Rx Freq valid: "
"Assume chan valid", mem.number)
mem.empty = False
_val.ch_valid = CHAN_VALID
else: # State not Invalid and Rx Freq not 0xFFFFFFFF
if _mem.rxfreq > 99999999: # Max poss Value = 999.99999 MHz
LOG.debug("CH %s Invalid Rx Frq: %s MHz - "
"Treating chan as empty", mem.number,
int(_mem.rxfreq) / 100000)
mem.empty = True
_val.ch_valid = CHAN_INVALID
return mem
else:
_val.ch_valid = CHAN_VALID
mem.empty = False
mem.freq = int(_mem.rxfreq) * 10
if _mem.txfreq == 0xFFFFFFFF:
# TX freq not set
mem.duplex = "off"
mem.offset = 0
elif int(_mem.rxfreq) == int(_mem.txfreq):
mem.duplex = ""
mem.offset = 0
elif abs(int(_mem.rxfreq) * 10 - int(_mem.txfreq) * 10) > 70000000:
mem.duplex = "split"
mem.offset = int(_mem.txfreq) * 10
else:
mem.duplex = int(_mem.rxfreq) > int(_mem.txfreq) and "-" or "+"
mem.offset = abs(int(_mem.rxfreq) - int(_mem.txfreq)) * 10
mem.name = name2str(_nam.name)
self._tone_model.get_tone(_mem, mem)
mem.skip = "" if bool(_mem.scan) else "S"
mem.power = self.POWER_LEVELS[_mem.pwr]
if _mem.mod == 1:
mem.mode = "AM"
elif _mem.fm_dev == 0:
mem.mode = "FM"
else:
mem.mode = "NFM"
# qt has no home in the UI
return mem
def set_memory(self, mem):
""" Public set_memory
Inverse of get_memory. Update the radio memory image
from the mem object
"""
number = mem.number
_mem = self._memobj.chan_blk[number - 1]
_nam = self._memobj.chan_name[number - 1]
_val = self._memobj.chan_valid[number - 1]
if mem.empty:
# consider putting in a check for chan # that is empty but
# listed as one of the 2 working channels and change them
# to channel 1 to be consistent with CPS and allow
# complete deletion from radio. Otherwise,
# a deleted channel will still show on radio with no name.
# MRT implement the above working channel check
if self._memobj.a_conf.w_chan == number:
self._memobj.a_conf.w_chan = 1
if self._memobj.b_conf.w_chan == number:
self._memobj.b_conf.w_chan = 1
_mem.set_raw("\xFF" * (_mem.size() // 8))
_nam.name = str2name("", 8, '\0', '\0')
_mem.state = MEM_INVALID
_val.ch_valid = CHAN_INVALID
return
_mem.rxfreq = int(mem.freq / 10)
if mem.duplex == "off":
_mem.txfreq = 0xFFFFFFFF
elif mem.duplex == "split":
_mem.txfreq = int(mem.offset / 10)
elif mem.duplex == "+":
_mem.txfreq = int(mem.freq / 10) + int(mem.offset / 10)
elif mem.duplex == "-":
_mem.txfreq = int(mem.freq / 10) - int(mem.offset / 10)
else:
_mem.txfreq = int(mem.freq / 10)
_mem.scan = int(mem.skip != "S")
if mem.mode == "FM":
_mem.mod = 0 # make sure forced AM is off
_mem.fm_dev = 0
elif mem.mode == "NFM":
_mem.mod = 0
_mem.fm_dev = 1
elif mem.mode == "AM":
_mem.mod = 1 # AM on
_mem.fm_dev = 1 # set NFM bandwidth
else:
_mem.mod = 0
_mem.fm_dev = 0 # Catchall default is FM
# set the tone
self._tone_model.set_tone(mem, _mem)
# set the power
if mem.power:
_mem.pwr = self.POWER_LEVELS.index(mem.power)
else:
_mem.pwr = True
# Set fields we can't access via the UI table to safe defaults
_mem.qt = 0 # mute mode to QT
_mem.bit5 = 0 # clear this bit to ensure accurate CPS power level
_nam.name = str2name(mem.name, 8, '\0', '\0')
_mem.state = MEM_VALID
_val.ch_valid = CHAN_VALID
# Build the UI configuration tabs
# the channel memory tab is built by the core.
# We have no control over it
def _core_tab(self):
""" Build Core Configuration tab
Radio settings common to all modes and areas go here.
"""
s = self._memobj.settings
if (self.MODEL == "KG-UV9PX" or self.MODEL == "KG-UV9GX"):
sm = self._memobj.screen
cf = RadioSettingGroup("cfg_grp", "Configuration")
cf.append(RadioSetting("auto_am",
"Auto detect AM (Menu 53)",
RadioSettingValueBoolean(s.auto_am)))
cf.append(RadioSetting("qt_sw",
"Scan tone detect (Menu 59)",
RadioSettingValueBoolean(s.qt_sw)))
cf.append(
RadioSetting("s_mute",
"SubFreq Mute (Menu 60)",
RadioSettingValueList(S_MUTE_LIST,
current_index=s.s_mute)))
cf.append(
RadioSetting("tot",
"Transmit timeout Timer (Menu 10)",
RadioSettingValueList(TIMEOUT_LIST,
current_index=s.tot)))
cf.append(
RadioSetting("toa",
"Transmit Timeout Alarm (Menu 11)",
RadioSettingValueList(TOA_LIST,
current_index=s.toa)))
cf.append(
RadioSetting("ptt_id",
"PTT Caller ID mode (Menu 23)",
RadioSettingValueList(PTTID_LIST,
current_index=s.ptt_id)))
cf.append(
RadioSetting("id_dly",
"Caller ID Delay time (Menu 25)",
RadioSettingValueList(ID_DLY_LIST,
current_index=s.id_dly)))
cf.append(RadioSetting("voice_sw",
"Voice Guide (Menu 12)",
RadioSettingValueBoolean(s.voice_sw)))
cf.append(RadioSetting("beep",
"Keypad Beep (Menu 13)",
RadioSettingValueBoolean(s.beep)))
cf.append(
RadioSetting("s_tone",
"Side Tone (Menu 36)",
RadioSettingValueList(S_TONES,
current_index=s.s_tone)))
cf.append(
RadioSetting("ring_time",
"Ring Time (Menu 26)",
RadioSettingValueList(
LIST_OFF_10,
current_index=s.ring_time)))
cf.append(
RadioSetting("roger",
"Roger Beep (Menu 9)",
RadioSettingValueList(ROGER_LIST,
current_index=s.roger)))
cf.append(RadioSetting("blcdsw",
"Backlight (Menu 41)",
RadioSettingValueBoolean(s.blcdsw)))
cf.append(
RadioSetting("abr",
"Auto Backlight Time (Menu 1)",
RadioSettingValueList(BACKLIGHT_LIST,
current_index=s.abr)))
cf.append(
RadioSetting("abr_lvl",
"Backlight Brightness (Menu 27)",
RadioSettingValueInteger(BACKLIGHT_BRIGHT_MIN,
BACKLIGHT_BRIGHT_MAX,
s.abr_lvl)))
cf.append(RadioSetting("lock",
"Keypad Lock",
RadioSettingValueBoolean(s.lock)))
cf.append(
RadioSetting("lock_m",
"Keypad Lock Mode (Menu 35)",
RadioSettingValueList(LOCK_MODES,
current_index=s.lock_m)))
cf.append(RadioSetting("auto_lk",
"Keypad Autolock (Menu 34)",
RadioSettingValueBoolean(s.auto_lk)))
cf.append(RadioSetting("prich_sw",
"Priority Channel Scan (Menu 33)",
RadioSettingValueBoolean(s.prich_sw)))
cf.append(RadioSetting("pri_ch",
"Priority Channel (Menu 32)",
RadioSettingValueInteger(1, 999,
s.pri_ch)))
cf.append(
RadioSetting("dtmf_st",
"DTMF Sidetone (Menu 22)",
RadioSettingValueList(DTMFST_LIST,
current_index=s.dtmf_st)))
cf.append(RadioSetting("sc_qt",
"Scan QT Save Mode (Menu 38)",
RadioSettingValueList(
SCQT_LIST,
current_index=s.sc_qt)))
cf.append(
RadioSetting("apo_tmr",
"Automatic Power-off (Menu 39)",
RadioSettingValueList(APO_TIMES,
current_index=s.apo_tmr)))
cf.append( # VOX "guard" is really VOX trigger audio level
RadioSetting("vox_grd",
"VOX level (Menu 7)",
RadioSettingValueList(VOX_GRDS,
current_index=s.vox_grd)))
cf.append(
RadioSetting("vox_dly",
"VOX Delay (Menu 37)",
RadioSettingValueList(VOX_DLYS,
current_index=s.vox_dly)))
cf.append(RadioSetting("bledsw",
"Receive LED (Menu 42)",
RadioSettingValueBoolean(s.bledsw)))
if (self.MODEL == "KG-UV9PX" or self.MODEL == "KG-UV9GX"):
cf.append(RadioSetting("screen.screen_mode",
"Screen Mode (Menu 62)",
RadioSettingValueList(
SCREEN_MODE_LIST,
current_index=sm.screen_mode)))
if (self.MODEL == "KG-UV9PX" or self.MODEL == "KG-UV9GX"):
langlst = LANGUAGE_LIST2
else:
langlst = LANGUAGE_LIST
cf.append(
RadioSetting("lang",
"Menu Language (Menu 14)",
RadioSettingValueList(langlst,
current_index=s.lang)))
if (self.MODEL == "KG-UV9PX" or self.MODEL == "KG-UV9GX"):
ponmsglst = PONMSG_LIST2
else:
ponmsglst = PONMSG_LIST
cf.append(RadioSetting("ponmsg",
"Poweron message (Menu 40)",
RadioSettingValueList(
ponmsglst, current_index=s.ponmsg)))
return cf
def _repeater_tab(self):
"""Repeater mode functions
"""
s = self._memobj.settings
cf = RadioSettingGroup("repeater", "Repeater Functions")
cf.append(
RadioSetting("type_set",
"Radio Mode (Menu 43)",
RadioSettingValueList(
RPTMODE_LIST,
current_index=s.type_set)))
cf.append(RadioSetting("rpt_ptt",
"Repeater PTT (Menu 45)",
RadioSettingValueBoolean(s.rpt_ptt)))
cf.append(RadioSetting("rpt_spk",
"Repeater Mode Speaker (Menu 44)",
RadioSettingValueBoolean(s.rpt_spk)))
cf.append(
RadioSetting("rpt_kpt",
"Repeater Hold Time (Menu 46)",
RadioSettingValueList(RPT_KPTS,
current_index=s.rpt_kpt)))
cf.append(RadioSetting("rpt_rct",
"Repeater Receipt Tone (Menu 47)",
RadioSettingValueBoolean(s.rpt_rct)))
return cf
def _admin_tab(self):
"""Admin functions not present in radio menu...
These are admin functions not radio operation configuration
"""
def apply_cid(setting, obj):
c = str2callid(setting.value)
obj.code = c
def apply_scc(setting, obj):
c = str2digits(setting.value)
obj.scc = c
def apply_mode_sw(setting, obj):
pw = str2pw(setting.value)
obj.mode_sw = pw
setting.value = pw2str(obj.mode_sw)
def apply_reset(setting, obj):
pw = str2pw(setting.value)
obj.reset = pw
setting.value = pw2str(obj.reset)
def apply_wake(setting, obj):
obj.wake = int(setting.value)/10
def apply_sleep(setting, obj):
obj.sleep = int(setting.value)/10
pw = self._memobj.passwords # admin passwords
s = self._memobj.settings
cf = RadioSettingGroup("admin", "Admin Functions")
cf.append(RadioSetting("menu_avail",
"Menu available in channel mode",
RadioSettingValueBoolean(s.menu_avail)))
mode_sw = RadioSettingValueString(0, 6,
pw2str(pw.mode_sw), False)
rs = RadioSetting("passwords.mode_sw",
"Mode Switch Password", mode_sw)
rs.set_apply_callback(apply_mode_sw, pw)
cf.append(rs)
cf.append(RadioSetting("reset_avail",
"Radio Reset Available",
RadioSettingValueBoolean(s.reset_avail)))
reset = RadioSettingValueString(0, 6, pw2str(pw.reset), False)
rs = RadioSetting("passwords.reset",
"Radio Reset Password", reset)
rs.set_apply_callback(apply_reset, pw)
cf.append(rs)
cf.append(
RadioSetting("dtmf_tx",
"DTMF Tx Duration",
RadioSettingValueList(DTMF_TIMES,
current_index=s.dtmf_tx)))
cid = self._memobj.my_callid
my_callid = RadioSettingValueString(3, 6,
self.callid2str(cid.code), False)
rs = RadioSetting("my_callid.code",
"PTT Caller ID code (Menu 24)", my_callid)
rs.set_apply_callback(apply_cid, cid)
cf.append(rs)
stun = self._memobj.stun
st = RadioSettingValueString(0, 6, digits2str(stun.scc), False)
rs = RadioSetting("stun.scc", "Security code", st)
rs.set_apply_callback(apply_scc, stun)
cf.append(rs)
cf.append(
RadioSetting("settings.save_m",
"Save Mode (Menu 2)",
RadioSettingValueList(SAVE_MODES,
current_index=s.save_m)))
for i in range(0, 4):
sm = self._memobj.save[i]
wake = RadioSettingValueInteger(0, 18000, sm.wake * 10, 1)
wf = RadioSetting("save[%i].wake" % i,
"Save Mode %d Wake Time" % (i+1), wake)
wf.set_apply_callback(apply_wake, sm)
cf.append(wf)
slp = RadioSettingValueInteger(0, 18000, sm.sleep * 10, 1)
wf = RadioSetting("save[%i].sleep" % i,
"Save Mode %d Sleep Time" % (i+1), slp)
wf.set_apply_callback(apply_sleep, sm)
cf.append(wf)
_msg = str(self._memobj.display.banner).split("\0")[0]
val = RadioSettingValueString(0, 16, _msg)
val.set_mutable(True)
cf.append(RadioSetting("display.banner",
"Display Message", val))
if (self.MODEL == "KG-UV9PX" or self.MODEL == "KG-UV9GX"):
_str = str(self._memobj.oemmodel.model).split("\0")[0]
val = RadioSettingValueString(0, 10, _str)
val.set_mutable(True)
cf.append(RadioSetting("oemmodel.model",
"Custom Sub-Receiver Message", val))
val = RadioSettingValueList(
TDR_LIST,
current_index=s.tdr)
val.set_mutable(True)
cf.append(RadioSetting("tdr", "TDR", val))
val = RadioSettingValueList(
ACTIVE_AREA_LIST,
current_index=s.act_area)
val.set_mutable(True)
cf.append(RadioSetting("act_area", "Active Receiver(BAND)", val))
return cf
def _fm_tab(self):
"""FM Broadcast channels
"""
def apply_fm(setting, obj):
f = freq2short(setting.value, 76000000, 108000000)
obj.fm_freq = f
fm = RadioSettingGroup("fm_chans", "FM Broadcast")
for ch in range(0, 20):
chan = self._memobj.fm_chans[ch]
freq = RadioSettingValueString(0, 20,
short2freq(chan.fm_freq))
rs = RadioSetting("fm_%d" % (ch + 1),
"FM Channel %d" % (ch + 1), freq)
rs.set_apply_callback(apply_fm, chan)
fm.append(rs)
return fm
def _scan_grp(self):
"""Scan groups
"""
def apply_name(setting, obj):
name = str2name(setting.value, 8, '\0', '\0')
obj.name = name
def apply_start(setting, obj):
"""Do a callback to deal with RadioSettingInteger limitation
on memory address resolution
"""
obj.scan_st = int(setting.value)
def apply_end(setting, obj):
"""Do a callback to deal with RadioSettingInteger limitation
on memory address resolution
"""
obj.scan_end = int(setting.value)
sgrp = self._memobj.scn_grps
scan = RadioSettingGroup("scn_grps", "Channel Scanner Groups")
for i in range(0, 10):
s_grp = sgrp.addrs[i]
s_name = sgrp.names[i]
rs_name = RadioSettingValueString(0, 8,
name2str(s_name.name))
rs = RadioSetting("scn_grps.names[%i].name" % i,
"Group %i Name" % (i + 1), rs_name)
rs.set_apply_callback(apply_name, s_name)
scan.append(rs)
rs_st = RadioSettingValueInteger(1, 999, s_grp.scan_st)
rs = RadioSetting("scn_grps.addrs[%i].scan_st" % i,
"Starting Channel", rs_st)
rs.set_apply_callback(apply_start, s_grp)
scan.append(rs)
rs_end = RadioSettingValueInteger(1, 999, s_grp.scan_end)
rs = RadioSetting("scn_grps.addrs[%i].scan_end" % i,
"Last Channel", rs_end)
rs.set_apply_callback(apply_end, s_grp)
scan.append(rs)
return scan
def _callid_grp(self):
"""Caller IDs to be recognized by radio
This really should be a table in the UI
"""
def apply_callid(setting, obj):
c = str2callid(setting.value)
obj.cid = c
def apply_name(setting, obj):
name = str2name(setting.value, 6, '\0', '\xff')
obj.name = name
cid = RadioSettingGroup("callids", "Caller IDs")
for i in range(0, 20):
callid = self._memobj.call_ids[i]
name = self._memobj.cid_names[i]
c_name = RadioSettingValueString(0, 6, name2str(name.name))
rs = RadioSetting("cid_names[%i].name" % i,
"Caller ID %i Name" % (i + 1), c_name)
rs.set_apply_callback(apply_name, name)
cid.append(rs)
c_id = RadioSettingValueString(0, 6,
self.callid2str(callid.cid),
False)
rs = RadioSetting("call_ids[%i].cid" % i,
"Caller ID Code", c_id)
rs.set_apply_callback(apply_callid, callid)
cid.append(rs)
return cid
def _band_tab(self, area, band):
""" Build a band tab inside a VFO/Area
"""
def apply_freq(setting, lo, hi, obj):
f = freq2int(setting.value, lo, hi)
obj.freq = f/10
def apply_offset(setting, obj):
f = freq2int(setting.value, 0, 5000000)
obj.offset = f/10
def apply_enc(setting, obj):
t = tone2short(setting.value)
obj.encqt = t
def apply_dec(setting, obj):
t = tone2short(setting.value)
obj.decqt = t
if area == "a":
if band == 150:
c = self._memobj.vfo_a.band_150
lo = 108000000
hi = 180000000
elif band == 200:
c = self._memobj.vfo_a.band_200
lo = 230000000
hi = 250000000
elif band == 300:
c = self._memobj.vfo_a.band_300
lo = 350000000
hi = 400000000
elif band == 450:
c = self._memobj.vfo_a.band_450
lo = 400000000
hi = 512000000
else: # 700
c = self._memobj.vfo_a.band_700
lo = 700000000
hi = 985000000
else: # area 'b'
if band == 150:
c = self._memobj.vfo_b.band_150
lo = 136000000
hi = 180000000
else: # 450
c = self._memobj.vfo_b.band_450
lo = 400000000
hi = 512000000
prefix = "vfo_%s.band_%d" % (area, band)
bf = RadioSettingGroup(prefix, "%d MHz Band" % band)
freq = RadioSettingValueString(0, 15, int2freq(c.freq * 10))
rs = RadioSetting(prefix + ".freq", "Rx Frequency", freq)
rs.set_apply_callback(apply_freq, lo, hi, c)
bf.append(rs)
off = RadioSettingValueString(0, 15, int2freq(c.offset * 10))
rs = RadioSetting(prefix + ".offset", "Tx Offset (Menu 28)", off)
rs.set_apply_callback(apply_offset, c)
bf.append(rs)
rs = RadioSetting(prefix + ".encqt",
"Encode QT (Menu 17,19)",
RadioSettingValueList(TONE_LIST,
short2tone(c.encqt)))
rs.set_apply_callback(apply_enc, c)
bf.append(rs)
rs = RadioSetting(prefix + ".decqt",
"Decode QT (Menu 16,18)",
RadioSettingValueList(TONE_LIST,
short2tone(c.decqt)))
rs.set_apply_callback(apply_dec, c)
bf.append(rs)
bf.append(RadioSetting(prefix + ".qt",
"Mute Mode (Menu 21)",
RadioSettingValueList(SPMUTE_LIST,
current_index=c.qt)))
bf.append(RadioSetting(prefix + ".scan",
"Scan this (Menu 48)",
RadioSettingValueBoolean(c.scan)))
bf.append(RadioSetting(prefix + ".pwr",
"Power (Menu 5)",
RadioSettingValueList(
POWER_LIST, current_index=c.pwr)))
bf.append(RadioSetting(prefix + ".mod",
"AM Modulation (Menu 54)",
RadioSettingValueBoolean(c.mod)))
bf.append(RadioSetting(prefix + ".fm_dev",
"FM Deviation (Menu 4)",
RadioSettingValueList(
BANDWIDTH_LIST,
current_index=c.fm_dev)))
bf.append(
RadioSetting(prefix + ".shift",
"Frequency Shift (Menu 6)",
RadioSettingValueList(OFFSET_LIST,
current_index=c.shift)))
return bf
def _area_tab(self, area):
"""Build a VFO tab
"""
def apply_scan_st(setting, scan_lo, scan_hi, obj):
f = freq2short(setting.value, scan_lo, scan_hi)
obj.scan_st = f
def apply_scan_end(setting, scan_lo, scan_hi, obj):
f = freq2short(setting.value, scan_lo, scan_hi)
obj.scan_end = f
if area == "a":
desc = "Area A Settings"
c = self._memobj.a_conf
scan_lo = 108000000
scan_hi = 985000000
scan_rng = self._memobj.settings.a
band_list = (150, 200, 300, 450, 700)
else:
desc = "Area B Settings"
c = self._memobj.b_conf
scan_lo = 136000000
scan_hi = 512000000
scan_rng = self._memobj.settings.b
band_list = (150, 450)
prefix = "%s_conf" % area
af = RadioSettingGroup(prefix, desc)
af.append(
RadioSetting(prefix + ".w_mode",
"Workmode",
RadioSettingValueList(
WORKMODE_LIST,
current_index=c.w_mode)))
af.append(RadioSetting(prefix + ".w_chan",
"Channel",
RadioSettingValueInteger(1, 999,
c.w_chan)))
af.append(
RadioSetting(prefix + ".scan_grp",
"Scan Group (Menu 49)",
RadioSettingValueList(
SCANGRP_LIST,
current_index=c.scan_grp)))
af.append(RadioSetting(prefix + ".bcl",
"Busy Channel Lock-out (Menu 15)",
RadioSettingValueBoolean(c.bcl)))
af.append(
RadioSetting(prefix + ".sql",
"Squelch Level (Menu 8)",
RadioSettingValueList(LIST_0_9,
current_index=c.sql)))
af.append(
RadioSetting(prefix + ".cset",
"Call ID Group (Menu 52)",
RadioSettingValueList(LIST_1_20,
current_index=c.cset)))
af.append(
RadioSetting(prefix + ".step",
"Frequency Step (Menu 3)",
RadioSettingValueList(
self._step_list, current_index=c.step)))
af.append(
RadioSetting(prefix + ".scan_mode",
"Scan Mode (Menu 20)",
RadioSettingValueList(
SCANMODE_LIST,
current_index=c.scan_mode)))
af.append(
RadioSetting(prefix + ".scan_range",
"Scan Range (Menu 50)",
RadioSettingValueList(
SCANRANGE_LIST,
current_index=c.scan_range)))
st = RadioSettingValueString(0, 15,
short2freq(scan_rng.scan_st))
rs = RadioSetting("settings.%s.scan_st" % area,
"Frequency Scan Start", st)
rs.set_apply_callback(apply_scan_st, scan_lo, scan_hi, scan_rng)
af.append(rs)
end = RadioSettingValueString(0, 15,
short2freq(scan_rng.scan_end))
rs = RadioSetting("settings.%s.scan_end" % area,
"Frequency Scan End", end)
rs.set_apply_callback(apply_scan_end, scan_lo, scan_hi,
scan_rng)
af.append(rs)
# Each area has its own set of bands
for band in (band_list):
af.append(self._band_tab(area, band))
return af
def _key_tab(self):
"""Build radio key/button menu
"""
s = self._memobj.settings
if self.MODEL == "KG-UV9PX":
pfkey1 = PF1KEY_LIST
pfkey2 = PF2KEY_LIST
pfkey3 = PF3KEY_LIST2
elif (self.MODEL == "KG-UV9GX" or
self.MODEL == "KG-UV9G Pro"):
pfkey1 = PF1KEY_LIST9GX
pfkey2 = PF2KEY_LIST9GX
pfkey3 = PF3KEY_LIST9GX
else:
pfkey1 = PF1KEY_LIST
pfkey2 = PF2KEY_LIST
pfkey3 = PF3KEY_LIST
kf = RadioSettingGroup("key_grp", "Key Settings")
kf.append(RadioSetting("settings.pf1",
"PF1 Key function (Menu 55)",
RadioSettingValueList(
pfkey1,
current_index=s.pf1)))
kf.append(RadioSetting("settings.pf2",
"PF2 Key function (Menu 56)",
RadioSettingValueList(
pfkey2,
current_index=s.pf2)))
kf.append(RadioSetting("settings.pf3",
"PF3 Key function (Menu 57)",
RadioSettingValueList(
pfkey3,
current_index=s.pf3)))
return kf
def _fl_tab(self):
"""Build the frequency limits tab
"""
# The stop limits in the factory KG-UV9D Mate memory image are 1 MHz
# higher than the published specs. The settings panel will crash if
# it encounters a value outside of these ranges.
hard_limits = {
"band_150": (108000000, 181000000),
"band_450": (400000000, 513000000),
"band_300": (350000000, 401000000),
"band_700": (700000000, 987000000),
"band_200": (230000000, 251000000)
}
def apply_freq_start(setting, low, high, obj):
f = freq2short(setting.value, low, high)
obj.start = f
def apply_freq_stop(setting, low, high, obj):
"""Sets the stop limit to 1 MHz below the input value"""
# The firmware has an off-by-1MHz error with stop limits.
# If you set the stop limit to 1480 (148 MHz), you can still tune
# up to 148.99 MHz. To compensate for this,
# we subtract 10 increments of 100 MHz before storing the value.
f = freq2short(setting.value, low, high) - 10
obj.stop = f
fl = RadioSettingGroup("freq_limit_grp", "Frequency Limits")
rx = self._memobj.rx_freq_limits
tx = self._memobj.tx_freq_limits
for rx_band in rx.items():
name, limits = rx_band
start_freq = RadioSettingValueString(1,
20,
short2freq(limits.start))
start_rs = RadioSetting("rx_start_" + name,
name + " Receive Start",
start_freq)
start_rs.set_apply_callback(apply_freq_start,
hard_limits[name][0],
hard_limits[name][1],
limits)
fl.append(start_rs)
# Add 10 increments of 100 MHz before displaying to compensate for
# the firmware off-by-1MHz problem.
stop_freq = RadioSettingValueString(1,
20,
short2freq(limits.stop + 10))
stop_rs = RadioSetting("rx_stop_" + name,
name + " Receive Stop",
stop_freq)
stop_rs.set_apply_callback(apply_freq_stop,
hard_limits[name][0],
hard_limits[name][1],
limits)
fl.append(stop_rs)
for tx_band in tx.items():
name, limits = tx_band
start_freq = RadioSettingValueString(1,
20,
short2freq(limits.start))
start_rs = RadioSetting("tx_start_" + name,
name + " Transmit Start",
start_freq)
start_rs.set_apply_callback(apply_freq_start,
hard_limits[name][0],
hard_limits[name][1], limits)
fl.append(start_rs)
# Add 10 increments of 100 MHz before displaying to compensate for
# the firmware off-by-1MHz problem.
stop_freq = RadioSettingValueString(1,
20,
short2freq(limits.stop + 10))
stop_rs = RadioSetting("tx_stop_" + name,
name + " Transmit Stop",
stop_freq)
stop_rs.set_apply_callback(apply_freq_stop,
hard_limits[name][0],
hard_limits[name][1],
limits)
fl.append(stop_rs)
return fl
def _get_settings(self):
"""Build the radio configuration settings menus
"""
core_grp = self._core_tab()
fm_grp = self._fm_tab()
area_a_grp = self._area_tab("a")
area_b_grp = self._area_tab("b")
key_grp = self._key_tab()
scan_grp = self._scan_grp()
callid_grp = self._callid_grp()
admin_grp = self._admin_tab()
rpt_grp = self._repeater_tab()
freq_limit_grp = self._fl_tab()
core_grp.append(key_grp)
core_grp.append(admin_grp)
core_grp.append(rpt_grp)
core_grp.append(freq_limit_grp)
group = RadioSettings(core_grp,
area_a_grp,
area_b_grp,
fm_grp,
scan_grp,
callid_grp
)
return group
def get_settings(self):
""" Public build out linkage between radio settings and UI
"""
try:
return self._get_settings()
except Exception:
import traceback
LOG.error("Failed to parse settings: %s",
traceback.format_exc())
return None
def _is_freq(self, element):
"""This is a hack to smoke out whether we need to do
frequency translations for otherwise innocent u16s and u32s
"""
return "rxfreq" in element.get_name() or \
"txfreq" in element.get_name() or \
"scan_st" in element.get_name() or \
"scan_end" in element.get_name() or \
"offset" in element.get_name() or \
"fm_stop" in element.get_name()
def _is_limit(self, element):
return "lower_limit" in element.get_name() or\
"upper_limit" in element.get_name()
def set_settings(self, settings):
""" Public update radio settings via UI callback
A lot of this should be in common code....
"""
for element in settings:
if not isinstance(element, RadioSetting):
LOG.debug("set_settings: not instance %s" %
element.get_name())
self.set_settings(element)
continue
else:
try:
if "." in element.get_name():
bits = element.get_name().split(".")
obj = self._memobj
for bit in bits[:-1]:
# decode an array index
if "[" in bit and "]" in bit:
bit, index = bit.split("[", 1)
index, junk = index.split("]", 1)
index = int(index)
obj = getattr(obj, bit)[index]
else:
obj = getattr(obj, bit)
setting = bits[-1]
else:
obj = self._memobj.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))
if self._is_freq(element):
setattr(obj, setting, int(element.value)/10)
elif self._is_limit(element):
setattr(obj, setting, int(element.value)*10)
else:
setattr(obj, setting, element.value)
except Exception:
LOG.debug("set_settings: Exception with %s" %
element.get_name())
raise
def callid2str(self, cid):
"""Caller ID per MDC-1200 spec? Must be 3-6 digits (100 - 999999).
One digit (binary) per byte, terminated with '0xc'
"""
bin2ascii = " 1234567890"
cidstr = ""
for i in range(0, 6):
b = cid[i].get_value()
if b == 0xc: # the cid EOL
break
if b == 0 or b > 0xa:
raise InvalidValueError(
"Caller ID code has illegal byte 0x%x" % b)
cidstr += bin2ascii[b]
return cidstr
@directory.register
class KGUV9PXRadio(KGUV9DPlusRadio):
"""Wouxun KG-UV9PX"""
VENDOR = "Wouxun"
MODEL = "KG-UV9PX"
_model = b"KG-UV9D"
_rev = b"02" # default rev for the radio I know about...
_file_ident = b"kg-uv9px"
_valid_steps = STEPS
_step_list = STEP_LIST
@classmethod
def match_model(cls, filedata, filename):
# This model is only ever matched via metadata
return False
def process_mmap(self):
if self._rev != b"02" and self._rev != b"00":
# new revision found - log it and assume same map and proceed
LOG.debug("Unrecognized model variation (%s) Using default Map" %
self._rev)
self._memobj = bitwise.parse(_MEM_FORMAT_9PX, self._mmap)
def get_features(self):
""" Public get_features
Return the features of this radio once we have identified
it and gotten its bits
"""
rf = chirp_common.RadioFeatures()
rf.has_settings = True
rf.has_ctone = True
rf.has_rx_dtcs = True
rf.has_cross = True
rf.has_tuning_step = False
rf.has_bank = False
rf.can_odd_split = True
rf.valid_skips = ["", "S"]
rf.valid_tmodes = ["", "Tone", "TSQL", "DTCS", "Cross"]
rf.valid_cross_modes = [
"Tone->Tone",
"Tone->DTCS",
"DTCS->Tone",
"DTCS->",
"->Tone",
"->DTCS",
"DTCS->DTCS",
]
rf.valid_modes = ["FM", "NFM", "AM"]
rf.valid_power_levels = self.POWER_LEVELS
rf.valid_name_length = 8
rf.valid_duplexes = ["", "-", "+", "split", "off"]
rf.valid_bands = [(108000000, 135997500), # Aircraft AM
(136000000, 180997500), # supports 2m
(219000000, 250997500),
(350000000, 399997500),
(400000000, 512997500), # supports 70cm
(700000000, 986997500)]
rf.valid_characters = chirp_common.CHARSET_ASCII
rf.valid_tuning_steps = STEPS
rf.memory_bounds = (1, 999) # 999 memories
return rf
def callid2str(self, cid):
"""Caller ID per MDC-1200 spec? Must be 3-6 digits (100 - 999999).
One digit (binary) per byte, terminated with '0xc'
"""
bin2ascii = " 1234567890"
cidstr = ""
for i in range(0, 6):
b = cid[i].get_value()
# 9PX factory reset CID use 0x00 for 0 digit - instead of 0x0a
# remap 0x00 to 0x0a
if b == 0x00:
b = 0x0a
if b == 0xc or b == 0xf0: # the cid EOL
break
if b > 0xa:
raise InvalidValueError(
"Caller ID code has illegal byte 0x%x" % b)
cidstr += bin2ascii[b]
return cidstr
def _get_settings(self):
"""Build the radio configuration settings menus
"""
core_grp = self._core_tab()
fm_grp = self._fm_tab()
area_a_grp = self._area_tab("a")
area_b_grp = self._area_tab("b")
key_grp = self._key_tab()
scan_grp = self._scan_grp()
callid_grp = self._callid_grp()
admin_grp = self._admin_tab()
rpt_grp = self._repeater_tab()
freq_limit_grp = self._fl_tab()
core_grp.append(key_grp)
core_grp.append(admin_grp)
core_grp.append(rpt_grp)
group = RadioSettings(core_grp,
area_a_grp,
area_b_grp,
fm_grp,
scan_grp,
callid_grp,
freq_limit_grp,)
return group
def _area_tab(self, area):
"""Build a VFO tab
"""
def apply_scan_st(setting, scan_lo, scan_hi, obj):
f = freq2short(setting.value, scan_lo, scan_hi)
obj.scan_st = f
def apply_scan_end(setting, scan_lo, scan_hi, obj):
f = freq2short(setting.value, scan_lo, scan_hi)
obj.scan_end = f
if area == "a":
desc = "Receiver A Settings"
c = self._memobj.a_conf
scan_lo = 108000000
scan_hi = 985997500
scan_rng = self._memobj.settings.a
band_list = (150, 200, 300, 450, 700)
else:
desc = "Receiver B Settings"
c = self._memobj.b_conf
scan_lo = 136000000
scan_hi = 512997500
scan_rng = self._memobj.settings.b
band_list = (150, 450)
prefix = "%s_conf" % area
af = RadioSettingGroup(prefix, desc)
af.append(
RadioSetting(prefix + ".w_mode",
"Workmode",
RadioSettingValueList(
WORKMODE_LIST,
current_index=c.w_mode)))
af.append(RadioSetting(prefix + ".w_chan",
"Channel",
RadioSettingValueInteger(1, 999,
c.w_chan)))
af.append(
RadioSetting(prefix + ".scan_grp",
"Scan Group (Menu 49)",
RadioSettingValueList(
SCANGRP_LIST,
current_index=c.scan_grp)))
af.append(RadioSetting(prefix + ".bcl",
"Busy Channel Lock-out (Menu 15)",
RadioSettingValueBoolean(c.bcl)))
af.append(
RadioSetting(prefix + ".sql",
"Squelch Level (Menu 8)",
RadioSettingValueList(LIST_0_9,
current_index=c.sql)))
af.append(
RadioSetting(prefix + ".cset",
"Call ID Group (Menu 52)",
RadioSettingValueList(LIST_1_20,
current_index=c.cset)))
af.append(
RadioSetting(prefix + ".step",
"Frequency Step (Menu 3)",
RadioSettingValueList(
self._step_list, current_index=c.step)))
af.append(
RadioSetting(prefix + ".scan_mode",
"Scan Mode (Menu 20)",
RadioSettingValueList(
SCANMODE_LIST,
current_index=c.scan_mode)))
af.append(
RadioSetting(prefix + ".scan_range",
"Scan Range (Menu 50)",
RadioSettingValueList(
SCANRANGE_LIST,
current_index=c.scan_range)))
st = RadioSettingValueString(0, 15,
short2freq(scan_rng.scan_st))
rs = RadioSetting("settings.%s.scan_st" % area,
"Frequency Scan Start", st)
rs.set_apply_callback(apply_scan_st, scan_lo, scan_hi, scan_rng)
af.append(rs)
end = RadioSettingValueString(0, 15,
short2freq(scan_rng.scan_end))
rs = RadioSetting("settings.%s.scan_end" % area,
"Frequency Scan End", end)
rs.set_apply_callback(apply_scan_end, scan_lo, scan_hi,
scan_rng)
af.append(rs)
# Each area has its own set of bands
for band in (band_list):
af.append(self._band_tab(area, band))
return af
def _band_tab(self, area, band):
""" Build a band tab inside a VFO/Area
"""
def apply_freq(setting, lo, hi, obj):
f = freq2int(setting.value, lo, hi)
obj.freq = f/10
def apply_offset(setting, obj):
f = freq2int(setting.value, 0, 5000000)
obj.offset = f/10
def apply_enc(setting, obj):
t = tone2short(setting.value)
obj.encqt = t
def apply_dec(setting, obj):
t = tone2short(setting.value)
obj.decqt = t
if area == "a":
if band == 150:
c = self._memobj.vfo_a.band_150
lo = 108000000
hi = 180997500
elif band == 200:
c = self._memobj.vfo_a.band_200
lo = 219000000
hi = 250997500
elif band == 300:
c = self._memobj.vfo_a.band_300
lo = 350000000
hi = 399997500
elif band == 450:
c = self._memobj.vfo_a.band_450
lo = 400000000
hi = 512997500
else: # 700
c = self._memobj.vfo_a.band_700
lo = 700000000
hi = 986997500
else: # area 'b'
if band == 150:
c = self._memobj.vfo_b.band_150
lo = 136000000
hi = 180997500
else: # 450
c = self._memobj.vfo_b.band_450
lo = 400000000
hi = 512997500
prefix = "vfo_%s.band_%d" % (area, band)
bf = RadioSettingGroup(prefix, "%d MHz Band" % band)
freq = RadioSettingValueString(0, 15, int2freq(c.freq * 10))
rs = RadioSetting(prefix + ".freq", "Rx Frequency", freq)
rs.set_apply_callback(apply_freq, lo, hi, c)
bf.append(rs)
off = RadioSettingValueString(0, 15, int2freq(c.offset * 10))
rs = RadioSetting(prefix + ".offset", "Tx Offset (Menu 28)", off)
rs.set_apply_callback(apply_offset, c)
bf.append(rs)
rs = RadioSetting(prefix + ".encqt",
"Encode QT (Menu 17,19)",
RadioSettingValueList(TONE_LIST,
short2tone(c.encqt)))
rs.set_apply_callback(apply_enc, c)
bf.append(rs)
rs = RadioSetting(prefix + ".decqt",
"Decode QT (Menu 16,18)",
RadioSettingValueList(TONE_LIST,
short2tone(c.decqt)))
rs.set_apply_callback(apply_dec, c)
bf.append(rs)
bf.append(RadioSetting(prefix + ".qt",
"Mute Mode (Menu 21)",
RadioSettingValueList(SPMUTE_LIST,
current_index=c.qt)))
bf.append(RadioSetting(prefix + ".scan",
"Scan this (Menu 48)",
RadioSettingValueBoolean(c.scan)))
bf.append(RadioSetting(prefix + ".pwr",
"Power (Menu 5)",
RadioSettingValueList(
POWER_LIST, current_index=c.pwr)))
bf.append(RadioSetting(prefix + ".mod",
"AM Modulation (Menu 54)",
RadioSettingValueBoolean(c.mod)))
bf.append(RadioSetting(prefix + ".fm_dev",
"FM Deviation (Menu 4)",
RadioSettingValueList(
BANDWIDTH_LIST,
current_index=c.fm_dev)))
bf.append(
RadioSetting(prefix + ".shift",
"Frequency Shift (Menu 6)",
RadioSettingValueList(OFFSET_LIST,
current_index=c.shift)))
return bf
def _fl_tab(self):
freq_limit_grp = RadioSettingGroup("limits",
"Freq Limits")
limgrp = freq_limit_grp
l = self._memobj.limits
if self.MODEL == "KG-UV9PX":
val = RadioSettingValueInteger(136, 180,
(l.lim_150M_Txlower_limit) / 10.0)
rs = RadioSetting("limits.lim_150M_Txlower_limit",
"150M Tx Lower Limit (MHz)",
val)
limgrp.append(rs)
val = RadioSettingValueInteger(136, 180,
(l.lim_150M_Txupper_limit) / 10.0)
rs = RadioSetting("limits.lim_150M_Txupper_limit",
"150M Tx Upper Limit (MHz + 0.9975)",
val)
limgrp.append(rs)
val = RadioSettingValueInteger(400, 512,
(l.lim_450M_Txlower_limit) / 10.0)
rs = RadioSetting("limits.lim_450M_Txlower_limit",
"450M Tx Lower Limit (MHz)",
val)
limgrp.append(rs)
val = RadioSettingValueInteger(400, 512,
(l.lim_450M_Txupper_limit) / 10.0)
rs = RadioSetting("limits.lim_450M_Txupper_limit",
"450M Tx Upper Limit (MHz + 0.9975)",
val)
limgrp.append(rs)
val = RadioSettingValueInteger(108, 180,
(l.lim_150M_area_a_rxlower_limit) /
10.0)
rs = RadioSetting("limits.lim_150M_area_a_rxlower_limit",
"Rcvr A 150M Rx Lower Limit (MHz)",
val)
limgrp.append(rs)
val = RadioSettingValueInteger(108, 180,
(l.lim_150M_area_a_rxupper_limit) /
10.0)
rs = RadioSetting("limits.lim_150M_area_a_rxupper_limit",
"Rcvr A 150M Rx Upper Limit (MHz + 0.9975)",
val)
limgrp.append(rs)
val = RadioSettingValueInteger(136, 180,
(l.lim_150M_area_b_rxlower_limit) /
10.0)
rs = RadioSetting("limits.lim_150M_area_b_rxlower_limit",
"Rcvr B 150M Rx Lower Limit (MHz)",
val)
limgrp.append(rs)
val = RadioSettingValueInteger(136, 180,
(l.lim_150M_area_b_rxupper_limit) /
10.0)
rs = RadioSetting("limits.lim_150M_area_b_rxupper_limit",
"Rcvr B 150M Rx Upper Limit (MHz + 0.9975)",
val)
limgrp.append(rs)
val = RadioSettingValueInteger(400, 512,
(l.lim_450M_rxlower_limit) / 10.0)
rs = RadioSetting("limits.lim_450M_rxlower_limit",
"450M Rx Lower Limit (MHz)",
val)
limgrp.append(rs)
val = RadioSettingValueInteger(400, 512,
(l.lim_450M_rxupper_limit) / 10.0)
rs = RadioSetting("limits.lim_450M_rxupper_limit",
"450M Rx Upper Limit (MHz + 0.9975)",
val)
limgrp.append(rs)
val = RadioSettingValueInteger(350, 399,
(l.lim_300M_rxlower_limit) / 10.0)
rs = RadioSetting("limits.lim_300M_rxlower_limit",
"300M Rx Lower Limit (MHz)",
val)
limgrp.append(rs)
val = RadioSettingValueInteger(350, 399,
(l.lim_300M_rxupper_limit) / 10.0)
rs = RadioSetting("limits.lim_300M_rxupper_limit",
"300M Rx Upper Limit (MHz + 0.9975)",
val)
limgrp.append(rs)
val = RadioSettingValueInteger(700, 986,
(l.lim_800M_rxlower_limit) / 10.0)
rs = RadioSetting("limits.lim_800M_rxlower_limit",
"800M Rx Lower Limit (MHz)",
val)
limgrp.append(rs)
val = RadioSettingValueInteger(700, 986,
(l.lim_800M_rxupper_limit) / 10.0)
rs = RadioSetting("limits.lim_800M_rxupper_limit",
"800M Rx Upper Limit (MHz + 0.9975)",
val)
limgrp.append(rs)
val = RadioSettingValueInteger(219, 250,
(l.lim_210M_rxlower_limit) / 10.0)
rs = RadioSetting("limits.lim_210M_rxlower_limit",
"210M Rx Lower Limit (MHz)",
val)
limgrp.append(rs)
val = RadioSettingValueInteger(219, 250,
(l.lim_210M_rxupper_limit) / 10.0)
rs = RadioSetting("limits.lim_210M_rxupper_limit",
"210M Rx Upper Limit (MHz + 0.9975)",
val)
limgrp.append(rs)
return limgrp
@directory.register
class KGUV9GXRadio(KGUV9PXRadio):
"""Wouxun KG-UV9GX"""
VENDOR = "Wouxun"
MODEL = "KG-UV9GX"
_model = b"KG-UV9D"
_rev = b"02" # default rev for the radio I know about...
_valid_steps = STEPS
_step_list = STEP_LIST
@classmethod
def match_model(cls, filedata, filename):
# This model is only ever matched via metadata
return False
@directory.register
class KGUV9KRadio(KGUV9DPlusRadio):
"""Wouxun KG-UV9K"""
VENDOR = "Wouxun"
MODEL = "KG-UV9K"
_model = b"KG-UV9D"
_file_ident = b"kg-uv9k"
_rev = b"02" # default rev for the radio I know about...
_step_list = STEP_LIST_9K
_valid_steps = STEPS_9K
@classmethod
def match_model(cls, filedata, filename):
# This model is only ever matched via metadata
return False
@directory.register
class KGUV9GProRadio(KGUV9DPlusRadio):
"""Wouxun KG-UV9G Pro"""
VENDOR = "Wouxun"
MODEL = "KG-UV9G Pro"
_model = b"KG-UV9D"
_file_ident = b"kg-uv9gpro"
_rev = b"02" # default rev for the radio I know about...
_step_list = STEP_LIST
_valid_steps = STEPS
@classmethod
def match_model(cls, filedata, filename):
# This model is only ever matched via metadata
return False
chirp-20261030/chirp/drivers/ksun_m6.py 0000644 0001750 0001750 00000035500 14670320477 016566 0 ustar hibby hibby # Copyright 2012 Dan Smith
# Copyright 2024 Yuri D'Elia
#
# 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 .
from chirp import chirp_common, directory, memmap, bitwise, errors
from chirp.settings import (
RadioSetting, RadioSettings, RadioSettingGroup,
RadioSettingValueBoolean, RadioSettingValueInteger, RadioSettingValueList
)
import struct
MEM_FORMAT = """
struct {
u8 _unk1;
u8 voice: 2,
beep: 1,
_unk2: 1,
vox: 4;
u8 led_timeout: 4,
led: 2,
_unk3: 2;
u8 _unk4: 4,
sq: 4;
u8 _unk5: 2,
tot: 6;
u8 _unk6: 4,
lock_timeout: 4;
u8 _unk7: 1,
channel: 7;
u8 _unk8: 5,
bat_save: 3;
u8 _unk9[2];
u8 pass[6];
} settings;
struct {
u8 freq[5]; // 20 bit rx + 20 bit tx
u16 low_pwr: 1,
no_tx: 1,
rx_tone: 2,
rx_code: 12;
u16 nfm: 1,
skip: 1,
tx_tone: 2,
tx_code: 12;
u8 compander: 2, // only 0b00/0b01 allowed
hopping: 2, // only 0b00/0b01 allowed
scrambler: 4;
} memory[80];
"""
VOICE_LIST = ["off", "Chinese", "English"]
SCRAMBLER_LIST = ["off", "1", "2", "3", "4", "5", "6", "7", "8"]
LED_LIST = ["Low", "Medium", "High"]
BAT_SAVE_LIST = ["off", "1:1", "1:2", "1:3", "1:4"]
TONE_LIST = ["Tone", "DTCS_N", "DTCS_I", ""]
LED_TIMEOUT_LIST = ["Continuous", "5", "10", "15", "20", "25", "30",
"35", "40", "45", "50", "55", "60"]
LOCK_TIMEOUT_LIST = ["off", "5", "10", "15", "20", "25", "30",
"35", "40", "45", "50", "55", "60"]
TOT_LIST = ["off", "15", "30", "45", "60", "75", "90",
"105", "120", "135", "150", "165", "180", "195",
"210", "225", "240", "255", "270", "285",
"300", "315", "330", "345", "360", "375", "390",
"405", "420", "435", "450", "465", "480", "495",
"510", "525", "540", "555", "570", "585", "600"]
POWER_LIST = [chirp_common.PowerLevel("High", watts=2.00),
chirp_common.PowerLevel("Low", watts=0.50)]
def _checksum(data):
cs = 2
for byte in data:
cs += byte
return cs % 256
def enter_programming_mode(radio):
serial = radio.pipe
cmd = b"\x32\x31\x05\x10"
req = cmd + bytes([_checksum(cmd)])
try:
serial.write(req)
res = serial.read(1)
if res != b"\x06":
raise Exception("invalid response")
except Exception as e:
msg = "Radio refused to enter programming mode: %s" % str(e)
raise errors.RadioError(msg)
def exit_programming_mode(radio):
serial = radio.pipe
cmd = b"\x32\x31\x05\xee"
req = cmd + bytes([_checksum(cmd)])
try:
# there is no response from this command as the radio resets
serial.write(req)
except Exception:
raise errors.RadioError("Radio refused to exit programming mode")
def _read_block(radio, block_addr):
serial = radio.pipe
cmd = struct.pack(">cH", b"R", block_addr)
req = cmd + bytes([_checksum(cmd)])
try:
serial.write(req)
res_len = len(cmd) + radio.BLOCK_SIZE + 1
res = serial.read(res_len)
if len(res) != res_len or res[:len(cmd)] != cmd:
raise Exception("unexpected reply!")
if res[-1] != _checksum(res[:-1]):
raise Exception("block failed checksum!")
block_data = res[len(cmd):-1]
except Exception as e:
msg = "Failed to read block at %04x: %s" % (block_addr, str(e))
raise errors.RadioError(msg)
return block_data
def _write_block(radio, block_addr, block_data):
serial = radio.pipe
cmd = struct.pack(">cH", b"W", block_addr) + block_data
req = cmd + bytes([_checksum(cmd)])
try:
serial.write(req)
res = serial.read(1)
if res != b"\x06":
raise Exception("unexpected reply!")
except Exception as e:
msg = "Failed to write block at %04x: %s" % (block_addr, str(e))
raise errors.RadioError(msg)
def verify_model(radio):
# Simply rely on the protocol/checksum to validate the radio model
# for now: attempt at least twice, so that garbage in the line is
# ignored on the first tries
for _ in range(3):
try:
_read_block(radio, radio.START_ADDR)
return
except Exception:
pass
raise errors.RadioError("Could not communicate with the radio")
def do_download(radio):
status = chirp_common.Status()
status.msg = "Cloning from radio"
status.max = radio._memsize
verify_model(radio)
data = b""
for addr in range(radio.START_ADDR,
radio.START_ADDR + radio._memsize,
radio.BLOCK_SIZE):
status.cur = addr
radio.status_fn(status)
block = _read_block(radio, addr)
data += block
return memmap.MemoryMapBytes(data)
def do_upload(radio):
verify_model(radio)
enter_programming_mode(radio)
status = chirp_common.Status()
status.msg = "Uploading to radio"
status.max = radio._memsize
mmap = radio.get_mmap()
for addr in range(0, radio._memsize, radio.BLOCK_SIZE):
status.cur = addr
radio.status_fn(status)
block = mmap[addr:addr + radio.BLOCK_SIZE]
_write_block(radio, radio.START_ADDR + addr, block)
exit_programming_mode(radio)
def mem_to_triplet(mem_tone, mem_code):
mem_tone = TONE_LIST[mem_tone]
if mem_tone == "Tone":
mode = "Tone"
code = mem_code / 10
polarity = None
elif mem_tone in ["DTCS_N", "DTCS_I"]:
mode = "DTCS"
code = int("%o" % mem_code)
polarity = "N" if mem_tone == "DTCS_N" else "R"
else:
mode = None
code = None
polarity = None
return (mode, code, polarity)
def triplet_to_mem(tone):
mode, code, polarity = tone
if mode == "Tone":
mem_tone = "Tone"
mem_code = int(code * 10)
elif mode == "DTCS":
mem_tone = "DTCS_N" if polarity == "N" else "DTCS_I"
mem_code = int('%i' % code, 8)
else:
mem_tone = ""
mem_code = 0
mem_tone = TONE_LIST.index(mem_tone)
return (mem_tone, mem_code)
@directory.register
class KSunM6Radio(chirp_common.CloneModeRadio):
VENDOR = "KSUN"
MODEL = "M6"
BAUD_RATE = 4800
BLOCK_SIZE = 0x10
START_ADDR = 0x0050
CHANNELS = 80
_memsize = BLOCK_SIZE + 10 * CHANNELS
# 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_name = False
rf.has_settings = True
rf.memory_bounds = (1, self.CHANNELS)
rf.can_odd_split = True
rf.has_cross = True
rf.has_rx_dtcs = True
rf.valid_duplexes = ["", "split", "off"]
rf.valid_tmodes = ["", "Tone", "TSQL", "DTCS", "Cross"]
rf.valid_cross_modes = ["Tone->Tone", "DTCS->", "->DTCS", "Tone->DTCS",
"DTCS->Tone", "->Tone", "DTCS->DTCS"]
rf.has_tuning_step = False
rf.has_nostep_tuning = True
rf.valid_bands = [(400000000, 480000000)]
rf.valid_modes = ["FM", "NFM"]
rf.valid_power_levels = POWER_LIST
return rf
def get_settings(self):
settings = self._memobj.settings
basic = RadioSettingGroup("basic", "Basic Settings")
top = RadioSettings(basic)
voice = settings.voice
rsv = RadioSettingValueList(VOICE_LIST, current_index=voice)
rs = RadioSetting("voice", "Voice language", rsv)
basic.append(rs)
led = settings.led
rsv = RadioSettingValueList(LED_LIST, current_index=led)
rs = RadioSetting("led", "LED brightness", rsv)
basic.append(rs)
led_timeout = settings.led_timeout
rsv = RadioSettingValueList(LED_TIMEOUT_LIST,
current_index=led_timeout)
rs = RadioSetting("led_timeout", "LED timeout", rsv)
basic.append(rs)
lock_timeout = settings.lock_timeout
rsv = RadioSettingValueList(LOCK_TIMEOUT_LIST,
current_index=lock_timeout)
rs = RadioSetting("lock_timeout", "Key Lock timeout", rsv)
basic.append(rs)
tot = settings.tot
rsv = RadioSettingValueList(TOT_LIST, current_index=tot)
rs = RadioSetting("tot", "Time-Out Timer", rsv)
basic.append(rs)
bat_save = settings.bat_save
rsv = RadioSettingValueList(BAT_SAVE_LIST, current_index=bat_save)
rs = RadioSetting("bat_save", "Battery Save", rsv)
basic.append(rs)
rsv = RadioSettingValueInteger(0, 9, settings.sq)
rs = RadioSetting("sq", "Squelch Level", rsv)
basic.append(rs)
rsv = RadioSettingValueInteger(0, 9, settings.vox)
rs = RadioSetting("vox", "VOX Level", rsv)
basic.append(rs)
rsv = RadioSettingValueBoolean(settings.beep)
rs = RadioSetting("beep", "Beep", rsv)
basic.append(rs)
channel = settings.channel + 1
rsv = RadioSettingValueInteger(1, self.CHANNELS, channel)
rs = RadioSetting("channel", "Current Channel", rsv)
basic.append(rs)
return top
def set_settings(self, settings):
settings = settings[0]
_settings = self._memobj.settings
_settings.voice = VOICE_LIST.index(settings["voice"].value.get_value())
_settings.led = LED_LIST.index(settings["led"].value.get_value())
_settings.led_timeout = LED_TIMEOUT_LIST.index(
settings["led_timeout"].value.get_value())
_settings.lock_timeout = LOCK_TIMEOUT_LIST.index(
settings["lock_timeout"].value.get_value())
_settings.tot = TOT_LIST.index(settings["tot"].value.get_value())
_settings.bat_save = BAT_SAVE_LIST.index(
settings["bat_save"].value.get_value())
_settings.sq = settings["sq"].value.get_value()
_settings.vox = settings["vox"].value.get_value()
_settings.beep = settings["beep"].value.get_value()
_settings.channel = settings["channel"].value.get_value() - 1
# 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):
_mem = self._memobj.memory[number-1]
mem = chirp_common.Memory()
mem.number = number
if _mem.freq.get_raw() == bytes([255] * 5):
mem.empty = True
else:
rx_freq = ((_mem.freq[0] << 12)
+ (_mem.freq[1] << 4) + (_mem.freq[2] >> 4))
tx_freq = (((_mem.freq[2] & 0xF) << 16)
+ (_mem.freq[3] << 8) + _mem.freq[4])
mem.freq = rx_freq * 1000000 // 2000
mem.offset = tx_freq * 1000000 // 2000
if _mem.no_tx:
mem.duplex = "off"
elif rx_freq != tx_freq:
mem.duplex = "split"
chirp_common.split_tone_decode(
mem,
mem_to_triplet(_mem.tx_tone, _mem.tx_code),
mem_to_triplet(_mem.rx_tone, _mem.rx_code))
mem.mode = "NFM" if not mem.empty and _mem.nfm else "FM"
mem.power = POWER_LIST[int(not mem.empty and _mem.low_pwr)]
mem.skip = "S" if not mem.empty and _mem.skip else ""
mem.extra = RadioSettingGroup("Extra", "extra")
hopping = False if mem.empty else _mem.hopping
rsv = RadioSettingValueBoolean(hopping)
rs = RadioSetting("hopping", "Hopping", rsv)
mem.extra.append(rs)
compander = False if mem.empty else _mem.compander
rsv = RadioSettingValueBoolean(compander)
rs = RadioSetting("compander", "Compander", rsv)
mem.extra.append(rs)
scrambler = False if mem.empty else _mem.scrambler
rsv = RadioSettingValueList(SCRAMBLER_LIST, current_index=scrambler)
rs = RadioSetting("scrambler", _("Scrambler"), rsv)
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):
_mem = self._memobj.memory[mem.number-1]
if mem.empty:
_mem.fill_raw(b"\xff")
else:
rx_freq = mem.freq
if mem.duplex == "split" and mem.offset:
tx_freq = mem.offset
else:
tx_freq = rx_freq
rx_freq = round(rx_freq / 1000000 * 2000)
tx_freq = round(tx_freq / 1000000 * 2000)
_mem.freq[0] = (rx_freq >> 12) & 0xff
_mem.freq[1] = (rx_freq >> 4) & 0xff
_mem.freq[2] = ((rx_freq << 4) & 0xf0) | ((tx_freq >> 16) & 0x0f)
_mem.freq[3] = (tx_freq >> 8) & 0xff
_mem.freq[4] = (tx_freq) & 0xff
tx_tone, rx_tone = chirp_common.split_tone_encode(mem)
_mem.tx_tone, _mem.tx_code = triplet_to_mem(tx_tone)
_mem.rx_tone, _mem.rx_code = triplet_to_mem(rx_tone)
_mem.no_tx = (mem.duplex == "off")
_mem.nfm = (mem.mode == "NFM")
_mem.skip = (mem.skip == "S")
if mem.power in POWER_LIST:
_mem.low_pwr = POWER_LIST.index(mem.power)
else:
_mem.low_pwr = False
if "hopping" in mem.extra:
_mem.hopping = mem.extra["hopping"].value.get_value()
else:
_mem.hopping = False
if "compander" in mem.extra:
_mem.compander = mem.extra["compander"].value.get_value()
else:
_mem.compander = False
if "scrambler" in mem.extra:
_mem.scrambler = SCRAMBLER_LIST.index(
mem.extra["scrambler"].value.get_value())
else:
_mem.scrambler = 0
chirp-20261030/chirp/drivers/kyd.py 0000644 0001750 0001750 00000031731 15172631605 015771 0 ustar hibby hibby # Copyright 2014 Jim Unroe
# Copyright 2014 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 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 .
import struct
import logging
from chirp import chirp_common, directory, memmap
from chirp import bitwise, errors, util
from chirp import kenwood_tone
from chirp.settings import RadioSetting, RadioSettingGroup, \
RadioSettingValueInteger, RadioSettingValueList, \
RadioSettingValueBoolean, RadioSettings
LOG = logging.getLogger(__name__)
MEM_FORMAT = """
#seekto 0x0010;
struct {
lbcd rxfreq[4];
lbcd txfreq[4];
ul16 rxtone;
ul16 txtone;
u8 unknown1:3,
bcl:2, // Busy Lock
unknown2:3;
u8 unknown3:2,
highpower:1, // Power Level
wide:1, // Bandwidth
unknown4:4;
u8 unknown5[2];
} memory[16];
#seekto 0x012F;
struct {
u8 voice; // Voice Annunciation
u8 tot; // Time-out Timer
u8 totalert; // Time-out Timer Pre-alert
u8 unknown1[2];
u8 squelch; // Squelch Level
u8 save; // Battery Saver
u8 beep; // Beep
u8 unknown2[3];
u8 vox; // VOX Gain
u8 voxdelay; // VOX Delay
} settings;
#seekto 0x017E;
u8 skipflags[2]; // SCAN_ADD
"""
CMD_ACK = "\x06"
NC630A_POWER_LEVELS = [chirp_common.PowerLevel("Low", watts=1.00),
chirp_common.PowerLevel("High", watts=5.00)]
NC630A_DTCS = tuple(sorted(chirp_common.DTCS_CODES + (645,)))
BCL_LIST = ["Off", "Carrier", "QT/DQT"]
TIMEOUTTIMER_LIST = [""] + ["%s seconds" % x for x in range(15, 615, 15)]
TOTALERT_LIST = ["", "Off"] + ["%s seconds" % x for x in range(1, 11)]
VOICE_LIST = ["Off", "Chinese", "English"]
VOX_LIST = ["OFF"] + ["%s" % x for x in range(1, 17)]
VOXDELAY_LIST = ["0.3", "0.5", "1.0", "1.5", "2.0", "3.0"]
def _nc630a_enter_programming_mode(radio):
serial = radio.pipe
try:
serial.write("PROGRAM")
ack = serial.read(1)
except:
raise errors.RadioError("Error communicating with radio")
if not ack:
raise errors.RadioNoResponse()
elif ack != CMD_ACK:
raise errors.RadioError("Radio refused to enter programming mode")
try:
serial.write("\x02")
ident = serial.read(8)
except:
raise errors.RadioError("Error communicating with radio")
if not ident.startswith(radio._fileid):
LOG.debug(util.hexprint(ident))
raise errors.RadioError("Radio returned unknown identification string")
try:
serial.write(CMD_ACK)
ack = serial.read(1)
except:
raise errors.RadioError("Error communicating with radio")
if ack != CMD_ACK:
raise errors.RadioError("Radio refused to enter programming mode")
def _nc630a_read_block(radio, block_addr, block_size):
serial = radio.pipe
cmd = struct.pack(">cHb", 'R', block_addr, block_size)
expectedresponse = "W" + cmd[1:]
LOG.debug("Reading block %04x..." % (block_addr))
try:
serial.write(cmd)
response = serial.read(4 + block_size)
if response[:4] != expectedresponse:
raise Exception("Error reading block %04x." % (block_addr))
block_data = response[4:]
serial.write(CMD_ACK)
ack = serial.read(1)
except:
raise errors.RadioError("Failed to read block at %04x" % block_addr)
if ack != CMD_ACK:
raise Exception("No ACK reading block %04x." % (block_addr))
return block_data
def _nc630a_write_block(radio, block_addr, block_size):
serial = radio.pipe
cmd = struct.pack(">cHb", 'W', block_addr, block_size)
data = radio.get_mmap()[block_addr:block_addr + block_size]
LOG.debug("Writing Data:")
LOG.debug(util.hexprint(cmd + data))
try:
serial.write(cmd + data)
if serial.read(1) != CMD_ACK:
raise Exception("No ACK")
except:
raise errors.RadioError("Failed to send block "
"to radio at %04x" % block_addr)
def do_download(radio):
LOG.debug("download")
_nc630a_enter_programming_mode(radio)
data = ""
status = chirp_common.Status()
status.msg = "Cloning from radio"
status.cur = 0
status.max = radio._memsize
for addr in range(0, radio._memsize, radio._block_size):
status.cur = addr + radio._block_size
radio.status_fn(status)
block = _nc630a_read_block(radio, addr, radio._block_size)
data += block
LOG.debug("Address: %04x" % addr)
LOG.debug(util.hexprint(block))
return memmap.MemoryMap(data)
def do_upload(radio):
status = chirp_common.Status()
status.msg = "Uploading to radio"
_nc630a_enter_programming_mode(radio)
status.cur = 0
status.max = radio._memsize
for start_addr, end_addr in radio._ranges:
for addr in range(start_addr, end_addr, radio._block_size):
status.cur = addr + radio._block_size
radio.status_fn(status)
_nc630a_write_block(radio, addr, radio._block_size)
class MT700Alias(chirp_common.Alias):
VENDOR = "Plant-Tours"
MODEL = "MT-700"
@directory.register
class NC630aRadio(chirp_common.CloneModeRadio):
"""KYD NC-630A"""
VENDOR = "KYD"
MODEL = "NC-630A"
ALIASES = [MT700Alias]
BAUD_RATE = 9600
NEEDS_COMPAT_SERIAL = True
_ranges = [
(0x0000, 0x0330),
]
_memsize = 0x03C8
_block_size = 0x08
_fileid = b"P32073"
_tone_model = kenwood_tone.KenwoodToneModel(
dcs_base=0x2800, pol_mask=0x8000, tone_init=0xFFFF, tone_flag=0x0000)
def get_features(self):
rf = chirp_common.RadioFeatures()
rf.has_settings = True
rf.has_bank = False
rf.has_ctone = True
rf.has_cross = True
rf.has_rx_dtcs = True
rf.has_tuning_step = False
rf.can_odd_split = True
rf.has_name = False
rf.valid_skips = ["", "S"]
rf.valid_tmodes = ["", "Tone", "TSQL", "DTCS", "Cross"]
rf.valid_cross_modes = ["Tone->Tone", "Tone->DTCS", "DTCS->Tone",
"->Tone", "->DTCS", "DTCS->", "DTCS->DTCS"]
rf.valid_power_levels = NC630A_POWER_LEVELS
rf.valid_duplexes = ["", "-", "+", "split", "off"]
rf.valid_modes = ["NFM", "FM"] # 12.5 kHz, 25 kHz.
rf.memory_bounds = (1, 16)
rf.valid_tuning_steps = [2.5, 5., 6.25, 10., 12.5, 25.]
rf.valid_bands = [(400000000, 520000000)]
rf.valid_dtcs_codes = NC630A_DTCS
return rf
def process_mmap(self):
self._memobj = bitwise.parse(MEM_FORMAT, self._mmap)
def sync_in(self):
self._mmap = do_download(self)
self.process_mmap()
def sync_out(self):
do_upload(self)
def get_raw_memory(self, number):
return repr(self._memobj.memory[number - 1])
def get_memory(self, number):
bitpos = (1 << ((number - 1) % 8))
bytepos = ((number - 1) / 8)
LOG.debug("bitpos %s" % bitpos)
LOG.debug("bytepos %s" % bytepos)
_mem = self._memobj.memory[number - 1]
_skp = self._memobj.skipflags[bytepos]
mem = chirp_common.Memory()
mem.number = number
mem.freq = int(_mem.rxfreq) * 10
# We'll consider any blank (i.e. 0 MHz frequency) to be empty
if mem.freq == 0:
mem.empty = True
return mem
if _mem.rxfreq.get_raw() == b"\xFF\xFF\xFF\xFF":
mem.freq = 0
mem.empty = True
return mem
if int(_mem.rxfreq) == int(_mem.txfreq):
mem.duplex = ""
mem.offset = 0
elif _mem.txfreq.get_raw() == b"\xFF\xFF\xFF\xFF":
mem.duplex = "off"
else:
mem.duplex = int(_mem.rxfreq) > int(_mem.txfreq) and "-" or "+"
mem.offset = abs(int(_mem.rxfreq) - int(_mem.txfreq)) * 10
mem.mode = _mem.wide and "FM" or "NFM"
self._tone_model.get_tone(_mem, mem)
mem.power = NC630A_POWER_LEVELS[_mem.highpower]
mem.skip = "" if (_skp & bitpos) else "S"
LOG.debug("mem.skip %s" % mem.skip)
mem.extra = RadioSettingGroup("Extra", "extra")
rs = RadioSetting("bcl", "Busy Channel Lockout",
RadioSettingValueList(
BCL_LIST, current_index=_mem.bcl))
mem.extra.append(rs)
return mem
def set_memory(self, mem):
bitpos = (1 << ((mem.number - 1) % 8))
bytepos = ((mem.number - 1) / 8)
LOG.debug("bitpos %s" % bitpos)
LOG.debug("bytepos %s" % bytepos)
_mem = self._memobj.memory[mem.number - 1]
_skp = self._memobj.skipflags[bytepos]
if mem.empty:
_mem.fill_raw(b"\xFF")
return
_mem.set_raw(b"\x00" * 14 + b"\xFF" * 2)
_mem.rxfreq = mem.freq / 10
if mem.duplex == "off":
_mem.txfreq.fill_raw(b"\xFF")
elif mem.duplex == "split":
_mem.txfreq = mem.offset / 10
elif mem.duplex == "+":
_mem.txfreq = (mem.freq + mem.offset) / 10
elif mem.duplex == "-":
_mem.txfreq = (mem.freq - mem.offset) / 10
else:
_mem.txfreq = mem.freq / 10
_mem.wide = mem.mode == "FM"
self._tone_model.set_tone(mem, _mem)
_mem.highpower = mem.power == NC630A_POWER_LEVELS[1]
if mem.skip != "S":
_skp |= bitpos
else:
_skp &= ~bitpos
LOG.debug("_skp %s" % _skp)
for setting in mem.extra:
setattr(_mem, setting.get_name(), setting.value)
def get_settings(self):
_settings = self._memobj.settings
basic = RadioSettingGroup("basic", "Basic Settings")
top = RadioSettings(basic)
rs = RadioSetting("tot", "Time-out timer",
RadioSettingValueList(
TIMEOUTTIMER_LIST,
current_index=_settings.tot))
basic.append(rs)
rs = RadioSetting("totalert", "TOT Pre-alert",
RadioSettingValueList(
TOTALERT_LIST,
current_index=_settings.totalert))
basic.append(rs)
rs = RadioSetting("vox", "VOX Gain",
RadioSettingValueList(
VOX_LIST, current_index=_settings.vox))
basic.append(rs)
rs = RadioSetting("voice", "Voice Annumciation",
RadioSettingValueList(
VOICE_LIST, current_index=_settings.voice))
basic.append(rs)
rs = RadioSetting("squelch", "Squelch Level",
RadioSettingValueInteger(0, 9, _settings.squelch))
basic.append(rs)
rs = RadioSetting("voxdelay", "VOX Delay",
RadioSettingValueList(
VOXDELAY_LIST,
current_index=_settings.voxdelay))
basic.append(rs)
rs = RadioSetting("beep", "Beep",
RadioSettingValueBoolean(_settings.beep))
basic.append(rs)
rs = RadioSetting("save", "Battery Saver",
RadioSettingValueBoolean(_settings.save))
basic.append(rs)
return top
def set_settings(self, settings):
for element in settings:
if not isinstance(element, RadioSetting):
self.set_settings(element)
continue
else:
try:
if "." in element.get_name():
bits = element.get_name().split(".")
obj = self._memobj
for bit in bits[:-1]:
obj = getattr(obj, bit)
setting = bits[-1]
else:
obj = self._memobj.settings
setting = element.get_name()
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):
match_size = match_model = False
# testing the file data size
if len(filedata) in [0x338, 0x3C8]:
match_size = True
# testing model fingerprint
if filedata[0x01B8:0x01BE] == cls._fileid:
match_model = True
if match_size and match_model:
return True
else:
return False
chirp-20261030/chirp/drivers/kyd_IP620.py 0000644 0001750 0001750 00000046552 15172631605 016620 0 ustar hibby hibby # Copyright 2015 Lepik.stv
# based on modification of Dan Smith's and Jim Unroe original work
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 2 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see .
"""KYD IP-620 radios management module"""
# TODO: Power on message
# TODO: Channel name
# TODO: Tuning step
import struct
import time
import logging
from chirp import util, chirp_common, bitwise, memmap, errors, directory
from chirp import kenwood_tone
from chirp.settings import RadioSetting, RadioSettingGroup, \
RadioSettingValueList, RadioSettings
LOG = logging.getLogger(__name__)
IP620_MEM_FORMAT = """
#seekto 0x0000;
struct { // Channel memory structure
lbcd rx_freq[4]; // RX frequency
lbcd tx_freq[4]; // TX frequency
ul16 rxtone; // RX tone
ul16 txtone; // TX tone
u8 unknown_1:4, // n-a
busy_loc:2, // NO-00, Carrier wave-01, SM-10
n_a:2; // n-a
u8 unknown_2:1, // n-a
scan_add:1, // Scan add
n_a2:1, // n-a
w_n:1, // Narrow-0 Wide-1
lout:1, // LOCKOUT OFF-0 ON-1
n_a_:1, // n-a
power:2; // Power low-00 middle-01 high-10
u8 unknown_3; // n-a
u8 unknown_4; // n-a
} memory[200];
#seekto 0x1000;
struct {
u8 chan_name[6]; //Channel name
u8 unknown_1[10];
} chan_names[200];
#seekto 0x0C80;
struct { // Settings memory structure ( A-Frequency mode )
lbcd freq_a_rx[4];
lbcd freq_a_tx[4];
ul16 freq_a_rx_tone; // RX tone
ul16 freq_a_tx_tone; // TX tone
u8 unknown_1_5:4,
freq_a_busy_loc:2,
n_a:2;
u8 unknown_1_6:3,
freq_a_w_n:1,
n_a2:1,
na:1,
freq_a_power:2;
u8 unknown_1_7;
u8 unknown_1_8;
} settings_freq_a;
#seekto 0x0E20;
struct {
u8 chan_disp_way; // Channel display way
u8 step_freq; // Step frequency kHz
u8 rf_sql; // Squelch level
u8 bat_save; // Battery Saver
u8 chan_pri; // Channel PRI
u8 end_beep; // End beep
u8 tot; // Time-out timer
u8 vox; // VOX Gain
u8 chan_pri_num; // Channel PRI time Sec
u8 n_a_2;
u8 ch_mode; // CH mode
u8 n_a_3;
u8 call_tone; // Call tone
u8 beep; // Beep
u8 unknown_1_1[2];
u8 unknown_1_2[8];
u8 scan_rev; // Scan rev
u8 unknown_1_3[2];
u8 enc; // Frequency lock
u8 vox_dly; // VOX Delay
u8 wait_back_light;// Wait back light
u8 unknown_1_4[2];
} settings;
#seekto 0x0E40;
struct {
u8 fm_radio; // FM radio
u8 auto_lock; // Auto lock
u8 unknown_1[8];
u8 pon_msg[6]; //Power on msg
} settings_misc;
#seekto 0x1C80;
struct {
u8 unknown_1[16];
u8 unknown_2[16];
} settings_radio_3;
"""
CMD_ACK = "\x06"
WRITE_BLOCK_SIZE = 0x10
READ_BLOCK_SIZE = 0x40
CHAR_LENGTH_MAX = 6
OFF_ON_LIST = ["OFF", "ON"]
ON_OFF_LIST = ["ON", "OFF"]
NO_YES_LIST = ["NO", "YES"]
STEP_LIST = ["5.0", "6.25", "10.0", "12.5", "25.0"]
BAT_SAVE_LIST = ["OFF", "0.2 Sec", "0.4 Sec", "0.6 Sec", "0.8 Sec", "1.0 Sec"]
SHIFT_LIST = ["", "-", "+"]
SCANM_LIST = ["Time", "Carrier wave", "Search"]
ENDBEEP_LIST = ["OFF", "Begin", "End", "Begin/End"]
POWER_LEVELS = [chirp_common.PowerLevel("Low", watts=1.00), chirp_common.PowerLevel("Medium", watts=2.50), chirp_common.PowerLevel("High", watts=5.00)]
TIMEOUT_LIST = ["OFF", "1 Min", "3 Min", "10 Min"]
TOTALERT_LIST = ["", "OFF"] + ["%s seconds" % x for x in range(1, 11)]
VOX_LIST = ["OFF"] + ["%s" % x for x in range(1, 17)]
VOXDELAY_LIST = ["0.3 Sec", "0.5 Sec", "1.0 Sec", "1.5 Sec", "2.0 Sec", "3.0 Sec", "4.0 Sec", "5.0 Sec"]
PRI_NUM = [3, 5, 8, 10]
PRI_NUM_LIST = [str(x) for x in PRI_NUM]
CH_FLAG_LIST = ["Channel+Freq", "Channel+Name"]
BACKLIGHT_LIST = ["Always Off", "Auto", "Always On"]
BUSYLOCK_LIST = ["NO", "Carrier", "SM"]
KEYBLOCK_LIST = ["Manual", "Auto"]
CALLTONE_LIST = ["OFF", "1", "2", "3", "4", "5", "6", "7", "8", "1750"]
RFSQL_LIST = ["OFF", "S-1", "S-2", "S-3", "S-4", "S-5", "S-6", "S-7", "S-8", "S-FULL"]
IP620_CHARSET = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ?+-* "
IP620_BANDS = [
(136000000, 174000000),
(200000000, 260000000),
(300000000, 340000000), # <--- this band supports only Russian model (ARGUT A-36)
(350000000, 390000000),
(400000000, 480000000),
(420000000, 510000000),
(450000000, 520000000),
]
@directory.register
class IP620Radio(chirp_common.CloneModeRadio,
chirp_common.ExperimentalRadio):
"""KYD IP-620"""
VENDOR = "KYD"
MODEL = "IP-620"
BAUD_RATE = 9600
NEEDS_COMPAT_SERIAL = True
_ranges = [
(0x0000, 0x2000),
]
_memsize = 0x2000
_tone_model = kenwood_tone.KenwoodToneModel(
dcs_base=0x2800, pol_mask=0x8000, tone_init=0xFFFF, tone_flag=0x0000)
@classmethod
def match_model(cls, filedata, filename):
return len(filedata) == cls._memsize and \
filedata[0xF7E:0xF80] == b"\x01\xE2"
def _ip620_exit_programming_mode(self):
try:
self.pipe.write("\x06")
except errors.RadioError:
raise
except Exception as e:
raise errors.RadioError("Radio refused to exit programming mode: %s" % e)
def _ip620_enter_programming_mode(self):
try:
self.pipe.write("iUHOUXUN")
self.pipe.write("\x02")
time.sleep(0.2)
_ack = self.pipe.read(1)
except errors.RadioError:
raise
except Exception as e:
raise errors.RadioError("Error communicating with radio: %s" % e)
if not _ack:
raise errors.RadioNoResponse()
elif _ack != CMD_ACK:
raise errors.RadioError("Radio refused to enter programming mode")
try:
self.pipe.write("\x02")
_ident = self.pipe.read(8)
except errors.RadioError:
raise
except Exception as e:
raise errors.RadioError("Error communicating with radio: %s" % e)
if not _ident.startswith("\x06\x4B\x47\x36\x37\x01\x56\xF8"):
print(util.hexprint(_ident))
raise errors.RadioError("Radio returned unknown identification string")
try:
self.pipe.write(CMD_ACK)
_ack = self.pipe.read(1)
except errors.RadioError:
raise
except Exception as e:
raise errors.RadioError("Error communicating with radio: %s" % e)
if _ack != CMD_ACK:
raise errors.RadioError("Radio refused to enter programming mode")
def _ip620_write_block(self, block_addr):
_cmd = struct.pack(">cHb", 'W', block_addr, WRITE_BLOCK_SIZE)
_data = self.get_mmap()[block_addr:block_addr + WRITE_BLOCK_SIZE]
LOG.debug("Writing Data:")
LOG.debug(util.hexprint(_cmd + _data))
try:
self.pipe.write(_cmd + _data)
if self.pipe.read(1) != CMD_ACK:
raise Exception("No ACK")
except:
raise errors.RadioError("Failed to send block "
"to radio at %04x" % block_addr)
def _ip620_read_block(self, block_addr):
_cmd = struct.pack(">cHb", 'R', block_addr, READ_BLOCK_SIZE)
_expectedresponse = "W" + _cmd[1:]
LOG.debug("Reading block %04x..." % (block_addr))
try:
self.pipe.write(_cmd)
_response = self.pipe.read(4 + READ_BLOCK_SIZE)
if _response[:4] != _expectedresponse:
raise Exception("Error reading block %04x." % (block_addr))
_block_data = _response[4:]
self.pipe.write(CMD_ACK)
_ack = self.pipe.read(1)
except:
raise errors.RadioError("Failed to read block at %04x" % block_addr)
if _ack != CMD_ACK:
raise Exception("No ACK reading block %04x." % (block_addr))
return _block_data
def _do_download(self):
self._ip620_enter_programming_mode()
_data = ""
_status = chirp_common.Status()
_status.msg = "Cloning from radio"
_status.cur = 0
_status.max = self._memsize
for _addr in range(0, self._memsize, READ_BLOCK_SIZE):
_status.cur = _addr + READ_BLOCK_SIZE
self.status_fn(_status)
_block = self._ip620_read_block(_addr)
_data += _block
LOG.debug("Address: %04x" % _addr)
LOG.debug(util.hexprint(_block))
self._ip620_exit_programming_mode()
return memmap.MemoryMap(_data)
def _do_upload(self):
_status = chirp_common.Status()
_status.msg = "Uploading to radio"
self._ip620_enter_programming_mode()
_status.cur = 0
_status.max = self._memsize
for _start_addr, _end_addr in self._ranges:
for _addr in range(_start_addr, _end_addr, WRITE_BLOCK_SIZE):
_status.cur = _addr + WRITE_BLOCK_SIZE
self.status_fn(_status)
self._ip620_write_block(_addr)
self._ip620_exit_programming_mode()
@classmethod
def get_prompts(cls):
rp = chirp_common.RadioPrompts()
rp.experimental = ("This radio driver is currently under development. "
"There are no known issues with it, but you should "
"proceed with caution. However, proceed at your own risk!")
return rp
def get_features(self):
rf = chirp_common.RadioFeatures()
rf.has_settings = True
rf.has_bank = False
rf.has_ctone = True
rf.has_cross = False
rf.has_rx_dtcs = True
rf.has_tuning_step = False
rf.can_odd_split = False
rf.has_name = False
rf.valid_skips = []
rf.valid_tmodes = ["", "Tone", "TSQL", "DTCS"]
rf.valid_power_levels = POWER_LEVELS
rf.valid_duplexes = SHIFT_LIST
rf.valid_modes = ["FM", "NFM"]
rf.memory_bounds = (1, 200)
rf.valid_bands = IP620_BANDS
rf.valid_characters = ''.join(set(IP620_CHARSET))
rf.valid_name_length = CHAR_LENGTH_MAX
return rf
def process_mmap(self):
self._memobj = bitwise.parse(IP620_MEM_FORMAT, self._mmap)
def sync_in(self):
try:
self._mmap = self._do_download()
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):
self._do_upload()
def get_raw_memory(self, number):
return repr(self._memobj.memory[number - 1])
def get_memory(self, number):
_mem = self._memobj.memory[number - 1]
_nam = self._memobj.chan_names[number - 1]
def _is_empty():
if _mem.rx_freq.get_raw() != b"\xFF\xFF\xFF\xFF":
return False
return True
mem = chirp_common.Memory()
mem.number = number
if _is_empty():
mem.empty = True
return mem
mem.freq = int(_mem.rx_freq) * 10
if int(_mem.rx_freq) == int(_mem.tx_freq):
mem.duplex = ""
mem.offset = 0
elif _mem.tx_freq.get_raw() == b"\xFF\xFF\xFF\xFF":
mem.duplex = "off"
else:
mem.duplex = int(_mem.rx_freq) > int(_mem.tx_freq) and "-" or "+"
mem.offset = abs(int(_mem.rx_freq) - int(_mem.tx_freq)) * 10
mem.mode = _mem.w_n and "FM" or "NFM"
self._tone_model.get_tone(_mem, mem)
mem.power = POWER_LEVELS[_mem.power]
mem.extra = RadioSettingGroup("Extra", "extra")
rs = RadioSetting("lout", "Lock out",
RadioSettingValueList(OFF_ON_LIST,
current_index=_mem.lout))
mem.extra.append(rs)
rs = RadioSetting("busy_loc", "Busy lock",
RadioSettingValueList(BUSYLOCK_LIST,
current_index=_mem.busy_loc))
mem.extra.append(rs)
rs = RadioSetting("scan_add", "Scan add",
RadioSettingValueList(NO_YES_LIST,
current_index=_mem.scan_add))
mem.extra.append(rs)
# TODO: Show name channel
# count = 0
# for i in _nam.chan_name:
# if i == 0xFF:
# break
# try:
# mem.name += IP620_CHARSET[i]
# except Exception:
# LOG.error("Unknown name char %i: 0x%02x (mem %i)" %
# (count, i, number - 1))
# mem.name += " "
# count += 1
# mem.name = mem.name.rstrip()
return mem
def set_memory(self, mem):
_mem = self._memobj.memory[mem.number - 1]
if mem.empty:
_mem.fill_raw(b"\xFF")
return
_mem.rx_freq = mem.freq / 10
if mem.duplex == "off":
_mem.tx_freq.fill_raw(b"\xFF")
elif mem.duplex == "+":
_mem.tx_freq = (mem.freq + mem.offset) / 10
elif mem.duplex == "-":
_mem.tx_freq = (mem.freq - mem.offset) / 10
else:
_mem.tx_freq = mem.freq / 10
_mem.w_n = mem.mode == "FM"
self._tone_model.set_tone(mem, _mem)
_mem.power = mem.power == POWER_LEVELS[1]
for setting in ('lout', 'busy_loc', 'scan_add'):
setattr(_mem, setting, 0)
for setting in mem.extra:
setattr(_mem, setting.get_name(), setting.value)
def get_settings(self):
_settings = self._memobj.settings
_settings_misc = self._memobj.settings_misc
basic = RadioSettingGroup("basic", "Basic Settings")
top = RadioSettings(basic)
rs = RadioSetting("rf_sql", "Squelch level (SQL)",
RadioSettingValueList(RFSQL_LIST,
current_index=_settings.rf_sql))
basic.append(rs)
rs = RadioSetting("step_freq", "Step frequency kHz (STP)",
RadioSettingValueList(STEP_LIST,
current_index=_settings.step_freq))
basic.append(rs)
rs = RadioSetting("fm_radio", "FM radio (DW)",
RadioSettingValueList(OFF_ON_LIST,
current_index=_settings_misc.fm_radio))
basic.append(rs)
rs = RadioSetting("call_tone", "Call tone (CK)",
RadioSettingValueList(CALLTONE_LIST,
current_index=_settings.call_tone))
basic.append(rs)
rs = RadioSetting("tot", "Time-out timer (TOT)",
RadioSettingValueList(TIMEOUT_LIST,
current_index=_settings.tot))
basic.append(rs)
rs = RadioSetting("chan_disp_way", "Channel display way",
RadioSettingValueList(CH_FLAG_LIST,
current_index=_settings.chan_disp_way))
basic.append(rs)
rs = RadioSetting("vox", "VOX Gain (VOX)",
RadioSettingValueList(VOX_LIST,
current_index=_settings.vox))
basic.append(rs)
rs = RadioSetting("vox_dly", "VOX Delay",
RadioSettingValueList(VOXDELAY_LIST,
current_index=_settings.vox_dly))
basic.append(rs)
rs = RadioSetting("beep", "Beep (BP)",
RadioSettingValueList(OFF_ON_LIST,
current_index=_settings.beep))
basic.append(rs)
rs = RadioSetting("auto_lock", "Auto lock (KY)",
RadioSettingValueList(NO_YES_LIST,
current_index=_settings_misc.auto_lock))
basic.append(rs)
rs = RadioSetting("bat_save", "Battery Saver (SAV)",
RadioSettingValueList(BAT_SAVE_LIST,
current_index=_settings.bat_save))
basic.append(rs)
rs = RadioSetting("chan_pri", "Channel PRI (PRI)",
RadioSettingValueList(OFF_ON_LIST,
current_index=_settings.chan_pri))
basic.append(rs)
rs = RadioSetting("chan_pri_num", "Channel PRI time Sec (PRI)",
RadioSettingValueList(PRI_NUM_LIST,
current_index=_settings.chan_pri_num))
basic.append(rs)
rs = RadioSetting("end_beep", "End beep (ET)",
RadioSettingValueList(ENDBEEP_LIST,
current_index=_settings.end_beep))
basic.append(rs)
rs = RadioSetting("ch_mode", "CH mode",
RadioSettingValueList(ON_OFF_LIST,
current_index=_settings.ch_mode))
basic.append(rs)
rs = RadioSetting("scan_rev", "Scan rev (SCAN)",
RadioSettingValueList(SCANM_LIST,
current_index=_settings.scan_rev))
basic.append(rs)
rs = RadioSetting("enc", "Frequency lock (ENC)",
RadioSettingValueList(OFF_ON_LIST,
current_index=_settings.enc))
basic.append(rs)
rs = RadioSetting("wait_back_light", "Wait back light (LED)",
RadioSettingValueList(BACKLIGHT_LIST,
current_index=_settings.wait_back_light))
basic.append(rs)
return top
def _set_misc_settings(self, settings):
for element in settings:
try:
setattr(self._memobj.settings_misc,
element.get_name(),
element.value)
except Exception:
LOG.debug(element.get_name())
raise
def set_settings(self, settings):
_settings = self._memobj.settings
_settings_misc = self._memobj.settings_misc
for element in settings:
if not isinstance(element, RadioSetting):
self.set_settings(element)
continue
if not element.changed():
continue
try:
setting = element.get_name()
if setting in ["auto_lock", "fm_radio"]:
oldval = getattr(_settings_misc, setting)
else:
oldval = getattr(_settings, setting)
newval = element.value
LOG.debug("Setting %s(%s) <= %s" % (setting, oldval, newval))
if setting in ["auto_lock", "fm_radio"]:
setattr(_settings_misc, setting, newval)
else:
setattr(_settings, setting, newval)
except Exception:
LOG.debug(element.get_name())
raise
chirp-20261030/chirp/drivers/leixen.py 0000644 0001750 0001750 00000113117 15266622247 016473 0 ustar hibby hibby # 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 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 .
import struct
import logging
from chirp import chirp_common, directory, memmap, errors, util, checksum
from chirp import bitwise
from chirp.settings import RadioSetting, RadioSettingGroup, \
RadioSettingValueInteger, RadioSettingValueList, \
RadioSettingValueBoolean, RadioSettingValueString, \
RadioSettings
LOG = logging.getLogger(__name__)
MEM_FORMAT = """
#seekto 0x0184;
struct {
u8 unknown1:4,
sql:4; // squelch level
u8 unknown2;
u8 obeep:1, // open beep
dw_off:1, // dual watch (inverted)
kbeep:1, // key beep
rbeep:1, // roger beep
unknown3:2,
ctdcsb:1, // ct/dcs busy lock
unknown4:1;
u8 alarm:1, // alarm key
unknown5:1,
aliasen_off:1, // alias enable (inverted)
save:1, // battery save
unknown6:2,
mrcha:1, // mr/cha
vfomr:1; // vfo/mr
u8 keylock_off:1, // key lock (inverted)
txstop_off:1, // tx stop (inverted)
scanm:1, // scan key/mode
vir:1, // vox inhibit on receive
keylockm:2, // key lock mode
lamp:2; // backlight
u8 opendis:2, // open display
fmen_off:1, // fm enable (inverted)
unknown7:1,
fmscan_off:1, // fm scan (inverted)
fmdw:1, // fm dual watch
unknown8:2;
u8 step:4, // step
vol:4; // volume
u8 apo:4, // auto power off
tot:4; // time out timer
u8 unknown9;
u8 voxdt:4, // vox delay time
voxgain:4; // vox gain
u8 unknown10;
u8 unknown11;
u8 unknown12:3,
lptime:5; // long press time
u8 keyp2long:4, // p2 key long press
keyp2short:4; // p2 key short press
u8 keyp1long:4, // p1 key long press
keyp1short:4; // p1 key short press
u8 keyp3long:4, // p3 key long press
keyp3short:4; // p3 key short press
u8 unknown13;
u8 menuen:1, // menu enable
absel:1, // a/b select
unknown14:2,
keymshort:4; // m key short press
u8 unknown15:4,
dtmfst:1, // dtmf sidetone
ackdecode:1, // ack decode
monitor:2; // monitor
u8 unknown16:3,
reset:1, // reset enable
unknown17:1,
keypadmic_off:1, // keypad mic (inverted)
unknown18:2;
u8 unknown19;
u8 unknown20:3,
dtmftime:5; // dtmf digit time
u8 unknown21:3,
dtmfspace:5; // dtmf digit space time
u8 unknown22:2,
dtmfdelay:6; // dtmf first digit delay
u8 unknown23:1,
dtmfpretime:7; // dtmf pretime
u8 unknown24:2,
dtmfdelay2:6; // dtmf * and # digit delay
u8 unknown25:3,
smfont_off:1, // small font (inverted)
unknown26:4;
} settings;
#seekto 0x01cd;
struct {
u8 rssi136; // squelch base level (vhf)
u8 unknown0x01ce;
u8 rssi400; // squelch base level (uhf)
} service;
#seekto 0x0900;
struct {
char user1[7]; // user message 1
char unknown0x0907;
char unknown0x0908[8];
char unknown0x0910[8];
char system[7]; // system message
char unknown0x091F;
char user2[7]; // user message 2
char unknown0x0927;
} messages;
struct channel {
bbcd rx_freq[4];
bbcd tx_freq[4];
u8 rx_tone;
u8 rx_tmode_extra:6,
rx_tmode:2;
u8 tx_tone;
u8 tx_tmode_extra:6,
tx_tmode:2;
u8 unknown5;
u8 pttidoff:1,
dtmfoff:1,
%(unknownormode)s,
tailcut:1,
aliasop:1,
talkaroundoff:1,
voxoff:1,
skip:1;
u8 %(modeorpower)s,
reverseoff:1,
blckoff:1,
unknown7:1,
apro:3;
u8 unknown8;
};
struct name {
char name[7];
u8 pad;
};
#seekto 0x%(chanstart)x;
struct channel default[%(defaults)i];
struct channel memory[199];
#seekto 0x%(namestart)x;
struct name defaultname[%(defaults)i];
struct name name[199];
"""
APO_LIST = ["OFF", "10M", "20M", "30M", "40M", "50M", "60M", "90M",
"2H", "4H", "6H", "8H", "10H", "12H", "14H", "16H"]
SQL_LIST = ["%s" % x for x in range(0, 10)]
SCANM_LIST = ["CO", "TO"]
TOT_LIST = ["OFF"] + ["%s seconds" % x for x in range(10, 130, 10)]
_STEP_LIST = [2.5, 5., 6.25, 10., 12.5, 25.]
STEP_LIST = ["{} kHz".format(x) for x in _STEP_LIST]
MONITOR_LIST = ["CTC/DCS", "DTMF", "CTC/DCS and DTMF", "CTC/DCS or DTMF"]
VFOMR_LIST = ["MR", "VFO"]
MRCHA_LIST = ["MR CHA", "Freq. MR"]
VOL_LIST = ["OFF"] + ["%s" % x for x in range(1, 16)]
OPENDIS_LIST = ["All", "Lease Time", "User-defined", "Leixen"]
LAMP_LIST = ["OFF", "KEY", "CONT"]
KEYLOCKM_LIST = ["K+S", "PTT", "KEY", "ALL"]
ABSEL_LIST = ["B Channel", "A Channel"]
VOXGAIN_LIST = ["%s" % x for x in range(1, 9)]
VOXDT_LIST = ["%s seconds" % x for x in range(1, 5)]
DTMFTIME_LIST = ["%i milliseconds" % x for x in range(50, 210, 10)]
DTMFDELAY_LIST = ["%i milliseconds" % x for x in range(0, 550, 50)]
DTMFPRETIME_LIST = ["%i milliseconds" % x for x in range(100, 1100, 100)]
DTMFDELAY2_LIST = ["%i milliseconds" % x for x in range(0, 450, 50)]
LPTIME_LIST = ["%i milliseconds" % x for x in range(500, 2600, 100)]
PFKEYLONG_LIST = ["OFF",
"FM",
"Monitor Momentary",
"Monitor Lock",
"SQ Off Momentary",
"Mute",
"SCAN",
"TX Power",
"EMG",
"VFO/MR",
"DTMF",
"CALL",
"Transmit 1750 Hz",
"A/B",
"Talk Around",
"Reverse"
]
PFKEYSHORT_LIST = ["OFF",
"FM",
"BandChange",
"Time",
"Monitor Lock",
"Mute",
"SCAN",
"TX Power",
"EMG",
"VFO/MR",
"DTMF",
"CALL",
"Transmit 1750 Hz",
"A/B",
"Talk Around",
"Reverse"
]
MODES = ["NFM", "FM"]
WTFTONES = tuple(float(x) for x in range(56, 64))
TONES = tuple(sorted(WTFTONES + chirp_common.TONES))
DTCS_CODES = tuple(sorted((17, 50, 645) + chirp_common.DTCS_CODES))
TMODES = ["", "Tone", "DTCS", "DTCS"]
def _image_ident_from_data(data):
return data[0x168:0x178]
def _image_ident_from_image(radio):
return _image_ident_from_data(radio.get_mmap())
def make_frame(cmd, addr, data=b""):
payload = struct.pack(">H", addr) + data
header = struct.pack(">cB", cmd, len(payload))
frame = header + payload
return frame + bytes([checksum.checksum_xor(frame)])
def send(radio, frame):
# LOG.debug("%04i P>R: %s" %
# (len(frame),
# util.hexprint(frame).replace("\n", "\n ")))
try:
radio.pipe.write(frame)
except Exception as e:
raise errors.RadioError("Failed to communicate with radio: %s" % e)
def recv(radio, readdata=True):
hdr = radio.pipe.read(4)
# LOG.debug("%04i PBBH", hdr)
length -= 2
if readdata:
data = radio.pipe.read(length)
# LOG.debug(" PTone",
"DTCS->",
"->DTCS",
"Tone->DTCS",
"DTCS->Tone",
"->Tone",
"DTCS->DTCS"]
rf.valid_characters = chirp_common.CHARSET_ASCII
rf.valid_name_length = 7
rf.valid_power_levels = self._power_levels
rf.valid_duplexes = ["", "-", "+", "split", "off"]
rf.valid_skips = ["", "S"]
rf.valid_tuning_steps = _STEP_LIST
rf.valid_bands = [(136000000, 174000001),
(400000000, 480000000)]
rf.valid_tones = TONES
rf.valid_dtcs_codes = DTCS_CODES
rf.memory_bounds = (1, 199)
return rf
def sync_in(self):
try:
self._mmap = do_download(self)
except Exception as e:
finish(self)
raise errors.RadioError("Failed to download from radio: %s" % e)
self.process_mmap()
def process_mmap(self):
self._memobj = bitwise.parse(
MEM_FORMAT % self._mem_formatter, self._mmap)
def sync_out(self):
try:
do_upload(self)
except errors.RadioError:
finish(self)
raise
except Exception as e:
raise errors.RadioError("Failed to upload to radio: %s" % e)
def get_raw_memory(self, number):
name, mem = self._get_memobjs(number)
return repr(name) + repr(mem)
def _get_tone(self, mem, _mem):
rx_tone = tx_tone = None
tx_tmode = TMODES[_mem.tx_tmode]
rx_tmode = TMODES[_mem.rx_tmode]
if tx_tmode == "Tone":
tx_tone = TONES[_mem.tx_tone - 1]
elif tx_tmode == "DTCS":
tx_tone = DTCS_CODES[_mem.tx_tone - 1]
if rx_tmode == "Tone":
rx_tone = TONES[_mem.rx_tone - 1]
elif rx_tmode == "DTCS":
rx_tone = DTCS_CODES[_mem.rx_tone - 1]
tx_pol = _mem.tx_tmode == 0x03 and "R" or "N"
rx_pol = _mem.rx_tmode == 0x03 and "R" or "N"
chirp_common.split_tone_decode(mem, (tx_tmode, tx_tone, tx_pol),
(rx_tmode, rx_tone, rx_pol))
def _is_txinh(self, _mem):
raw_tx = b""
for i in range(0, 4):
raw_tx += _mem.tx_freq[i].get_raw()
return raw_tx == b"\xFF\xFF\xFF\xFF"
def _get_memobjs(self, number):
_mem = self._memobj.memory[number - 1]
_name = self._memobj.name[number - 1]
return _mem, _name
def get_memory(self, number):
_mem, _name = self._get_memobjs(number)
mem = chirp_common.Memory()
mem.number = number
if _mem.get_raw()[:4] == b"\xFF\xFF\xFF\xFF":
mem.empty = True
return mem
mem.freq = int(_mem.rx_freq) * 10
if self._is_txinh(_mem):
mem.duplex = "off"
mem.offset = 0
elif int(_mem.rx_freq) == int(_mem.tx_freq):
mem.duplex = ""
mem.offset = 0
elif abs(int(_mem.rx_freq) * 10 - int(_mem.tx_freq) * 10) > 70000000:
mem.duplex = "split"
mem.offset = int(_mem.tx_freq) * 10
else:
mem.duplex = int(_mem.rx_freq) > int(_mem.tx_freq) and "-" or "+"
mem.offset = abs(int(_mem.rx_freq) - int(_mem.tx_freq)) * 10
mem.name = str(_name.name).rstrip()
self._get_tone(mem, _mem)
mem.mode = MODES[_mem.mode]
powerindex = _mem.power if _mem.power < len(self._power_levels) else -1
mem.power = self._power_levels[powerindex]
mem.skip = _mem.skip and "S" or ""
mem.extra = RadioSettingGroup("Extra", "extra")
opts = ["On", "Off"]
rs = RadioSetting("blckoff", "Busy Channel Lockout",
RadioSettingValueList(
opts, current_index=_mem.blckoff))
mem.extra.append(rs)
opts = ["Off", "On"]
rs = RadioSetting("tailcut", "Squelch Tail Elimination",
RadioSettingValueList(
opts, current_index=_mem.tailcut))
mem.extra.append(rs)
apro = _mem.apro if _mem.apro < 0x5 else 0
opts = ["Off", "Compander", "Scrambler", "TX Scrambler",
"RX Scrambler"]
rs = RadioSetting("apro", "Audio Processing",
RadioSettingValueList(
opts, current_index=apro))
mem.extra.append(rs)
opts = ["On", "Off"]
rs = RadioSetting("voxoff", "VOX",
RadioSettingValueList(
opts, current_index=_mem.voxoff))
mem.extra.append(rs)
opts = ["On", "Off"]
rs = RadioSetting("pttidoff", "PTT ID",
RadioSettingValueList(
opts, current_index=_mem.pttidoff))
mem.extra.append(rs)
opts = ["On", "Off"]
rs = RadioSetting("dtmfoff", "DTMF",
RadioSettingValueList(
opts, current_index=_mem.dtmfoff))
mem.extra.append(rs)
opts = ["Name", "Frequency"]
aliasop = RadioSetting("aliasop", "Display",
RadioSettingValueList(
opts, current_index=_mem.aliasop))
mem.extra.append(aliasop)
opts = ["On", "Off"]
rs = RadioSetting("reverseoff", "Reverse Frequency",
RadioSettingValueList(
opts, current_index=_mem.reverseoff))
mem.extra.append(rs)
opts = ["On", "Off"]
rs = RadioSetting("talkaroundoff", "Talk Around",
RadioSettingValueList(
opts, current_index=_mem.talkaroundoff))
mem.extra.append(rs)
return mem
def _set_tone(self, mem, _mem):
((txmode, txtone, txpol),
(rxmode, rxtone, rxpol)) = chirp_common.split_tone_encode(mem)
_mem.tx_tmode = TMODES.index(txmode)
_mem.rx_tmode = TMODES.index(rxmode)
if txmode == "Tone":
_mem.tx_tone = TONES.index(txtone) + 1
elif txmode == "DTCS":
_mem.tx_tmode = txpol == "R" and 0x03 or 0x02
_mem.tx_tone = DTCS_CODES.index(txtone) + 1
if rxmode == "Tone":
_mem.rx_tone = TONES.index(rxtone) + 1
elif rxmode == "DTCS":
_mem.rx_tmode = rxpol == "R" and 0x03 or 0x02
_mem.rx_tone = DTCS_CODES.index(rxtone) + 1
def set_memory(self, mem):
_mem, _name = self._get_memobjs(mem.number)
if mem.empty:
_mem.set_raw(b"\xFF" * 16)
return
elif _mem.get_raw() == (b"\xFF" * 16):
_mem.set_raw(b"\xFF" * 8 + b"\xFF\x00\xFF\x00\xFF\xFE\xF0\xFC")
_mem.rx_freq = mem.freq / 10
if mem.duplex == "off":
for i in range(0, 4):
_mem.tx_freq[i].set_raw(b"\xFF")
elif mem.duplex == "split":
_mem.tx_freq = mem.offset / 10
elif mem.duplex == "+":
_mem.tx_freq = (mem.freq + mem.offset) / 10
elif mem.duplex == "-":
_mem.tx_freq = (mem.freq - mem.offset) / 10
else:
_mem.tx_freq = mem.freq / 10
self._set_tone(mem, _mem)
_mem.power = mem.power and self._power_levels.index(mem.power) or 0
_mem.mode = MODES.index(mem.mode)
_mem.skip = mem.skip == "S"
_name.name = mem.name.ljust(7)
# autoset display to name if filled, else show frequency
if mem.extra:
# mem.extra only seems to be populated when called from edit panel
aliasop = mem.extra["aliasop"]
else:
aliasop = None
if mem.name:
_mem.aliasop = False
else:
_mem.aliasop = True
for setting in mem.extra:
setattr(_mem, setting.get_name(), setting.value)
def _get_settings(self):
_settings = self._memobj.settings
_service = self._memobj.service
_msg = self._memobj.messages
cfg_grp = RadioSettingGroup("cfg_grp", "Basic Settings")
adv_grp = RadioSettingGroup("adv_grp", "Advanced Settings")
key_grp = RadioSettingGroup("key_grp", "Key Assignment")
group = RadioSettings(cfg_grp, adv_grp, key_grp)
#
# Basic Settings
#
rs = RadioSetting("apo", "Auto Power Off",
RadioSettingValueList(
APO_LIST, current_index=_settings.apo))
cfg_grp.append(rs)
rs = RadioSetting("sql", "Squelch Level",
RadioSettingValueList(
SQL_LIST, current_index=_settings.sql))
cfg_grp.append(rs)
rs = RadioSetting("scanm", "Scan Mode",
RadioSettingValueList(
SCANM_LIST, current_index=_settings.scanm))
cfg_grp.append(rs)
rs = RadioSetting("tot", "Time Out Timer",
RadioSettingValueList(
TOT_LIST, current_index=_settings.tot))
cfg_grp.append(rs)
rs = RadioSetting("step", "Step",
RadioSettingValueList(
STEP_LIST, current_index=_settings.step))
cfg_grp.append(rs)
rs = RadioSetting("monitor", "Monitor",
RadioSettingValueList(
MONITOR_LIST, current_index=_settings.monitor))
cfg_grp.append(rs)
rs = RadioSetting("vfomr", "VFO/MR",
RadioSettingValueList(
VFOMR_LIST, current_index=_settings.vfomr))
cfg_grp.append(rs)
rs = RadioSetting("mrcha", "MR/CHA",
RadioSettingValueList(
MRCHA_LIST, current_index=_settings.mrcha))
cfg_grp.append(rs)
rs = RadioSetting("vol", "Volume",
RadioSettingValueList(
VOL_LIST, current_index=_settings.vol))
cfg_grp.append(rs)
rs = RadioSetting("opendis", "Open Display",
RadioSettingValueList(
OPENDIS_LIST, current_index=_settings.opendis))
cfg_grp.append(rs)
def _filter(name):
filtered = ""
for char in str(name):
if char in chirp_common.CHARSET_ASCII:
filtered += char
else:
filtered += " "
LOG.debug("Filtered: %s" % filtered)
return filtered
rs = RadioSetting("messages.user1", "User-defined Message 1",
RadioSettingValueString(0, 7, _filter(_msg.user1)))
cfg_grp.append(rs)
rs = RadioSetting("messages.user2", "User-defined Message 2",
RadioSettingValueString(0, 7, _filter(_msg.user2)))
cfg_grp.append(rs)
val = RadioSettingValueString(0, 7, _filter(_msg.system))
val.set_mutable(False)
rs = RadioSetting("messages.system", "System Message", val)
cfg_grp.append(rs)
rs = RadioSetting("lamp", "Backlight",
RadioSettingValueList(
LAMP_LIST, current_index=_settings.lamp))
cfg_grp.append(rs)
rs = RadioSetting("keylockm", "Key Lock Mode",
RadioSettingValueList(
KEYLOCKM_LIST,
current_index=_settings.keylockm))
cfg_grp.append(rs)
rs = RadioSetting("absel", "A/B Select",
RadioSettingValueList(ABSEL_LIST,
current_index=_settings.absel))
cfg_grp.append(rs)
rs = RadioSetting("obeep", "Open Beep",
RadioSettingValueBoolean(_settings.obeep))
cfg_grp.append(rs)
rs = RadioSetting("rbeep", "Roger Beep",
RadioSettingValueBoolean(_settings.rbeep))
cfg_grp.append(rs)
rs = RadioSetting("keylock_off", "Key Lock",
RadioSettingValueBoolean(not _settings.keylock_off))
cfg_grp.append(rs)
rs = RadioSetting("ctdcsb", "CT/DCS Busy Lock",
RadioSettingValueBoolean(_settings.ctdcsb))
cfg_grp.append(rs)
rs = RadioSetting("alarm", "Alarm Key",
RadioSettingValueBoolean(_settings.alarm))
cfg_grp.append(rs)
rs = RadioSetting("save", "Battery Save",
RadioSettingValueBoolean(_settings.save))
cfg_grp.append(rs)
rs = RadioSetting("kbeep", "Key Beep",
RadioSettingValueBoolean(_settings.kbeep))
cfg_grp.append(rs)
rs = RadioSetting("reset", "Reset Enable",
RadioSettingValueBoolean(_settings.reset))
cfg_grp.append(rs)
rs = RadioSetting("smfont_off", "Small Font",
RadioSettingValueBoolean(not _settings.smfont_off))
cfg_grp.append(rs)
rs = RadioSetting("aliasen_off", "Alias Enable",
RadioSettingValueBoolean(not _settings.aliasen_off))
cfg_grp.append(rs)
rs = RadioSetting("txstop_off", "TX Stop",
RadioSettingValueBoolean(not _settings.txstop_off))
cfg_grp.append(rs)
rs = RadioSetting("dw_off", "Dual Watch",
RadioSettingValueBoolean(not _settings.dw_off))
cfg_grp.append(rs)
rs = RadioSetting("fmen_off", "FM Enable",
RadioSettingValueBoolean(not _settings.fmen_off))
cfg_grp.append(rs)
rs = RadioSetting("fmdw", "FM Dual Watch",
RadioSettingValueBoolean(_settings.fmdw))
cfg_grp.append(rs)
rs = RadioSetting("fmscan_off", "FM Scan",
RadioSettingValueBoolean(
not _settings.fmscan_off))
cfg_grp.append(rs)
rs = RadioSetting("keypadmic_off", "Keypad MIC",
RadioSettingValueBoolean(
not _settings.keypadmic_off))
cfg_grp.append(rs)
rs = RadioSetting("voxgain", "VOX Gain",
RadioSettingValueList(
VOXGAIN_LIST, current_index=_settings.voxgain))
cfg_grp.append(rs)
rs = RadioSetting("voxdt", "VOX Delay Time",
RadioSettingValueList(
VOXDT_LIST, current_index=_settings.voxdt))
cfg_grp.append(rs)
rs = RadioSetting("vir", "VOX Inhibit on Receive",
RadioSettingValueBoolean(_settings.vir))
cfg_grp.append(rs)
#
# Advanced Settings
#
val = (_settings.dtmftime) - 5
rs = RadioSetting("dtmftime", "DTMF Digit Time",
RadioSettingValueList(
DTMFTIME_LIST, current_index=val))
adv_grp.append(rs)
val = (_settings.dtmfspace) - 5
rs = RadioSetting("dtmfspace", "DTMF Digit Space Time",
RadioSettingValueList(
DTMFTIME_LIST, current_index=val))
adv_grp.append(rs)
val = (_settings.dtmfdelay) // 5
rs = RadioSetting("dtmfdelay", "DTMF 1st Digit Delay",
RadioSettingValueList(
DTMFDELAY_LIST, current_index=val))
adv_grp.append(rs)
val = (_settings.dtmfpretime) // 10 - 1
rs = RadioSetting("dtmfpretime", "DTMF Pretime",
RadioSettingValueList(
DTMFPRETIME_LIST, current_index=val))
adv_grp.append(rs)
val = (_settings.dtmfdelay2) // 5
rs = RadioSetting("dtmfdelay2", "DTMF * and # Digit Delay",
RadioSettingValueList(
DTMFDELAY2_LIST, current_index=val))
adv_grp.append(rs)
rs = RadioSetting("ackdecode", "ACK Decode",
RadioSettingValueBoolean(_settings.ackdecode))
adv_grp.append(rs)
rs = RadioSetting("dtmfst", "DTMF Sidetone",
RadioSettingValueBoolean(_settings.dtmfst))
adv_grp.append(rs)
rs = RadioSetting("service.rssi400", "Squelch Base Level (UHF)",
RadioSettingValueInteger(0, 255, _service.rssi400))
adv_grp.append(rs)
rs = RadioSetting("service.rssi136", "Squelch Base Level (VHF)",
RadioSettingValueInteger(0, 255, _service.rssi136))
adv_grp.append(rs)
#
# Key Settings
#
val = (_settings.lptime) - 5
rs = RadioSetting("lptime", "Long Press Time",
RadioSettingValueList(
LPTIME_LIST, current_index=val))
key_grp.append(rs)
rs = RadioSetting("keyp1long", "P1 Long Key",
RadioSettingValueList(
PFKEYLONG_LIST,
current_index=_settings.keyp1long))
key_grp.append(rs)
rs = RadioSetting("keyp1short", "P1 Short Key",
RadioSettingValueList(
PFKEYSHORT_LIST,
current_index=_settings.keyp1short))
key_grp.append(rs)
rs = RadioSetting("keyp2long", "P2 Long Key",
RadioSettingValueList(
PFKEYLONG_LIST,
current_index=_settings.keyp2long))
key_grp.append(rs)
rs = RadioSetting("keyp2short", "P2 Short Key",
RadioSettingValueList(
PFKEYSHORT_LIST,
current_index=_settings.keyp2short))
key_grp.append(rs)
rs = RadioSetting("keyp3long", "P3 Long Key",
RadioSettingValueList(
PFKEYLONG_LIST,
current_index=_settings.keyp3long))
key_grp.append(rs)
rs = RadioSetting("keyp3short", "P3 Short Key",
RadioSettingValueList(
PFKEYSHORT_LIST,
current_index=_settings.keyp3short))
key_grp.append(rs)
val = RadioSettingValueList(PFKEYSHORT_LIST,
current_index=_settings.keymshort)
val.set_mutable(_settings.menuen == 0)
rs = RadioSetting("keymshort", "M Short Key", val)
key_grp.append(rs)
val = RadioSettingValueBoolean(_settings.menuen)
rs = RadioSetting("menuen", "Menu Enable", val)
key_grp.append(rs)
return group
def get_settings(self):
try:
return self._get_settings()
except:
import traceback
LOG.error("Failed to parse settings: %s", traceback.format_exc())
return None
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()
elif setting == "keylock_off":
setattr(obj, setting, not int(element.value))
elif setting == "smfont_off":
setattr(obj, setting, not int(element.value))
elif setting == "aliasen_off":
setattr(obj, setting, not int(element.value))
elif setting == "txstop_off":
setattr(obj, setting, not int(element.value))
elif setting == "dw_off":
setattr(obj, setting, not int(element.value))
elif setting == "fmen_off":
setattr(obj, setting, not int(element.value))
elif setting == "fmscan_off":
setattr(obj, setting, not int(element.value))
elif setting == "keypadmic_off":
setattr(obj, setting, not int(element.value))
elif setting == "dtmftime":
setattr(obj, setting, int(element.value) + 5)
elif setting == "dtmfspace":
setattr(obj, setting, int(element.value) + 5)
elif setting == "dtmfdelay":
setattr(obj, setting, int(element.value) * 5)
elif setting == "dtmfpretime":
setattr(obj, setting, (int(element.value) + 1) * 10)
elif setting == "dtmfdelay2":
setattr(obj, setting, int(element.value) * 5)
elif setting == "lptime":
setattr(obj, setting, int(element.value) + 5)
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):
if filedata[0x168:0x170].startswith(cls._file_ident) and \
filedata[0x170:0x178].startswith(cls._model_ident):
return True
else:
return False
@directory.register
class JetstreamJT270MRadio(LeixenVV898Radio):
"""Jetstream JT270M"""
VENDOR = "Jetstream"
MODEL = "JT270M"
ALIASES = []
_file_ident = b"JET"
_model_ident = b'LX-\x89\x85\x53'
class LT898UV(LeixenVV898Radio):
VENDOR = "LUITON"
MODEL = "LT-898UV"
@classmethod
def match_model(cls, filedata, filename):
return False
@directory.register
class JetstreamJT270MHRadio(LeixenVV898Radio):
"""Jetstream JT270MH"""
VENDOR = "Jetstream"
MODEL = "JT270MH"
_file_ident = b"Leixen"
_model_ident = b'LX-\x89\x85\x85'
_ranges = [(0x0C00, 0x2000)]
_mem_formatter = {'unknownormode': 'mode:1',
'modeorpower': 'power:2',
'chanstart': 0x0C00,
'namestart': 0x1900,
'defaults': 6}
_power_levels = [chirp_common.PowerLevel("Low", watts=5),
chirp_common.PowerLevel("Mid", watts=10),
chirp_common.PowerLevel("High", watts=25)]
# Base radio has offset zero to distinguish from sub devices
_offset = 0
def get_features(self):
rf = super(JetstreamJT270MHRadio, self).get_features()
rf.has_sub_devices = self._offset == 0
rf.memory_bounds = (1, 99)
return rf
def get_sub_devices(self):
return [JetstreamJT270MHRadioA(self._mmap),
JetstreamJT270MHRadioB(self._mmap)]
def _get_memobjs(self, number):
number = number * 2 - self._offset
_mem = self._memobj.memory[number]
_name = self._memobj.name[number]
return _mem, _name
class JetstreamJT270MHRadioA(JetstreamJT270MHRadio):
VARIANT = 'A Band'
_offset = 1
class JetstreamJT270MHRadioB(JetstreamJT270MHRadio):
VARIANT = 'B Band'
_offset = 2
@directory.register
class LeixenVV898SRadio(LeixenVV898Radio):
"""Leixen VV-898S, also VV-898E which is identical"""
VENDOR = "Leixen"
MODEL = "VV-898S"
_model_ident = b'LX-\x89\x85\x75'
_mem_formatter = {'unknownormode': 'mode:1',
'modeorpower': 'power:2',
'chanstart': 0x0D00,
'namestart': 0x19B0,
'defaults': 3}
_power_levels = [chirp_common.PowerLevel("Low", watts=5),
chirp_common.PowerLevel("Med", watts=10),
chirp_common.PowerLevel("High", watts=25)]
@directory.register
class VV898E(LeixenVV898SRadio):
'''Leixen has called this radio both 898E and S historically, ident is
identical'''
VENDOR = "Leixen"
MODEL = "VV-898E"
@classmethod
def match_model(cls, filedata, filename):
return False
@directory.register
@directory.detected_by(LeixenVV898SRadio)
class VV898SDualBank(JetstreamJT270MHRadio):
'''Newer VV898S 1.06+ firmware that features dual memory banks'''
VENDOR = "Leixen"
MODEL = "VV-898S"
VARIANT = "Dual Bank"
@classmethod
def match_model(cls, filedata, filename):
return False
@directory.register
@directory.detected_by(VV898E)
class VV898EDualBank(JetstreamJT270MHRadio):
'''Newer VV898E 1.06+ firmware that features dual memory banks'''
VENDOR = "Leixen"
MODEL = "VV-898E"
VARIANT = "Dual Bank"
@classmethod
def match_model(cls, filedata, filename):
return False
chirp-20261030/chirp/drivers/lt725uv.py 0000644 0001750 0001750 00000156752 15172631605 016445 0 ustar hibby hibby # Copyright 2016-2023:
# * Jim Unroe KC9HI,
# Modified for Baojie BJ-218: 2018
# Rick DeWitt (RJD),
# Modified for Baojie BJ-318: April 2021
# Mark Hartong, AJ4YI
# Modified for Baojie BJ-318: January 2023
# Mark Hartong, AJ4YI
# 1. Removed Experimental Warning
# 2. Added Brown & Yellow as Color Selections
# 3. Max VFO Volume Setting to 15 ( vice 10)
# Modified UHF band limits: February 2023
# Jim Unroe, KC9HI
#
# 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 .
import struct
import logging
from chirp import chirp_common, directory, memmap
from chirp import bitwise, errors, util
from chirp.settings import RadioSettingGroup, RadioSetting, \
RadioSettingValueBoolean, RadioSettingValueList, \
RadioSettingValueString, RadioSettingValueInteger, \
RadioSettingValueFloat, RadioSettings, InvalidValueError
LOG = logging.getLogger(__name__)
MEM_FORMAT = """
#seekto 0x0200;
struct {
u8 init_bank; // determines which VFO is primary A or B
u8 volume; // not used BJ-318, band volume is controlled vfo u8 bvol
u16 fm_freq; // not used BJ-318, freq is controlled hello_lims u16 fm_318
u8 wtled; // not used BJ-318
u8 rxled; // not used BJ-318
u8 txled; // not used BJ-318
u8 ledsw;
u8 beep;
u8 ring;
u8 bcl;
u8 tot;
u16 sig_freq;
u16 dtmf_txms;
u8 init_sql; // not used BJ-318, band squelch is controlled vfo u8 sql
u8 rptr_mode;
} settings;
#seekto 0x0240;
struct {
u8 dtmf1_cnt;
u8 dtmf1[7];
u8 dtmf2_cnt;
u8 dtmf2[7];
u8 dtmf3_cnt;
u8 dtmf3[7];
u8 dtmf4_cnt;
u8 dtmf4[7];
u8 dtmf5_cnt;
u8 dtmf5[7];
u8 dtmf6_cnt;
u8 dtmf6[7];
u8 dtmf7_cnt;
u8 dtmf7[7];
u8 dtmf8_cnt;
u8 dtmf8[7];
} dtmf_tab;
#seekto 0x0280;
struct {
u8 native_id_cnt;
u8 native_id_code[7];
u8 master_id_cnt;
u8 master_id_code[7];
u8 alarm_cnt;
u8 alarm_code[5];
u8 id_disp_cnt;
u8 id_disp_code[5];
u8 revive_cnt;
u8 revive_code[5];
u8 stun_cnt;
u8 stun_code[5];
u8 kill_cnt;
u8 kill_code[5];
u8 monitor_cnt;
u8 monitor_code[5];
u8 state_now;
} codes;
#seekto 0x02d0;
struct {
u8 hello1_cnt; // not used in BJ-318, is set in memory map
char hello1[7]; // not used in BJ-318, is set in memory map
u8 hello2_cnt; // not used in BJ-318, is set in memory map
char hello2[7]; // not used in BJ-318, is set in memory map
u32 vhf_low;
u32 vhf_high;
u32 uhf_low;
u32 uhf_high;
u8 lims_on; // first byte @ at address 0x02f0;
u8 unknown_1; // use in BJ-318 unknown
u8 lang; // BJ-318 Display language
// 02 = English 00 and 01 are Asian
u8 up_scr_color; // BJ-318 upper screen character color
u8 dn_scr_color; // BJ-318 lower screen character color
// purple=00, red=01, emerald=02, blue=03,
// sky_blue=04, black=05, brown=06, yellow=07 required
// hex values for color
// Note sky_blue and black look the same on the
// BJ-318 screen
u16 fm_318; // FM stored Frequency in BJ-318
} hello_lims;
struct vfo {
u8 frq_chn_mode;
u8 chan_num;
u32 rxfreq;
u16 is_rxdigtone:1,
rxdtcs_pol:1,
rx_tone:14;
u8 rx_mode;
u8 unknown_ff;
u16 is_txdigtone:1,
txdtcs_pol:1,
tx_tone:14;
u8 launch_sig;
u8 tx_end_sig;
u8 bpower; // sets power Hi=02h, Medium=01h, Low=00
// sets power for entire vfo band
u8 fm_bw;
u8 cmp_nder;
u8 scrm_blr;
u8 shift;
u32 offset;
u16 step;
u8 sql; // squelch for entire vfo band
// integer values 0 (low) to 9 (high)
u8 bvol; // sets volume for vfo band
// integer values 0 (low) TO 15 (high)
};
#seekto 0x0300;
struct {
struct vfo vfoa;
} upper;
#seekto 0x0380;
struct {
struct vfo vfob;
} lower;
struct mem {
u32 rxfreq;
u16 is_rxdigtone:1,
rxdtcs_pol:1,
rxtone:14;
u8 recvmode;
u32 txfreq;
u16 is_txdigtone:1,
txdtcs_pol:1,
txtone:14;
u8 botsignal;
u8 eotsignal;
u8 power:1, // binary value for
// individual channel power
// set to "High" or "Low"
// BJ-318 band power overrides any
// individual channel power setting
wide:1,
compander:1,
scrambler:1,
unknown:4;
u8 namelen;
u8 name[7];
};
#seekto 0x0400;
struct mem upper_memory[128];
#seekto 0x1000;
struct mem lower_memory[128];
#seekto 0x1C00;
struct {
char mod_num[6];
} mod_id;
"""
MEM_SIZE = 0x1C00
BLOCK_SIZE = 0x40
STIMEOUT = 2
LIST_RECVMODE = ["QT/DQT", "QT/DQT + Signaling"]
LIST_SIGNAL = ["Off"] + ["DTMF%s" % x for x in range(1, 9)] + \
["DTMF%s + Identity" % x for x in range(1, 9)] + \
["Identity code"]
# Band Power settings, can be different than channel power
LIST_BPOWER = ["Low", "Mid", "High"] # Tri-power models
# Screen Color Settings
# BJ-218 Colors
LIST_COLOR = ["Off", "Orange", "Blue", "Purple"]
# BJ-318 Colors
LIST_COLOR318 = ["Purple", "Red", "Emerald", "Blue", "Sky_Blue", "Black",
"Brown", "Yellow"]
LIST_LEDSW = ["Auto", "On"]
LIST_RING = ["Off"] + ["%s" % x for x in range(1, 10)]
LIST_TDR_DEF = ["A-Upper", "B-Lower"]
LIST_TIMEOUT = ["Off"] + ["%s" % x for x in range(30, 630, 30)]
LIST_VFOMODE = ["Frequency Mode", "Channel Mode"]
# Tones are numeric, Defined in \chirp\chirp_common.py
TONES_CTCSS = sorted(chirp_common.TONES)
# Converted to strings
LIST_CTCSS = ["Off"] + [str(x) for x in TONES_CTCSS]
# Now append the DxxxN and DxxxI DTCS codes from chirp_common
for x in chirp_common.DTCS_CODES:
LIST_CTCSS.append("D{:03d}N".format(x))
for x in chirp_common.DTCS_CODES:
LIST_CTCSS.append("D{:03d}R".format(x))
LIST_BW = ["Narrow", "Wide"]
LIST_SHIFT = ["Off", " + ", " - "]
STEPS = [2.5, 5.0, 6.25, 10.0, 12.5, 20.0, 25.0, 50.0]
LIST_STEPS = [str(x) for x in STEPS]
LIST_STATE = ["Normal", "Stun", "Kill"]
LIST_SSF = ["1000", "1450", "1750", "2100"]
LIST_DTMFTX = ["50", "100", "150", "200", "300", "500"]
def _clean_buffer(radio):
radio.pipe.timeout = 0.005
junk = radio.pipe.read(256)
radio.pipe.timeout = STIMEOUT
if junk:
LOG.debug("Got %i bytes of junk before starting" % len(junk))
def _rawrecv(radio, amount):
"""Raw read from the radio device"""
data = b""
try:
data = radio.pipe.read(amount)
except:
_exit_program_mode(radio)
msg = "Generic error reading data from radio; check your cable."
raise errors.RadioError(msg)
if not data:
_exit_program_mode(radio)
raise errors.RadioNoResponse()
if len(data) != amount:
_exit_program_mode(radio)
msg = "Error reading from radio: not the amount of data we want."
raise errors.RadioError(msg)
return data
def _rawsend(radio, data):
"""Raw send to the radio device"""
try:
radio.pipe.write(data)
except:
raise errors.RadioError("Error sending data to radio")
def _make_frame(cmd, addr, length, data=""):
"""Pack the info in the header format"""
frame = struct.pack(">4sHH", cmd, addr, length)
# Add the data if set
if len(data) != 0:
frame += data
# Return the data
return frame
def _recv(radio, addr, length):
"""Get data from the radio """
data = _rawrecv(radio, length)
# DEBUG
LOG.info("Response:")
LOG.debug(util.hexprint(data))
return data
def _do_ident(radio):
"""Put the radio in PROGRAM mode & identify it"""
# Set the serial discipline
radio.pipe.baudrate = 19200
radio.pipe.parity = "N"
radio.pipe.timeout = STIMEOUT
# Flush input buffer
_clean_buffer(radio)
magic = b"PROM_LIN"
_rawsend(radio, magic)
ack = _rawrecv(radio, 1)
if not ack:
_exit_program_mode(radio)
raise errors.RadioNoResponse()
if ack != b"\x06":
_exit_program_mode(radio)
LOG.debug(repr(ack))
raise errors.RadioError("Unexpected response from radio")
return True
def _exit_program_mode(radio):
endframe = b"EXIT"
_rawsend(radio, endframe)
def _download(radio):
"""Get the memory map"""
# Put radio in program mode and identify it
_do_ident(radio)
# UI progress
status = chirp_common.Status()
status.cur = 0
status.max = MEM_SIZE // BLOCK_SIZE
status.msg = "Cloning from radio..."
radio.status_fn(status)
data = b""
for addr in range(0, MEM_SIZE, BLOCK_SIZE):
frame = _make_frame(b"READ", addr, BLOCK_SIZE)
# DEBUG
LOG.info("Request sent:")
LOG.debug(util.hexprint(frame))
# Sending the read request
_rawsend(radio, frame)
# Now we read
d = _recv(radio, addr, BLOCK_SIZE)
# Aggregate the data
data += d
# UI Update
status.cur = addr // BLOCK_SIZE
status.msg = "Cloning from radio..."
radio.status_fn(status)
_exit_program_mode(radio)
return data
def _upload(radio):
"""Upload procedure"""
# Put radio in program mode and identify it
_do_ident(radio)
# UI progress
status = chirp_common.Status()
status.cur = 0
status.max = MEM_SIZE // BLOCK_SIZE
status.msg = "Cloning to radio..."
radio.status_fn(status)
# The fun starts here
for addr in range(0, MEM_SIZE, BLOCK_SIZE):
# Sending the data
data = radio.get_mmap()[addr:addr + BLOCK_SIZE]
frame = _make_frame(b"WRIE", addr, BLOCK_SIZE, data)
_rawsend(radio, frame)
# Receiving the response
ack = _rawrecv(radio, 1)
if ack != b"\x06":
_exit_program_mode(radio)
msg = "Bad ack writing block 0x%04x" % addr
raise errors.RadioError(msg)
# UI Update
status.cur = addr // BLOCK_SIZE
status.msg = "Cloning to radio..."
radio.status_fn(status)
_exit_program_mode(radio)
def model_match(cls, data):
"""Match the opened/downloaded image to the correct version"""
if len(data) == 0x1C08:
rid = data[0x1C00:0x1C08]
return rid.startswith(cls.MODEL.encode())
else:
return False
@directory.register
class LT725UV(chirp_common.CloneModeRadio):
"""LUITON LT-725UV Radio"""
VENDOR = "LUITON"
MODEL = "LT-725UV"
MODES = ["NFM", "FM"]
TONES = chirp_common.TONES
DTCS_CODES = tuple(sorted(chirp_common.DTCS_CODES + (645,)))
NAME_LENGTH = 7
DTMF_CHARS = list("0123456789ABCD*#")
# Channel Power: 2 levels
# BJ-318 uses 3 power levels for each VFO "Band"
# Low = 5W, Med = 10W, High = 25W
# The band power selection in a VFO applies to the VFO and overrides
# the stored channel power selection
# The firmware channel memory structure provides only 1 bit for
# individual channel power settings, limiting potential channel
# power selection options to 2 levels: Low or High.
POWER_LEVELS = [chirp_common.PowerLevel("Low", watts=5.00),
chirp_common.PowerLevel("High", watts=30.00)]
VALID_BANDS = [(136000000, 176000000),
(400000000, 490000000)]
# Valid chars on the LCD
VALID_CHARS = chirp_common.CHARSET_ALPHANUMERIC + \
"`{|}!\"#$%&'()*+,-./:;<=>?@[]^_"
@classmethod
def get_prompts(cls):
rp = chirp_common.RadioPrompts()
if cls.MODEL == "BJ-318":
msg = \
('\n'
'The individual BJ-318 channel power settings set in CHIRP'
' are ignored by \n'
'the radio. They are allowed to be set in hopes of a future'
' firmware \n'
'change by the manufacturer. While the CHIRP driver will'
' allow setting \n'
'the individual VFO power to High (25W), Med (10W) or Low'
' (5W) they \n'
'are converted to just Low (5W) and High (25W) on upload. The'
' Med setting \n'
'reverts to Low. \n'
'\n'
'Changing a channel power setting must be done manually using'
' the front \n'
'panel menu control (or the microphone controls while in VFO'
' mode.)'
)
else:
msg = \
('Some notes about POWER settings:\n'
'- The individual channel power settings are ignored'
' by the radio.\n'
' They are allowed to be set (and downloaded) in hopes of'
' a future firmware update.\n'
'- Power settings done \'Live\' in the radio apply to the'
' entire upper or lower band.\n'
'- Tri-power radio models will set and download the three'
' band-power'
' levels, but they are converted to just Low and High at'
' upload.'
' The Mid setting reverts to Low.'
)
rp.info = msg
rp.pre_download = _(
"Follow this instructions to download your info:\n"
"1 - Turn off your radio\n"
"2 - Connect your interface cable\n"
"3 - Turn on your radio\n"
"4 - Do the download of your radio data\n")
rp.pre_upload = _(
"Follow this instructions to upload your info:\n"
"1 - Turn off your radio\n"
"2 - Connect your interface cable\n"
"3 - Turn on your radio\n"
"4 - Do the upload of your radio data\n")
return rp
def get_features(self):
rf = chirp_common.RadioFeatures()
rf.has_settings = True
rf.has_bank = False
rf.has_tuning_step = False
rf.can_odd_split = True
rf.has_name = True
rf.has_offset = True
rf.has_mode = True
rf.has_dtcs = True
rf.has_rx_dtcs = True
rf.has_dtcs_polarity = True
rf.has_ctone = True
rf.has_cross = True
rf.has_sub_devices = self.VARIANT == ""
rf.valid_modes = self.MODES
rf.valid_characters = self.VALID_CHARS
rf.valid_duplexes = ["", "-", "+", "split", "off"]
rf.valid_tmodes = ['', 'Tone', 'TSQL', 'DTCS', 'Cross']
rf.valid_cross_modes = [
"Tone->Tone",
"DTCS->",
"->DTCS",
"Tone->DTCS",
"DTCS->Tone",
"->Tone",
"DTCS->DTCS"]
rf.valid_skips = []
rf.valid_power_levels = self.POWER_LEVELS
rf.valid_name_length = self.NAME_LENGTH
rf.valid_dtcs_codes = self.DTCS_CODES
rf.valid_bands = self.VALID_BANDS
rf.memory_bounds = (1, 128)
rf.valid_tuning_steps = STEPS
return rf
def get_sub_devices(self):
return [LT725UVUpper(self._mmap), LT725UVLower(self._mmap)]
def sync_in(self):
"""Download from radio"""
try:
data = _download(self)
except errors.RadioError:
# Pass through any real errors we raise
raise
except:
# If anything unexpected happens, make sure we raise
# a RadioError and log the problem
LOG.exception('Unexpected error during download')
raise errors.RadioError('Unexpected error communicating '
'with the radio')
self._mmap = memmap.MemoryMapBytes(data)
self.process_mmap()
def sync_out(self):
"""Upload to radio"""
try:
_upload(self)
except errors.RadioError:
raise
except Exception:
# If anything unexpected happens, make sure we raise
# a RadioError and log the problem
LOG.exception('Unexpected error during upload')
raise errors.RadioError('Unexpected error communicating '
'with the radio')
def process_mmap(self):
"""Process the mem map into the mem object"""
self._memobj = bitwise.parse(MEM_FORMAT, self._mmap)
def get_raw_memory(self, number):
return repr(self._memobj.memory[number - 1])
def _memory_obj(self, suffix=""):
return getattr(self._memobj, "%s_memory%s" % (self._vfo, suffix))
def _get_dcs(self, val):
return val.get_bbcd()
def _set_dcs(self, val):
return bitwise.dec_to_bbcd(val)
def get_memory(self, number):
_mem = self._memory_obj()[number - 1]
mem = chirp_common.Memory()
mem.number = number
if _mem.get_raw(asbytes=False)[0] == "\xff":
mem.empty = True
return mem
mem.freq = int(_mem.rxfreq) * 10
if _mem.txfreq == 0xFFFFFFFF:
# TX freq not set
mem.duplex = "off"
mem.offset = 0
elif int(_mem.rxfreq) == int(_mem.txfreq):
mem.duplex = ""
mem.offset = 0
elif chirp_common.is_split(self.get_features().valid_bands,
mem.freq, int(_mem.txfreq) * 10):
mem.duplex = "split"
mem.offset = int(_mem.txfreq) * 10
else:
mem.duplex = int(_mem.rxfreq) > int(_mem.txfreq) and "-" or "+"
mem.offset = abs(int(_mem.rxfreq) - int(_mem.txfreq)) * 10
for char in _mem.name[0:_mem.namelen]:
mem.name += chr(char)
dtcs_pol = ["N", "N"]
if _mem.rxtone == 0x3FFF:
rxmode = ""
elif _mem.is_rxdigtone == 0:
# CTCSS
rxmode = "Tone"
mem.ctone = int(_mem.rxtone) / 10.0
else:
# Digital
rxmode = "DTCS"
mem.rx_dtcs = self._get_dcs(_mem.rxtone)
if _mem.rxdtcs_pol == 1:
dtcs_pol[1] = "R"
if _mem.txtone == 0x3FFF:
txmode = ""
elif _mem.is_txdigtone == 0:
# CTCSS
txmode = "Tone"
mem.rtone = int(_mem.txtone) / 10.0
else:
# Digital
txmode = "DTCS"
mem.dtcs = self._get_dcs(_mem.txtone)
if _mem.txdtcs_pol == 1:
dtcs_pol[0] = "R"
if txmode == "Tone" and not rxmode:
mem.tmode = "Tone"
elif txmode == rxmode and txmode == "Tone" and mem.rtone == mem.ctone:
mem.tmode = "TSQL"
elif txmode == rxmode and txmode == "DTCS" and mem.dtcs == mem.rx_dtcs:
mem.tmode = "DTCS"
elif rxmode or txmode:
mem.tmode = "Cross"
mem.cross_mode = "%s->%s" % (txmode, rxmode)
mem.dtcs_polarity = "".join(dtcs_pol)
mem.mode = _mem.wide and "FM" or "NFM"
mem.power = self.POWER_LEVELS[_mem.power]
# Extra
mem.extra = RadioSettingGroup("extra", "Extra")
if _mem.recvmode == 0xFF:
val = 0x00
else:
val = _mem.recvmode
recvmode = RadioSetting("recvmode", "Receiving mode",
RadioSettingValueList(LIST_RECVMODE,
current_index=val))
mem.extra.append(recvmode)
if _mem.botsignal == 0xFF:
val = 0x00
else:
val = _mem.botsignal
botsignal = RadioSetting("botsignal", "Launch signaling",
RadioSettingValueList(LIST_SIGNAL,
current_index=val))
mem.extra.append(botsignal)
if _mem.eotsignal == 0xFF:
val = 0x00
else:
val = _mem.eotsignal
rx = RadioSettingValueList(LIST_SIGNAL, current_index=val)
eotsignal = RadioSetting("eotsignal", "Transmit end signaling", rx)
mem.extra.append(eotsignal)
rx = RadioSettingValueBoolean(bool(_mem.compander))
compander = RadioSetting("compander", "Compander", rx)
mem.extra.append(compander)
rx = RadioSettingValueBoolean(bool(_mem.scrambler))
scrambler = RadioSetting("scrambler", "Scrambler", rx)
mem.extra.append(scrambler)
return mem
def set_memory(self, mem):
_mem = self._memory_obj()[mem.number - 1]
if mem.empty:
_mem.set_raw("\xff" * 24)
_mem.namelen = 0
return
_mem.set_raw("\xFF" * 15 + "\x00\x00" + "\xFF" * 7)
_mem.rxfreq = mem.freq / 10
if mem.duplex == "off":
_mem.txfreq = 0xFFFFFFFF
elif mem.duplex == "split":
_mem.txfreq = mem.offset / 10
elif mem.duplex == "+":
_mem.txfreq = (mem.freq + mem.offset) / 10
elif mem.duplex == "-":
_mem.txfreq = (mem.freq - mem.offset) / 10
else:
_mem.txfreq = mem.freq / 10
_mem.namelen = len(mem.name.rstrip())
_namelength = self.get_features().valid_name_length
for i in range(_namelength):
try:
_mem.name[i] = ord(mem.name[i])
except IndexError:
_mem.name[i] = 0xFF
rxmode = ""
txmode = ""
if mem.tmode == "Tone":
txmode = "Tone"
elif mem.tmode == "TSQL":
rxmode = "Tone"
txmode = "TSQL"
elif mem.tmode == "DTCS":
rxmode = "DTCSSQL"
txmode = "DTCS"
elif mem.tmode == "Cross":
txmode, rxmode = mem.cross_mode.split("->", 1)
if rxmode == "":
_mem.rxdtcs_pol = 1
_mem.is_rxdigtone = 1
_mem.rxtone = 0x3FFF
elif rxmode == "Tone":
_mem.rxdtcs_pol = 0
_mem.is_rxdigtone = 0
_mem.rxtone = int(mem.ctone * 10)
elif rxmode == "DTCSSQL":
_mem.rxdtcs_pol = 1 if mem.dtcs_polarity[1] == "R" else 0
_mem.is_rxdigtone = 1
_mem.rxtone = self._set_dcs(mem.dtcs)
elif rxmode == "DTCS":
_mem.rxdtcs_pol = 1 if mem.dtcs_polarity[1] == "R" else 0
_mem.is_rxdigtone = 1
_mem.rxtone = self._set_dcs(mem.rx_dtcs)
if txmode == "":
_mem.txdtcs_pol = 1
_mem.is_txdigtone = 1
_mem.txtone = 0x3FFF
elif txmode == "Tone":
_mem.txdtcs_pol = 0
_mem.is_txdigtone = 0
_mem.txtone = int(mem.rtone * 10)
elif txmode == "TSQL":
_mem.txdtcs_pol = 0
_mem.is_txdigtone = 0
_mem.txtone = int(mem.ctone * 10)
elif txmode == "DTCS":
_mem.txdtcs_pol = 1 if mem.dtcs_polarity[0] == "R" else 0
_mem.is_txdigtone = 1
_mem.txtone = self._set_dcs(mem.dtcs)
_mem.wide = self.MODES.index(mem.mode)
_mem.power = mem.power == self.POWER_LEVELS[1]
# Extra settings
for setting in mem.extra:
setattr(_mem, setting.get_name(), setting.value)
def get_settings(self):
"""Translate the bit in the mem_struct into settings in the UI"""
# Define mem struct write-back shortcuts
_sets = self._memobj.settings
_vfoa = self._memobj.upper.vfoa
_vfob = self._memobj.lower.vfob
_lims = self._memobj.hello_lims
_codes = self._memobj.codes
_dtmf = self._memobj.dtmf_tab
basic = RadioSettingGroup("basic", "Basic Settings")
a_band = RadioSettingGroup("a_band", "VFO A-Upper Settings")
b_band = RadioSettingGroup("b_band", "VFO B-Lower Settings")
codes = RadioSettingGroup("codes", "Codes & DTMF Groups")
lims = RadioSettingGroup("lims", "PowerOn & Freq Limits")
group = RadioSettings(basic, a_band, b_band, lims, codes)
# Basic Settings
bnd_mode = RadioSetting(
"settings.init_bank", "TDR Band Default",
RadioSettingValueList(
LIST_TDR_DEF, current_index=_sets.init_bank))
basic.append(bnd_mode)
# BJ-318 set by vfo, skip
if self.MODEL != "BJ-318":
rs = RadioSettingValueInteger(0, 20, _sets.volume)
volume = RadioSetting("settings.volume", "Volume", rs)
basic.append(volume)
# BJ-318 set by vfo, skip
if self.MODEL != "BJ-318":
vala = _vfoa.bpower # 2bits values 0,1,2= Low, Mid, High
rx = RadioSettingValueList(LIST_BPOWER, current_index=vala)
powera = RadioSetting("upper.vfoa.bpower", "Power (Upper)", rx)
basic.append(powera)
# BJ-318 set by vfo, skip
if self.MODEL != "BJ-318":
valb = _vfob.bpower
rx = RadioSettingValueList(LIST_BPOWER, current_index=valb)
powerb = RadioSetting("lower.vfob.bpower", "Power (Lower)", rx)
basic.append(powerb)
def my_word2raw(setting, obj, atrb, mlt=10):
"""Callback function to convert UI floating value to u16 int"""
if str(setting.value) == "Off":
frq = 0x0FFFF
else:
frq = int(float(str(setting.value)) * float(mlt))
if frq == 0:
frq = 0xFFFF
setattr(obj, atrb, frq)
return
def my_adjraw(setting, obj, atrb, fix):
"""Callback: add or subtract fix from value."""
vx = int(str(setting.value))
value = vx + int(fix)
if value < 0:
value = 0
if atrb == "frq_chn_mode" and int(str(setting.value)) == 2:
value = vx * 2 # Special handling for frq_chn_mode
setattr(obj, atrb, value)
return
def my_dbl2raw(setting, obj, atrb, flg=1):
"""Callback: convert from freq 146.7600 to 14760000 U32."""
value = chirp_common.parse_freq(str(setting.value)) / 10
# flg=1 means 0 becomes ff, else leave as possible 0
if flg == 1 and value == 0:
value = 0xFFFFFFFF
setattr(obj, atrb, value)
return
def my_val_list(setting, obj, atrb):
"""Callback:from ValueList with non-sequential, actual values."""
value = int(str(setting.value)) # Get the integer value
if atrb == "tot":
value = int(value / 30) # 30 second increments
setattr(obj, atrb, value)
return
def my_spcl(setting, obj, atrb):
"""Callback: Special handling based on atrb."""
if atrb == "frq_chn_mode":
idx = LIST_VFOMODE.index(str(setting.value)) # Returns 0 or 1
value = idx * 2 # Set bit 1
setattr(obj, atrb, value)
return
def my_tone_strn(obj, is_atr, pol_atr, tone_atr):
"""Generate the CTCS/DCS tone code string."""
vx = int(getattr(obj, tone_atr))
if vx == 16383 or vx == 0:
return "Off" # 16383 is all bits set
if getattr(obj, is_atr) == 0: # Simple CTCSS code
tstr = str(vx / 10.0)
else: # DCS
if getattr(obj, pol_atr) == 0:
tstr = "D{:03x}R".format(vx)
else:
tstr = "D{:03x}N".format(vx)
return tstr
def my_set_tone(setting, obj, is_atr, pol_atr, tone_atr):
"""Callback- create the tone setting from string code."""
sx = str(setting.value) # '131.8' or 'D231N' or 'Off'
if sx == "Off":
isx = 1
polx = 1
tonx = 0x3FFF
elif sx[0] == "D": # DCS
isx = 1
if sx[4] == "N":
polx = 1
else:
polx = 0
tonx = int(sx[1:4], 16)
else: # CTCSS
isx = 0
polx = 0
tonx = int(float(sx) * 10.0)
setattr(obj, is_atr, isx)
setattr(obj, pol_atr, polx)
setattr(obj, tone_atr, tonx)
return
# not used by BJ-318 skip
if self.MODEL != "BJ-318":
val = _sets.fm_freq / 10.0
if val == 0:
val = 88.9 # 0 is not valid
rx = RadioSettingValueFloat(65, 108.0, val, 0.1, 1)
rs = RadioSetting("settings.fm_freq",
"FM Broadcast Freq (MHz)", rx)
rs.set_apply_callback(my_word2raw, _sets, "fm_freq")
basic.append(rs)
# BJ-318 fm frequency
if self.MODEL == "BJ-318":
val = _lims.fm_318 / 10.0
if val == 0:
val = 88.9 # 0 is not valid
rx = RadioSettingValueFloat(65, 108.0, val, 0.1, 1)
rs = RadioSetting("hello_lims.fm_318",
"FM Broadcast Freq (MHz)", rx)
rs.set_apply_callback(my_word2raw, _lims, "fm_318")
basic.append(rs)
# not used in BJ-318, skip
if self.MODEL != "BJ-318":
rs = RadioSettingValueList(LIST_COLOR,
current_index=_sets.wtled)
wtled = RadioSetting("settings.wtled", "Standby LED Color", rs)
basic.append(wtled)
# not used in BJ-318, skip
if self.MODEL != "BJ-318":
rs = RadioSettingValueList(LIST_COLOR,
current_index=_sets.rxled)
rxled = RadioSetting("settings.rxled", "RX LED Color", rs)
basic.append(rxled)
# not used in BJ-318, skip
if self.MODEL != "BJ-318":
rs = RadioSettingValueList(LIST_COLOR,
current_index=_sets.txled)
txled = RadioSetting("settings.txled", "TX LED Color", rs)
basic.append(txled)
ledsw = RadioSetting(
"settings.ledsw", "Back light mode",
RadioSettingValueList(LIST_LEDSW, current_index=_sets.ledsw))
basic.append(ledsw)
beep = RadioSetting("settings.beep", "Beep",
RadioSettingValueBoolean(bool(_sets.beep)))
basic.append(beep)
ring = RadioSetting("settings.ring", "Ring",
RadioSettingValueList(
LIST_RING, current_index=_sets.ring))
basic.append(ring)
bcl = RadioSetting("settings.bcl", "Busy channel lockout",
RadioSettingValueBoolean(bool(_sets.bcl)))
basic.append(bcl)
# squelch for non-BJ-318 models
# BJ-318 squelch set by VFO basis
if self.MODEL != "BJ-318":
if _vfoa.sql == 0xFF:
val = 0x04
else:
val = _vfoa.sql
sqla = RadioSetting("upper.vfoa.sql", "Squelch (Upper)",
RadioSettingValueInteger(0, 9, val))
basic.append(sqla)
if _vfob.sql == 0xFF:
val = 0x04
else:
val = _vfob.sql
sqlb = RadioSetting("lower.vfob.sql", "Squelch (Lower)",
RadioSettingValueInteger(0, 9, val))
basic.append(sqlb)
tmp = str(int(_sets.tot) * 30) # 30 sec step counter
rs = RadioSetting("settings.tot", "Transmit Timeout (Secs)",
RadioSettingValueList(LIST_TIMEOUT, tmp))
rs.set_apply_callback(my_val_list, _sets, "tot")
basic.append(rs)
tmp = str(int(_sets.sig_freq))
rs = RadioSetting("settings.sig_freq", "Single Signaling Tone (Hz)",
RadioSettingValueList(LIST_SSF, tmp))
rs.set_apply_callback(my_val_list, _sets, "sig_freq")
basic.append(rs)
tmp = str(int(_sets.dtmf_txms))
rs = RadioSetting("settings.dtmf_txms", "DTMF Tx Duration (mSecs)",
RadioSettingValueList(LIST_DTMFTX, tmp))
rs.set_apply_callback(my_val_list, _sets, "dtmf_txms")
basic.append(rs)
rs = RadioSetting("settings.rptr_mode", "Repeater Mode",
RadioSettingValueBoolean(bool(_sets.rptr_mode)))
basic.append(rs)
# UPPER BAND SETTINGS
# Freq Mode, convert bit 1 state to index pointer
val = _vfoa.frq_chn_mode // 2
rx = RadioSettingValueList(LIST_VFOMODE, current_index=val)
rs = RadioSetting("upper.vfoa.frq_chn_mode", "Default Mode", rx)
rs.set_apply_callback(my_spcl, _vfoa, "frq_chn_mode")
a_band.append(rs)
val = _vfoa.chan_num + 1 # Add 1 for 1-128 displayed
rs = RadioSetting("upper.vfoa.chan_num", "Initial Chan",
RadioSettingValueInteger(1, 128, val))
rs.set_apply_callback(my_adjraw, _vfoa, "chan_num", -1)
a_band.append(rs)
val = _vfoa.rxfreq / 100000.0
if (val < 136.0 or val > 176.0):
val = 146.520 # 2m calling
rs = RadioSetting("upper.vfoa.rxfreq ", "Default Recv Freq (MHz)",
RadioSettingValueFloat(136.0, 176.0, val, 0.001, 5))
rs.set_apply_callback(my_dbl2raw, _vfoa, "rxfreq")
a_band.append(rs)
tmp = my_tone_strn(_vfoa, "is_rxdigtone", "rxdtcs_pol", "rx_tone")
rs = RadioSetting("rx_tone", "Default Recv CTCSS (Hz)",
RadioSettingValueList(LIST_CTCSS, tmp))
rs.set_apply_callback(my_set_tone, _vfoa, "is_rxdigtone",
"rxdtcs_pol", "rx_tone")
a_band.append(rs)
rx = RadioSettingValueList(LIST_RECVMODE,
current_index=_vfoa.rx_mode)
rs = RadioSetting("upper.vfoa.rx_mode", "Default Recv Mode", rx)
a_band.append(rs)
tmp = my_tone_strn(_vfoa, "is_txdigtone", "txdtcs_pol", "tx_tone")
rs = RadioSetting("tx_tone", "Default Xmit CTCSS (Hz)",
RadioSettingValueList(LIST_CTCSS, tmp))
rs.set_apply_callback(my_set_tone, _vfoa, "is_txdigtone",
"txdtcs_pol", "tx_tone")
a_band.append(rs)
rs = RadioSetting(
"upper.vfoa.launch_sig", "Launch Signaling",
RadioSettingValueList(
LIST_SIGNAL, current_index=_vfoa.launch_sig))
a_band.append(rs)
rx = RadioSettingValueList(LIST_SIGNAL, current_index=_vfoa.tx_end_sig)
rs = RadioSetting("upper.vfoa.tx_end_sig", "Xmit End Signaling", rx)
a_band.append(rs)
rx = RadioSettingValueList(LIST_BW, current_index=_vfoa.fm_bw)
rs = RadioSetting("upper.vfoa.fm_bw", "Wide/Narrow Band", rx)
a_band.append(rs)
rx = RadioSettingValueBoolean(bool(_vfoa.cmp_nder))
rs = RadioSetting("upper.vfoa.cmp_nder", "Compander", rx)
a_band.append(rs)
rs = RadioSetting("upper.vfoa.scrm_blr", "Scrambler",
RadioSettingValueBoolean(bool(_vfoa.scrm_blr)))
a_band.append(rs)
rx = RadioSettingValueList(LIST_SHIFT, current_index=_vfoa.shift)
rs = RadioSetting("upper.vfoa.shift", "Xmit Shift", rx)
a_band.append(rs)
val = _vfoa.offset / 100000.0
rs = RadioSetting("upper.vfoa.offset", "Xmit Offset (MHz)",
RadioSettingValueFloat(0, 100.0, val, 0.001, 3))
# Allow zero value
rs.set_apply_callback(my_dbl2raw, _vfoa, "offset", 0)
a_band.append(rs)
tmp = str(_vfoa.step / 100.0)
rs = RadioSetting("step", "Freq step (kHz)",
RadioSettingValueList(LIST_STEPS, tmp))
rs.set_apply_callback(my_word2raw, _vfoa, "step", 100)
a_band.append(rs)
# BJ-318 upper band squelch
if self.MODEL == "BJ-318":
if _vfoa.sql == 0xFF:
valq = 0x04 # setting default squelch to 04
else:
valq = _vfoa.sql
sqla = RadioSetting("upper.vfoa.sql", "Squelch",
RadioSettingValueInteger(0, 9, valq))
a_band.append(sqla)
# BJ-318 upper band volume
if self.MODEL == "BJ-318":
bvolume_u = RadioSetting("upper.vfoa.bvol",
"Volume", RadioSettingValueInteger(
0, 15, _vfoa.bvol))
a_band.append(bvolume_u)
# BJ-318 Upper Screen Color
if self.MODEL == "BJ-318":
rs_u = RadioSettingValueList(LIST_COLOR318,
current_index=_lims.up_scr_color)
up_scr_color = RadioSetting("hello_lims.up_scr_color",
"Screen Color", rs_u)
a_band.append(up_scr_color)
# BJ-318 Upper band power
if self.MODEL == "BJ-318":
val_b = _vfoa.bpower # 2bits values 0,1,2= Low, Mid, High
rx = RadioSettingValueList(LIST_BPOWER, current_index=val_b)
power_u = RadioSetting("upper.vfoa.bpower", "Power", rx)
a_band.append(power_u)
# LOWER BAND SETTINGS
val = _vfob.frq_chn_mode // 2
rx = RadioSettingValueList(LIST_VFOMODE, current_index=val)
rs = RadioSetting("lower.vfob.frq_chn_mode", "Default Mode", rx)
rs.set_apply_callback(my_spcl, _vfob, "frq_chn_mode")
b_band.append(rs)
val = _vfob.chan_num + 1
rs = RadioSetting("lower.vfob.chan_num", "Initial Chan",
RadioSettingValueInteger(1, 128, val))
rs.set_apply_callback(my_adjraw, _vfob, "chan_num", -1)
b_band.append(rs)
val = _vfob.rxfreq / 100000.0
if (val < 400.0 or val > 490.0):
val = 446.0 # UHF calling
rs = RadioSetting("lower.vfob.rxfreq ", "Default Recv Freq (MHz)",
RadioSettingValueFloat(400.0, 490.0, val, 0.001, 5))
rs.set_apply_callback(my_dbl2raw, _vfob, "rxfreq")
b_band.append(rs)
tmp = my_tone_strn(_vfob, "is_rxdigtone", "rxdtcs_pol", "rx_tone")
rs = RadioSetting("rx_tone", "Default Recv CTCSS (Hz)",
RadioSettingValueList(LIST_CTCSS, tmp))
rs.set_apply_callback(my_set_tone, _vfob, "is_rxdigtone",
"rxdtcs_pol", "rx_tone")
b_band.append(rs)
rx = RadioSettingValueList(LIST_RECVMODE, current_index=_vfob.rx_mode)
rs = RadioSetting("lower.vfob.rx_mode", "Default Recv Mode", rx)
b_band.append(rs)
tmp = my_tone_strn(_vfob, "is_txdigtone", "txdtcs_pol", "tx_tone")
rs = RadioSetting("tx_tone", "Default Xmit CTCSS (Hz)",
RadioSettingValueList(LIST_CTCSS, tmp))
rs.set_apply_callback(my_set_tone, _vfob, "is_txdigtone",
"txdtcs_pol", "tx_tone")
b_band.append(rs)
rx = RadioSettingValueList(LIST_SIGNAL, current_index=_vfob.launch_sig)
rs = RadioSetting("lower.vfob.launch_sig", "Launch Signaling", rx)
b_band.append(rs)
rx = RadioSettingValueList(LIST_SIGNAL, current_index=_vfob.tx_end_sig)
rs = RadioSetting("lower.vfob.tx_end_sig", "Xmit End Signaling", rx)
b_band.append(rs)
rx = RadioSettingValueList(LIST_BW, current_index=_vfob.fm_bw)
rs = RadioSetting("lower.vfob.fm_bw", "Wide/Narrow Band", rx)
b_band.append(rs)
rs = RadioSetting("lower.vfob.cmp_nder", "Compander",
RadioSettingValueBoolean(bool(_vfob.cmp_nder)))
b_band.append(rs)
rs = RadioSetting("lower.vfob.scrm_blr", "Scrambler",
RadioSettingValueBoolean(bool(_vfob.scrm_blr)))
b_band.append(rs)
rx = RadioSettingValueList(LIST_SHIFT, current_index=_vfob.shift)
rs = RadioSetting("lower.vfob.shift", "Xmit Shift", rx)
b_band.append(rs)
val = _vfob.offset / 100000.0
rs = RadioSetting("lower.vfob.offset", "Xmit Offset (MHz)",
RadioSettingValueFloat(0, 100.0, val, 0.001, 3))
rs.set_apply_callback(my_dbl2raw, _vfob, "offset", 0)
b_band.append(rs)
tmp = str(_vfob.step / 100.0)
rs = RadioSetting("step", "Freq step (kHz)",
RadioSettingValueList(LIST_STEPS, tmp))
rs.set_apply_callback(my_word2raw, _vfob, "step", 100)
b_band.append(rs)
# BJ-318 lower band squelch
if self.MODEL == "BJ-318":
if _vfob.sql == 0xFF:
val_l = 0x04 # setting default squelch to 04
else:
val_l = _vfob.sql
sql_b = RadioSetting("lower.vfob.sql", "Squelch",
RadioSettingValueInteger(0, 9, val_l))
b_band.append(sql_b)
# BJ-318 lower band volume
if self.MODEL == "BJ-318":
bvolume_l = RadioSetting("lower.vfob.bvol",
"Volume", RadioSettingValueInteger(
0, 15, _vfob.bvol))
b_band.append(bvolume_l)
# BJ-318 Lower Screen Color
if self.MODEL == "BJ-318":
rs_l = RadioSettingValueList(LIST_COLOR318,
current_index=_lims.dn_scr_color)
dn_scr_color = RadioSetting("hello_lims.dn_scr_color",
"Screen Color", rs_l)
b_band.append(dn_scr_color)
# BJ-318 lower band power
if self.MODEL == "BJ-318":
val_l = _vfoa.bpower # 2bits values 0,1,2= Low, Mid, High
rx = RadioSettingValueList(LIST_BPOWER, current_index=val_l)
powera = RadioSetting("lower.vfob.bpower", "Power", rx)
b_band.append(powera)
# PowerOn & Freq Limits Settings
def chars2str(cary, knt):
"""Convert raw memory char array to a string: NOT a callback."""
stx = ""
for char in cary[0:knt]:
stx += chr(int(char))
return stx
def my_str2ary(setting, obj, atrba, atrbc):
"""Callback: convert 7-char string to char array with count."""
ary = ""
knt = 7
for j in range(6, -1, -1): # Strip trailing spaces
if str(setting.value)[j] == "" or str(setting.value)[j] == " ":
knt = knt - 1
else:
break
for j in range(0, 7, 1):
if j < knt:
ary += str(setting.value)[j]
else:
ary += chr(0xFF)
setattr(obj, atrba, ary)
setattr(obj, atrbc, knt)
return
# not used in BJ-318 startup screen
if self.MODEL != "BJ-318":
tmp = chars2str(_lims.hello1, _lims.hello1_cnt)
rs = RadioSetting("hello_lims.hello1", "Power-On Message 1",
RadioSettingValueString(0, 7, tmp))
rs.set_apply_callback(my_str2ary, _lims, "hello1", "hello1_cnt")
lims.append(rs)
# not used in BJ-318 startup screen
if self.MODEL != "BJ-318":
tmp = chars2str(_lims.hello2, _lims.hello2_cnt)
rs = RadioSetting("hello_lims.hello2", "Power-On Message 2",
RadioSettingValueString(0, 7, tmp))
rs.set_apply_callback(my_str2ary, _lims, "hello2", "hello2_cnt")
lims.append(rs)
# VALID_BANDS = [(136000000, 176000000),400000000, 490000000)]
lval = _lims.vhf_low / 100000.0
uval = _lims.vhf_high / 100000.0
if lval >= uval:
lval = 144.0
uval = 158.0
rs = RadioSetting("hello_lims.vhf_low", "Lower VHF Band Limit (MHz)",
RadioSettingValueFloat(136.0, 176.0, lval, 0.001, 3))
rs.set_apply_callback(my_dbl2raw, _lims, "vhf_low")
lims.append(rs)
rs = RadioSetting("hello_lims.vhf_high", "Upper VHF Band Limit (MHz)",
RadioSettingValueFloat(136.0, 176.0, uval, 0.001, 3))
rs.set_apply_callback(my_dbl2raw, _lims, "vhf_high")
lims.append(rs)
lval = _lims.uhf_low / 100000.0
uval = _lims.uhf_high / 100000.0
if lval >= uval:
lval = 420.0
uval = 470.0
rs = RadioSetting("hello_lims.uhf_low", "Lower UHF Band Limit (MHz)",
RadioSettingValueFloat(400.0, 490.0, lval, 0.001, 3))
rs.set_apply_callback(my_dbl2raw, _lims, "uhf_low")
lims.append(rs)
rs = RadioSetting("hello_lims.uhf_high", "Upper UHF Band Limit (MHz)",
RadioSettingValueFloat(400.0, 490.0, uval, 0.001, 3))
rs.set_apply_callback(my_dbl2raw, _lims, "uhf_high")
lims.append(rs)
# Codes and DTMF Groups Settings
def make_dtmf(ary, knt):
"""Generate the DTMF code 1-8, NOT a callback."""
tmp = ""
if knt > 0 and knt != 0xff:
for val in ary[0:knt]:
if val > 0 and val <= 9:
tmp += chr(val + 48)
elif val == 0x0a:
tmp += "0"
elif val == 0x0d:
tmp += "A"
elif val == 0x0e:
tmp += "B"
elif val == 0x0f:
tmp += "C"
elif val == 0x00:
tmp += "D"
elif val == 0x0b:
tmp += "*"
elif val == 0x0c:
tmp += "#"
else:
msg = ("Invalid Character. Must be: 0-9,A,B,C,D,*,#")
raise InvalidValueError(msg)
return tmp
def my_dtmf2raw(setting, obj, atrba, atrbc, syz=7):
"""Callback: DTMF Code; sends 5 or 7-byte string."""
draw = []
knt = syz
for j in range(syz - 1, -1, -1): # Strip trailing spaces
if str(setting.value)[j] == "" or str(setting.value)[j] == " ":
knt = knt - 1
else:
break
for j in range(0, syz):
bx = str(setting.value)[j]
obx = ord(bx)
dig = 0x0ff
if j < knt and knt > 0: # (Else) is pads
if bx == "0":
dig = 0x0a
elif bx == "A":
dig = 0x0d
elif bx == "B":
dig = 0x0e
elif bx == "C":
dig = 0x0f
elif bx == "D":
dig = 0x00
elif bx == "*":
dig = 0x0b
elif bx == "#":
dig = 0x0c
elif obx >= 49 and obx <= 57:
dig = obx - 48
else:
msg = ("Must be: 0-9,A,B,C,D,*,#")
raise InvalidValueError(msg)
# - End if/elif/else for bx
# - End if J<=knt
draw.append(dig) # Generate string of bytes
# - End for j
setattr(obj, atrba, draw)
setattr(obj, atrbc, knt)
return
tmp = make_dtmf(_codes.native_id_code, _codes.native_id_cnt)
rs = RadioSetting("codes.native_id_code", "Native ID Code",
RadioSettingValueString(0, 7, tmp))
rs.set_apply_callback(my_dtmf2raw, _codes, "native_id_code",
"native_id_cnt", 7)
codes.append(rs)
tmp = make_dtmf(_codes.master_id_code, _codes.master_id_cnt)
rs = RadioSetting("codes.master_id_code", "Master Control ID Code",
RadioSettingValueString(0, 7, tmp))
rs.set_apply_callback(my_dtmf2raw, _codes, "master_id_code",
"master_id_cnt", 7)
codes.append(rs)
tmp = make_dtmf(_codes.alarm_code, _codes.alarm_cnt)
rs = RadioSetting("codes.alarm_code", "Alarm Code",
RadioSettingValueString(0, 5, tmp))
rs.set_apply_callback(my_dtmf2raw, _codes, "alarm_code",
"alarm_cnt", 5)
codes.append(rs)
tmp = make_dtmf(_codes.id_disp_code, _codes.id_disp_cnt)
rs = RadioSetting("codes.id_disp_code", "Identify Display Code",
RadioSettingValueString(0, 5, tmp))
rs.set_apply_callback(my_dtmf2raw, _codes, "id_disp_code",
"id_disp_cnt", 5)
codes.append(rs)
tmp = make_dtmf(_codes.revive_code, _codes.revive_cnt)
rs = RadioSetting("codes.revive_code", "Revive Code",
RadioSettingValueString(0, 5, tmp))
rs.set_apply_callback(my_dtmf2raw, _codes, "revive_code",
"revive_cnt", 5)
codes.append(rs)
tmp = make_dtmf(_codes.stun_code, _codes.stun_cnt)
rs = RadioSetting("codes.stun_code", "Remote Stun Code",
RadioSettingValueString(0, 5, tmp))
rs.set_apply_callback(my_dtmf2raw, _codes, "stun_code",
"stun_cnt", 5)
codes.append(rs)
tmp = make_dtmf(_codes.kill_code, _codes.kill_cnt)
rs = RadioSetting("codes.kill_code", "Remote KILL Code",
RadioSettingValueString(0, 5, tmp))
rs.set_apply_callback(my_dtmf2raw, _codes, "kill_code",
"kill_cnt", 5)
codes.append(rs)
tmp = make_dtmf(_codes.monitor_code, _codes.monitor_cnt)
rs = RadioSetting("codes.monitor_code", "Monitor Code",
RadioSettingValueString(0, 5, tmp))
rs.set_apply_callback(my_dtmf2raw, _codes, "monitor_code",
"monitor_cnt", 5)
codes.append(rs)
val = _codes.state_now
if val > 2:
val = 0
rx = RadioSettingValueList(LIST_STATE, current_index=val)
rs = RadioSetting("codes.state_now", "Current State", rx)
codes.append(rs)
dtm = make_dtmf(_dtmf.dtmf1, _dtmf.dtmf1_cnt)
rs = RadioSetting("dtmf_tab.dtmf1", "DTMF1 String",
RadioSettingValueString(0, 7, dtm))
rs.set_apply_callback(my_dtmf2raw, _dtmf, "dtmf1", "dtmf1_cnt")
codes.append(rs)
dtm = make_dtmf(_dtmf.dtmf2, _dtmf.dtmf2_cnt)
rs = RadioSetting("dtmf_tab.dtmf2", "DTMF2 String",
RadioSettingValueString(0, 7, dtm))
rs.set_apply_callback(my_dtmf2raw, _dtmf, "dtmf2", "dtmf2_cnt")
codes.append(rs)
dtm = make_dtmf(_dtmf.dtmf3, _dtmf.dtmf3_cnt)
rs = RadioSetting("dtmf_tab.dtmf3", "DTMF3 String",
RadioSettingValueString(0, 7, dtm))
rs.set_apply_callback(my_dtmf2raw, _dtmf, "dtmf3", "dtmf3_cnt")
codes.append(rs)
dtm = make_dtmf(_dtmf.dtmf4, _dtmf.dtmf4_cnt)
rs = RadioSetting("dtmf_tab.dtmf4", "DTMF4 String",
RadioSettingValueString(0, 7, dtm))
rs.set_apply_callback(my_dtmf2raw, _dtmf, "dtmf4", "dtmf4_cnt")
codes.append(rs)
dtm = make_dtmf(_dtmf.dtmf5, _dtmf.dtmf5_cnt)
rs = RadioSetting("dtmf_tab.dtmf5", "DTMF5 String",
RadioSettingValueString(0, 7, dtm))
rs.set_apply_callback(my_dtmf2raw, _dtmf, "dtmf5", "dtmf5_cnt")
codes.append(rs)
dtm = make_dtmf(_dtmf.dtmf6, _dtmf.dtmf6_cnt)
rs = RadioSetting("dtmf_tab.dtmf6", "DTMF6 String",
RadioSettingValueString(0, 7, dtm))
rs.set_apply_callback(my_dtmf2raw, _dtmf, "dtmf6", "dtmf6_cnt")
codes.append(rs)
dtm = make_dtmf(_dtmf.dtmf7, _dtmf.dtmf7_cnt)
rs = RadioSetting("dtmf_tab.dtmf7", "DTMF7 String",
RadioSettingValueString(0, 7, dtm))
rs.set_apply_callback(my_dtmf2raw, _dtmf, "dtmf7", "dtmf7_cnt")
codes.append(rs)
dtm = make_dtmf(_dtmf.dtmf8, _dtmf.dtmf8_cnt)
rs = RadioSetting("dtmf_tab.dtmf8", "DTMF8 String",
RadioSettingValueString(0, 7, dtm))
rs.set_apply_callback(my_dtmf2raw, _dtmf, "dtmf8", "dtmf8_cnt")
codes.append(rs)
return group # END get_settings()
def set_settings(self, settings):
_settings = self._memobj.settings
_mem = self._memobj
for element in settings:
if not isinstance(element, RadioSetting):
self.set_settings(element)
continue
else:
try:
name = element.get_name()
if "." in name:
bits = name.split(".")
obj = self._memobj
for bit in bits[:-1]:
if "/" in bit:
bit, index = bit.split("/", 1)
index = int(index)
obj = getattr(obj, bit)[index]
else:
obj = getattr(obj, bit)
setting = bits[-1]
else:
obj = _settings
setting = element.get_name()
if element.has_apply_callback():
LOG.debug("Using apply callback")
element.run_apply_callback()
elif element.value.get_mutable():
LOG.debug("Setting %s = %s" % (setting, element.value))
setattr(obj, setting, element.value)
except Exception:
LOG.debug(element.get_name())
raise
@classmethod
def match_model(cls, filedata, filename):
match_size = False
match_model = False
# Testing the file data size
if len(filedata) == MEM_SIZE + 8:
match_size = True
# Testing the firmware model fingerprint
match_model = model_match(cls, filedata)
if match_size and match_model:
return True
else:
return False
class LT725UVUpper(LT725UV):
VARIANT = "Upper"
_vfo = "upper"
class LT725UVLower(LT725UV):
VARIANT = "Lower"
_vfo = "lower"
class Zastone(chirp_common.Alias):
"""Declare BJ-218 alias for Zastone BJ-218."""
VENDOR = "Zastone"
MODEL = "BJ-218"
class Hesenate(chirp_common.Alias):
"""Declare BJ-218 alias for Hesenate BJ-218."""
VENDOR = "Hesenate"
MODEL = "BJ-218"
class Baojie218Upper(LT725UVUpper):
VENDOR = "Baojie"
MODEL = "BJ-218"
class Baojie218Lower(LT725UVLower):
VENDOR = "Baojie"
MODEL = "BJ-218"
@directory.register
class Baojie218(LT725UV):
"""Baojie BJ-218"""
VENDOR = "Baojie"
MODEL = "BJ-218"
ALIASES = [Zastone, Hesenate, ]
def get_sub_devices(self):
return [Baojie218Upper(self._mmap), Baojie218Lower(self._mmap)]
class Baojie318Upper(LT725UVUpper):
VENDOR = "Baojie"
MODEL = "BJ-318"
class Baojie318Lower(LT725UVLower):
VENDOR = "Baojie"
MODEL = "BJ-318"
@directory.register
class Baojie318(LT725UV):
"""Baojie BJ-318"""
VENDOR = "Baojie"
MODEL = "BJ-318"
def get_sub_devices(self):
return [Baojie318Upper(self._mmap), Baojie318Lower(self._mmap)]
chirp-20261030/chirp/drivers/mml_jc8810.py 0000644 0001750 0001750 00000155770 15172631605 016776 0 ustar hibby hibby # Copyright 2023 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 .
import logging
import struct
from chirp import (
bitwise,
chirp_common,
directory,
errors,
memmap,
util,
)
from chirp.settings import (
RadioSetting,
RadioSettingGroup,
RadioSettings,
RadioSettingValueBoolean,
RadioSettingValueInteger,
RadioSettingValueList,
RadioSettingValueString,
RadioSettingValueMap,
)
LOG = logging.getLogger(__name__)
MEM_FORMAT = """
struct {
lbcd rxfreq[4]; // 0-3
lbcd txfreq[4]; // 4-7
ul16 rxtone; // 8-9
ul16 txtone; // A-B
u8 unknown1:4, // C
scode:4; // Signaling
u8 unknown2:6, // D
pttid:2; // PTT-ID
u8 scramble:6, // Scramble
txpower:2; // Power Level 0 = H, 1 = L, 2 = M
u8 unknown4:1, // F
narrow:1, // Bandwidth 0 = Wide, 1 = Narrow
encrypt:2, // Encrypt
bcl:1, // BCL
scan:1, // Scan 0 = Skip, 1 = Scan
unknown5:1, //
learning:1; // Learning
lbcd code[3]; // 0-2 Code
u8 unknown6; // 3
char name[12]; // 4-F 12-character Alpha Tag
} memory[%d];
#seekto 0x9000;
struct {
u8 unused:4, // 9000
sql:4; // Squelch Level
u8 unused_9001:6, // 9001
save:2; // Battery Save
u8 unused_9002:4, // 9002
vox:4; // VOX Level
u8 unused_9003:4, // 9003
abr:4; // Auto BackLight
u8 unused_9004:7, // 9004
tdr:1; // TDR
u8 unused_9005:5, // 9005
tot:3; // Time-out Timer
u8 unused_9006:7, // 9006
beep:1; // Beep
u8 unused_9007:7, // 9007
voice:1; // Voice Prompt
u8 unused_9008:7, // 9008
language:1; // Language
u8 unused_9009:6, // 9009
dtmfst:2; // DTMF ST
u8 unused_900a:6, // 900A
screv:2; // Scan Mode
u8 unused_900B:4, // 900B
pttid:4; // PTT-ID
u8 unused_900c:5, // 900C
pttlt:3; // PTT Delay
u8 unused_900d:6, // 900D
mdfa:2; // Channel_A Display
u8 unused_900e:6, // 900E
mdfb:2; // Channel_B Display
u8 unknown_900f; // 900F
u8 unused_9010:4, // 9010
autolk:4; // Key Auto Lock
u8 unused_9011:6, // 9011
almode:2; // Alarm Mode
u8 unused_9012:7, // 9012
alarmsound:1; // Alarm Sound
u8 unused_9013:6, // 9013
dualtx:2; // Dual TX
u8 unused_9014:7, // 9014
ste:1; // Tail Noise Clear
u8 unused_9015:4, // 9015
rpste:4; // RPT Noise Clear
u8 unused_9016:4, // 9016
rptrl:4; // RPT Noise Delay
u8 unused_9017:6, // 9017
roger:2; // Roger
u8 unknown_9018; // 9018
u8 unused_9019:7, // 9019
fmradio:1; // FM Radio
u8 unused_901a1:3, // 901A
vfomrb:1, // WorkMode B
unused_901a2:3, //
vfomra:1; // WorkMode A
u8 unused_901b:7, // 901B
kblock:1; // KB Lock
u8 unused_901c:7, // 901C
ponmsg:1; // Power On Msg
u8 unknown_901d; // 901D
u8 unused_901e:6, // 901E
tone:2; // Pilot
u8 unknown_901f; // 901F
u8 unused_9020:4, // 9020
voxd:4; // VOX Delay
u8 unused_9021:4, // 9021
menuquit:4; // Menu Auto Quit
u8 unused_9022:7, // 9022
tailcode:1; // Tail Code (RT-470L)
u8 unknown_9023; // 9023
u8 unlock_sw_ch; // 9024 UNLOCK SW CH (A36plus 8w)
u8 unknown_9025; // 9025
u8 unknown_9026; // 9026
u8 unknown_9027; // 9027
u8 unknown_9028; // 9028
u8 unused_9029:6, // 9029
qtsave:2; // QT Save Type
u8 ani; // 902A ANI
u8 skey2_sp; // 902B Skey2 Short
u8 skey2_lp; // 902C Skey2 Long
u8 skey3_sp; // 902D Skey3 Short
u8 topkey_sp; // 902E Top Key (RT-470L)
u8 unused_902f:6, // 902F
rxendtail:2; // RX END TAIL (RT-470)
// TAIL PHASE (A36plus)
u8 skey3_lp; // 9030 Skey3 Long (RT-470L)
// RX END TAIL (A36plus)
u8 unknown_9031; // 9031
u8 unknown_9032; // 9032
u8 unknown_9033; // 9033
u8 unknown_9034; // 9034
u8 unknown_9035; // 9035
u8 unknown_9036; // 9036
u8 unknown_9037; // 9037
u8 unknown_9038; // 9038
u8 unknown_9039; // 9039
u8 unknown_903a; // 903a
u8 single_mode; // 903b SINGLE MODE (A36plus 8w)
u8 dis_s_table; // 903b DIS S TABLE (A36plus 8w)
} settings;
#seekto 0xA006;
struct {
u8 unknown_A006; // A006
u8 unused_a007:5, // A007
dtmfon:3; // DTMF Speed (on time)
u8 unused_a008:5, // A008
dtmfoff:3; // DTMF Speed (off time)
} dtmf;
#seekto 0xA020;
struct {
u8 code[5]; // 5-character DTMF Encoder Groups
u8 unused[11];
} pttid[15];
#seekto 0xB000;
struct {
u8 code[3]; // 3-character ANI Code Groups
} anicodes[60];
#seekto 0xB0C0;
struct {
char name[10]; // 10-character ANI Code Group Names
u8 unused[6];
} aninames[%d];
"""
MEM_FORMAT_A36PLUS = """
#seekto 0xB200;
struct {
char name[10]; // 10-character Custom CH Names (Talkpod A36plus)
u8 unused[6];
} customnames[30];
"""
CMD_ACK = b"\x06"
DTCS = tuple(sorted(chirp_common.DTCS_CODES + (645,)))
DTMF_CHARS = "0123456789 *#ABCD"
TXPOWER_HIGH = 0x00
TXPOWER_LOW = 0x01
TXPOWER_MID = 0x02
ABR_LIST = ["On", "5 seconds", "10 seconds", "15 seconds", "20 seconds",
"30 seconds", "1 minute", "2 minutes", "3 minutes"]
ALMODE_LIST = ["Site", "Tone", "Code"]
AUTOLK_LIST = ["Off"] + ABR_LIST[1:4]
DTMFSPEED_LIST = ["50 ms", "100 ms", "200 ms", "300 ms", "500 ms"]
DTMFST_LIST = ["Off", "KeyBoard Side Tone", "ANI Side Tone", "KB ST + ANI ST"]
DUALTX_LIST = ["Off", "A", "B"]
ENCRYPT_LIST = ["Off", "DCP1", "DCP2", "DCP3"]
LANGUAGE_LIST = ["English", "Chinese"]
MDF_LIST = ["Name", "Frequency", "Channel"]
MENUQUIT_LIST = ["%s seconds" % x for x in range(5, 55, 5)] + ["60 seconds"]
OFF1TO9_LIST = ["Off"] + ["%s" % x for x in range(1, 10)]
PONMSG_LIST = ["Logo", "Voltage"]
PTTID_LIST = ["Off", "BOT", "EOT", "Both"]
PTTIDCODE_LIST = ["%s" % x for x in range(1, 16)]
PTTLT_LIST = ["None", "100 ms"] + \
["%s ms" % x for x in range(200, 1200, 200)]
QTSAVE_LIST = ["All", "RX", "TX"]
ROGER_LIST = ["OFF", "BEEP", "TONE1200"]
RPSTE_LIST = ["Off"] + ["%s ms" % x for x in range(100, 1100, 100)]
SAVE_LIST = ["Off", "Normal", "Super", "Deep"]
SCREV_LIST = ["Time (TO)", "Carrier (CO)", "Search (SE)"]
TAILCODE_LIST = ["55 Hz", "62.5 Hz"]
TAILPHASE_LIST = ["None", "120 Shift", "180 Shift", "240 Shift"]
TONERXEND_LIST = ["Off", "MDC-1200"]
TONE_LIST = ["1000 Hz", "1450 Hz", "1750 Hz", "2100 Hz"]
TOT_LIST = ["Off", "30 seconds", "60 seconds", "120 seconds", "240 seconds",
"480 seconds"]
VOXD_LIST = ["%s seconds" % str(x / 10) for x in range(5, 21)]
WORKMODE_LIST = ["VFO Mode", "Channel Mode"]
ALL_SKEY_CHOICES = ["OFF",
"FM Radio",
"TX Power Level",
"Scan",
"Search",
"Flashlight",
"NOAA Weather",
"Monitor",
"PTT B",
"SOS",
"DTMF",
"REVERSE",
"REMOTE Scan"]
ALL_SKEY_VALUES = [0xFF,
0x07,
0x0A,
0x1C,
0x1D,
0x08,
0x0C,
0x05,
0x01,
0x03,
0x2A,
0x2D,
0x23]
SCRAMBLE_VALUEMAP = [("Off", 0x00), ("SCRAM1", 0x04), ("SCRAM2", 0x08)]
def _enter_programming_mode(radio):
serial = radio.pipe
exito = False
for i in range(0, 5):
serial.write(radio._magic)
ack = serial.read(1)
try:
if ack == CMD_ACK:
exito = True
break
except Exception:
LOG.debug("Attempt #%s, failed, trying again" % i)
pass
# check if we had EXITO
if exito is False:
if not ack:
raise errors.RadioNoResponse()
msg = "The radio did not accept program mode after five tries.\n"
msg += "Check you interface cable and power cycle your radio."
raise errors.RadioError(msg)
try:
serial.write(b"F")
ident = serial.read(8)
except Exception:
raise errors.RadioError("Error communicating with radio")
if ident not in radio._fingerprint:
LOG.debug(util.hexprint(ident))
raise errors.RadioError("Radio returned unknown identification string")
if radio.MODEL == "RT-470X":
if ident in radio._fingerprint_pcb1:
LOG.info("Radtel RT-470X - original pcb")
radio.RT470X_ORIG = True
elif ident in radio._fingerprint_pcb2:
LOG.info("Radtel RT-470X - pcb2")
radio.RT470X_ORIG = False
def _exit_programming_mode(radio):
serial = radio.pipe
try:
serial.write(b"E")
except Exception:
raise errors.RadioError("Radio refused to exit programming mode")
def _read_block(radio, block_addr, block_size):
serial = radio.pipe
cmd = struct.pack(">cHb", b'R', block_addr, block_size)
expectedresponse = b"R" + cmd[1:]
LOG.debug("Reading block %04x..." % (block_addr))
try:
serial.write(cmd)
response = serial.read(4 + block_size)
if response[:4] != expectedresponse:
raise Exception("Error reading block %04x." % (block_addr))
block_data = response[4:]
except Exception:
raise errors.RadioError("Failed to read block at %04x" % block_addr)
return block_data
def _write_block(radio, block_addr, block_size):
serial = radio.pipe
cmd = struct.pack(">cHb", b'W', block_addr, block_size)
data = radio.get_mmap()[block_addr:block_addr + block_size]
LOG.debug("Writing Data:")
LOG.debug(util.hexprint(cmd + data))
try:
serial.write(cmd + data)
if serial.read(1) != CMD_ACK:
raise Exception("No ACK")
except Exception:
raise errors.RadioError("Failed to send block "
"to radio at %04x" % block_addr)
def do_download(radio):
LOG.debug("download")
_enter_programming_mode(radio)
data = b""
status = chirp_common.Status()
status.msg = "Cloning from radio"
status.cur = 0
status.max = radio._memsize
for addr in range(0, radio._memsize, radio.BLOCK_SIZE):
status.cur = addr + radio.BLOCK_SIZE
radio.status_fn(status)
block = _read_block(radio, addr, radio.BLOCK_SIZE)
data += block
LOG.debug("Address: %04x" % addr)
LOG.debug(util.hexprint(block))
_exit_programming_mode(radio)
return memmap.MemoryMapBytes(data)
def do_upload(radio):
status = chirp_common.Status()
status.msg = "Uploading to radio"
_enter_programming_mode(radio)
status.cur = 0
status.max = radio._memsize
for start_addr, end_addr in radio._ranges:
for addr in range(start_addr, end_addr, radio.BLOCK_SIZE_UP):
status.cur = addr + radio.BLOCK_SIZE_UP
radio.status_fn(status)
_write_block(radio, addr, radio.BLOCK_SIZE_UP)
_exit_programming_mode(radio)
class JC8810base(chirp_common.CloneModeRadio):
"""MML JC-8810"""
VENDOR = "MML"
MODEL = "JC-8810base"
BAUD_RATE = 57600
BLOCK_SIZE = 0x40
BLOCK_SIZE_UP = 0x40
POWER_LEVELS = [chirp_common.PowerLevel("H", watts=10.00),
chirp_common.PowerLevel("M", watts=8.00),
chirp_common.PowerLevel("L", watts=4.00)]
VALID_BANDS = [(108000000, 136000000),
(136000000, 180000000),
(200000000, 260000000),
(330000000, 400000000),
(400000000, 520000000)]
_magic = b"PROGRAMJC81U"
_fingerprint = [b"\x00\x00\x00\x26\x00\x20\xD8\x04",
b"\x00\x00\x00\x42\x00\x20\xF0\x04",
b"\x00\x00\x00\x4A\x00\x20\xF8\x04"]
_ranges = [
(0x0000, 0x2000),
(0x8000, 0x8040),
(0x9000, 0x9040),
(0xA000, 0xA140),
(0xB000, 0xB300)
]
_memsize = 0xB300
_upper = 256
_aninames = 30
_mem_params = (_upper, # number of channels
_aninames, # number of aninames
)
_valid_chars = chirp_common.CHARSET_ALPHANUMERIC + \
"`~!@#$%^&*()-=_+[]\\{}|;':\",./<>?"
def get_features(self):
rf = chirp_common.RadioFeatures()
rf.has_settings = True
rf.has_bank = False
rf.has_ctone = True
rf.has_cross = True
rf.has_rx_dtcs = True
rf.has_tuning_step = False
rf.can_odd_split = True
rf.has_name = True
rf.valid_name_length = 12
rf.valid_characters = self._valid_chars
rf.valid_skips = ["", "S"]
rf.valid_tmodes = ["", "Tone", "TSQL", "DTCS", "Cross"]
rf.valid_cross_modes = ["Tone->Tone", "Tone->DTCS", "DTCS->Tone",
"->Tone", "->DTCS", "DTCS->", "DTCS->DTCS"]
rf.valid_power_levels = self.POWER_LEVELS
rf.valid_duplexes = ["", "-", "+", "split", "off"]
rf.valid_modes = ["FM", "NFM"] # 25 kHz, 12.5 kHz.
rf.valid_dtcs_codes = DTCS
rf.memory_bounds = (1, self._upper)
rf.valid_tuning_steps = [2.5, 5., 6.25, 8.33, 10., 12.5, 20., 25., 50.]
rf.valid_bands = self.VALID_BANDS
return rf
def process_mmap(self):
self._memobj = bitwise.parse(MEM_FORMAT % self._mem_params, self._mmap)
def sync_in(self):
"""Download from radio"""
try:
data = do_download(self)
except errors.RadioError:
# Pass through any real errors we raise
raise
except Exception:
# If anything unexpected happens, make sure we raise
# a RadioError and log the problem
LOG.exception('Unexpected error during download')
raise errors.RadioError('Unexpected error communicating '
'with the radio')
self._mmap = data
self.process_mmap()
def sync_out(self):
"""Upload to radio"""
try:
do_upload(self)
except errors.RadioError:
raise
except Exception:
# If anything unexpected happens, make sure we raise
# a RadioError and log the problem
LOG.exception('Unexpected error during upload')
raise errors.RadioError('Unexpected error communicating '
'with the radio')
def get_memory(self, number):
"""Get the mem representation from the radio image"""
_mem = self._memobj.memory[number - 1]
# Create a high-level memory object to return to the UI
mem = chirp_common.Memory()
# Memory number
mem.number = number
if _mem.get_raw()[:1] == b"\xFF":
mem.empty = True
return mem
# Freq and offset
mem.freq = int(_mem.rxfreq) * 10
# tx freq can be blank
if _mem.txfreq.get_raw() == b"\xFF\xFF\xFF\xFF":
# TX freq not set
mem.offset = 0
mem.duplex = "off"
else:
# TX freq set
offset = (int(_mem.txfreq) * 10) - mem.freq
if offset != 0:
if chirp_common.is_split(self.get_features().valid_bands,
mem.freq, int(_mem.txfreq) * 10):
mem.duplex = "split"
mem.offset = int(_mem.txfreq) * 10
elif offset < 0:
mem.offset = abs(offset)
mem.duplex = "-"
elif offset > 0:
mem.offset = offset
mem.duplex = "+"
else:
mem.offset = 0
for char in _mem.name:
if str(char) == "\xFF":
char = " " # may have 0xFF mid-name
mem.name += str(char)
mem.name = mem.name.rstrip().replace('\x00', '')
dtcs_pol = ["N", "N"]
if _mem.txtone in [0, 0xFFFF]:
txmode = ""
elif _mem.txtone >= 0x0258:
txmode = "Tone"
mem.rtone = int(_mem.txtone) / 10.0
elif _mem.txtone <= 0x0258:
txmode = "DTCS"
if _mem.txtone > 0x69:
index = _mem.txtone - 0x6A
dtcs_pol[0] = "R"
else:
index = _mem.txtone - 1
mem.dtcs = DTCS[index]
else:
LOG.warn("Bug: txtone is %04x" % _mem.txtone)
if _mem.rxtone in [0, 0xFFFF]:
rxmode = ""
elif _mem.rxtone >= 0x0258:
rxmode = "Tone"
mem.ctone = int(_mem.rxtone) / 10.0
elif _mem.rxtone <= 0x0258:
rxmode = "DTCS"
if _mem.rxtone >= 0x6A:
index = _mem.rxtone - 0x6A
dtcs_pol[1] = "R"
else:
index = _mem.rxtone - 1
mem.rx_dtcs = DTCS[index]
else:
LOG.warn("Bug: rxtone is %04x" % _mem.rxtone)
if txmode == "Tone" and not rxmode:
mem.tmode = "Tone"
elif txmode == rxmode and txmode == "Tone" and mem.rtone == mem.ctone:
mem.tmode = "TSQL"
elif txmode == rxmode and txmode == "DTCS" and mem.dtcs == mem.rx_dtcs:
mem.tmode = "DTCS"
elif rxmode or txmode:
mem.tmode = "Cross"
mem.cross_mode = "%s->%s" % (txmode, rxmode)
mem.dtcs_polarity = "".join(dtcs_pol)
if not _mem.scan:
mem.skip = "S"
_levels = self.POWER_LEVELS
if self.MODEL in ["A36plus", "A36plus_8w", "UV-A37", "AR-730"]:
if _mem.txpower == TXPOWER_HIGH:
mem.power = _levels[0]
elif _mem.txpower == TXPOWER_LOW:
mem.power = _levels[1]
else:
LOG.error('%s: get_mem: unhandled power level: 0x%02x' %
(mem.name, _mem.txpower))
else:
if _mem.txpower == TXPOWER_HIGH:
mem.power = _levels[0]
elif _mem.txpower == TXPOWER_MID:
mem.power = _levels[1]
elif _mem.txpower == TXPOWER_LOW:
mem.power = _levels[2]
else:
LOG.error('%s: get_mem: unhandled power level: 0x%02x' %
(mem.name, _mem.txpower))
mem.mode = _mem.narrow and "NFM" or "FM"
mem.extra = RadioSettingGroup("Extra", "extra")
# Encryption
rs = RadioSettingValueList(ENCRYPT_LIST, current_index=_mem.encrypt)
rset = RadioSetting("encrypt", "Encryption", rs)
mem.extra.append(rset)
# Scramble
rs = RadioSettingValueMap(SCRAMBLE_VALUEMAP, _mem.scramble)
rset = RadioSetting("scramble", "Scramble", rs)
mem.extra.append(rset)
# BCL (Busy Channel Lockout)
rs = RadioSettingValueBoolean(_mem.bcl)
rset = RadioSetting("bcl", "BCL", rs)
mem.extra.append(rset)
# PTT-ID
rs = RadioSettingValueList(PTTID_LIST, current_index=_mem.pttid)
rset = RadioSetting("pttid", "PTT ID", rs)
mem.extra.append(rset)
# Signal (DTMF Encoder Group #)
rs = RadioSettingValueList(PTTIDCODE_LIST, current_index=_mem.scode)
rset = RadioSetting("scode", "PTT ID Code", rs)
mem.extra.append(rset)
return mem
def set_memory(self, mem):
_mem = self._memobj.memory[mem.number - 1]
if mem.empty:
_mem.set_raw("\xff" * 32)
return
_mem.set_raw("\x00" * 16 + "\xFF" * 16)
_mem.rxfreq = mem.freq / 10
if mem.duplex == "off":
_mem.txfreq.fill_raw(b"\xFF")
elif mem.duplex == "split":
_mem.txfreq = mem.offset / 10
elif mem.duplex == "+":
_mem.txfreq = (mem.freq + mem.offset) / 10
elif mem.duplex == "-":
_mem.txfreq = (mem.freq - mem.offset) / 10
else:
_mem.txfreq = mem.freq / 10
_namelength = self.get_features().valid_name_length
for i in range(_namelength):
try:
_mem.name[i] = mem.name[i]
except IndexError:
_mem.name[i] = "\xFF"
rxmode = txmode = ""
if mem.tmode == "Tone":
_mem.txtone = int(mem.rtone * 10)
_mem.rxtone = 0
elif mem.tmode == "TSQL":
_mem.txtone = int(mem.ctone * 10)
_mem.rxtone = int(mem.ctone * 10)
elif mem.tmode == "DTCS":
rxmode = txmode = "DTCS"
_mem.txtone = DTCS.index(mem.dtcs) + 1
_mem.rxtone = DTCS.index(mem.dtcs) + 1
elif mem.tmode == "Cross":
txmode, rxmode = mem.cross_mode.split("->", 1)
if txmode == "Tone":
_mem.txtone = int(mem.rtone * 10)
elif txmode == "DTCS":
_mem.txtone = DTCS.index(mem.dtcs) + 1
else:
_mem.txtone = 0
if rxmode == "Tone":
_mem.rxtone = int(mem.ctone * 10)
elif rxmode == "DTCS":
_mem.rxtone = DTCS.index(mem.rx_dtcs) + 1
else:
_mem.rxtone = 0
else:
_mem.rxtone = 0
_mem.txtone = 0
if txmode == "DTCS" and mem.dtcs_polarity[0] == "R":
_mem.txtone += 0x69
if rxmode == "DTCS" and mem.dtcs_polarity[1] == "R":
_mem.rxtone += 0x69
_mem.scan = mem.skip != "S"
_mem.narrow = mem.mode == "NFM"
_levels = self.POWER_LEVELS
if self.MODEL in ["A36plus", "A36plus_8w", "UV-A37", "AR-730"]:
if mem.power is None:
_mem.txpower = TXPOWER_HIGH
elif mem.power == _levels[0]:
_mem.txpower = TXPOWER_HIGH
elif mem.power == _levels[1]:
_mem.txpower = TXPOWER_LOW
else:
LOG.error('%s: set_mem: unhandled power level: %s' %
(mem.name, mem.power))
else:
if mem.power is None:
_mem.txpower = TXPOWER_HIGH
elif mem.power == _levels[0]:
_mem.txpower = TXPOWER_HIGH
elif mem.power == _levels[1]:
_mem.txpower = TXPOWER_MID
elif mem.power == _levels[2]:
_mem.txpower = TXPOWER_LOW
else:
LOG.error('%s: set_mem: unhandled power level: %s' %
(mem.name, mem.power))
for setting in mem.extra:
if setting.get_name() == "scramble_type":
setattr(_mem, setting.get_name(), int(setting.value) + 8)
setattr(_mem, "scramble_type2", int(setting.value) + 8)
else:
setattr(_mem, setting.get_name(), setting.value)
def get_settings(self):
_dtmf = self._memobj.dtmf
_settings = self._memobj.settings
basic = RadioSettingGroup("basic", "Basic Settings")
group = RadioSettings(basic)
# Menu 12: TOT
rs = RadioSettingValueList(TOT_LIST, current_index=_settings.tot)
rset = RadioSetting("tot", "Time Out Timer", rs)
basic.append(rset)
# Menu 00: SQL
rs = RadioSettingValueInteger(0, 9, _settings.sql)
rset = RadioSetting("sql", "Squelch Level", rs)
basic.append(rset)
# Menu 13: VOX
rs = RadioSettingValueList(OFF1TO9_LIST, current_index=_settings.vox)
rset = RadioSetting("vox", "VOX", rs)
basic.append(rset)
# Menu 39: VOX DELAY
rs = RadioSettingValueList(VOXD_LIST, current_index=_settings.voxd)
rset = RadioSetting("voxd", "VOX Delay", rs)
basic.append(rset)
# Menu 15: VOICE
rs = RadioSettingValueBoolean(_settings.voice)
rset = RadioSetting("voice", "Voice Prompts", rs)
basic.append(rset)
if self.MODEL not in ["A36plus", "A36plus_8w", "UV-A37", "AR-730"]:
# Menu 17: LANGUAGE
rs = RadioSettingValueList(LANGUAGE_LIST,
current_index=_settings.language)
rset = RadioSetting("language", "Voice", rs)
basic.append(rset)
# Menu 23: ABR
rs = RadioSettingValueList(ABR_LIST, current_index=_settings.abr)
rset = RadioSetting("abr", "Auto BackLight", rs)
basic.append(rset)
# Work Mode A
rs = RadioSettingValueList(WORKMODE_LIST,
current_index=_settings.vfomra)
rset = RadioSetting("vfomra", "Work Mode A", rs)
basic.append(rset)
# Work Mode B
rs = RadioSettingValueList(WORKMODE_LIST,
current_index=_settings.vfomrb)
rset = RadioSetting("vfomrb", "Work Mode B", rs)
basic.append(rset)
# Menu 19: SC-REV
rs = RadioSettingValueList(SCREV_LIST, current_index=_settings.screv)
rset = RadioSetting("screv", "Scan Resume Method", rs)
basic.append(rset)
# Menu 10: SAVE
rs = RadioSettingValueList(SAVE_LIST,
current_index=_settings.save)
rset = RadioSetting("save", "Battery Save Mode", rs)
basic.append(rset)
# Menu 42: MDF-A
rs = RadioSettingValueList(MDF_LIST, current_index=_settings.mdfa)
rset = RadioSetting("mdfa", "Memory Display Format A", rs)
basic.append(rset)
# Menu 43: MDF-B
rs = RadioSettingValueList(MDF_LIST, current_index=_settings.mdfb)
rset = RadioSetting("mdfb", "Memory Display Format B", rs)
basic.append(rset)
# Menu 33: DTMFST (DTMF ST)
rs = RadioSettingValueList(DTMFST_LIST, current_index=_settings.dtmfst)
rset = RadioSetting("dtmfst", "DTMF Side Tone", rs)
basic.append(rset)
# Menu 37: PTT-LT
rs = RadioSettingValueList(PTTLT_LIST, current_index=_settings.pttlt)
rset = RadioSetting("pttlt", "PTT Delay", rs)
basic.append(rset)
rs = RadioSettingValueInteger(1, 60, _settings.ani + 1)
rset = RadioSetting("ani", "ANI", rs)
basic.append(rset)
# Menu 20: PF2
def apply_skey2s_listvalue(setting, obj):
LOG.debug("Setting value: " + str(setting.value) + " from list")
val = str(setting.value)
index = SKEY2S_CHOICES.index(val)
val = SKEY2S_VALUES[index]
obj.set_value(val)
if self.MODEL in ["HI-8811", "RT-470L", "RT-470X", "RT-470X_BT",
"RT-470"]:
unwanted = [9, 10, 11, 12]
elif self.MODEL in ["UV-A37", "AR-730"]:
unwanted = [0, 5, 7, 9, 10, 11, 12]
elif self.MODEL in ["A36plus", "A36plus_8w"]:
unwanted = [0, 5, 7, 9, 10, 11]
elif self.MODEL in ["RT-630", "RT-495"]:
unwanted = [5, 9, 10, 11, 12]
else:
unwanted = []
SKEY2S_CHOICES = ALL_SKEY_CHOICES.copy()
SKEY2S_VALUES = ALL_SKEY_VALUES.copy()
for ele in sorted(unwanted, reverse=True):
del SKEY2S_CHOICES[ele]
del SKEY2S_VALUES[ele]
if _settings.skey2_sp in SKEY2S_VALUES:
idx = SKEY2S_VALUES.index(_settings.skey2_sp)
else:
idx = SKEY2S_VALUES.index(0x07) # default FM
rs = RadioSettingValueList(SKEY2S_CHOICES, current_index=idx)
rset = RadioSetting("skey2_sp", "PF2 Key (Short Press)", rs)
rset.set_apply_callback(apply_skey2s_listvalue, _settings.skey2_sp)
basic.append(rset)
# Menu 21: PF2 LONG PRESS
def apply_skey2l_listvalue(setting, obj):
LOG.debug("Setting value: " + str(setting.value) + " from list")
val = str(setting.value)
index = SKEY2L_CHOICES.index(val)
val = SKEY2L_VALUES[index]
obj.set_value(val)
if self.MODEL in ["HI-8811", "RT-470L", "RT-470X", "RT-470X_BT",
"RT-470"]:
unwanted = [8, 9, 10, 11, 12]
elif self.MODEL in ["UV-A37", "AR-730"]:
unwanted = [0, 5, 7, 8, 10, 11, 12]
elif self.MODEL in ["A36plus", "A36plus_8w"]:
unwanted = [0, 5, 7, 8, 11, 12]
elif self.MODEL in ["RT-630", "RT-495"]:
unwanted = [5, 9, 10, 11, 12]
else:
unwanted = []
SKEY2L_CHOICES = ALL_SKEY_CHOICES.copy()
SKEY2L_VALUES = ALL_SKEY_VALUES.copy()
for ele in sorted(unwanted, reverse=True):
del SKEY2L_CHOICES[ele]
del SKEY2L_VALUES[ele]
if _settings.skey2_lp in SKEY2L_VALUES:
idx = SKEY2L_VALUES.index(_settings.skey2_lp)
else:
idx = SKEY2L_VALUES.index(0x1D) # default Search
rs = RadioSettingValueList(SKEY2L_CHOICES, current_index=idx)
rset = RadioSetting("skey2_lp", "PF2 Key (Long Press)", rs)
rset.set_apply_callback(apply_skey2l_listvalue, _settings.skey2_lp)
basic.append(rset)
# Menu 22: PF3
def apply_skey3s_listvalue(setting, obj):
LOG.debug("Setting value: " + str(setting.value) + " from list")
val = str(setting.value)
index = SKEY3S_CHOICES.index(val)
val = SKEY3S_VALUES[index]
obj.set_value(val)
if self.MODEL in ["HI-8811", "RT-470L", "RT-470X", "RT-470X_BT",
"RT-470"]:
unwanted = [8, 9, 10, 11, 12]
elif self.MODEL in ["UV-A37", "AR-730"]:
unwanted = [0, 5, 7, 8, 9, 10, 11, 12]
elif self.MODEL in ["A36plus", "A36plus_8w"]:
unwanted = [0, 5, 7, 8, 11]
elif self.MODEL in ["RT-630", "RT-495"]:
unwanted = [5, 9, 10, 11, 12]
else:
unwanted = []
SKEY3S_CHOICES = ALL_SKEY_CHOICES.copy()
SKEY3S_VALUES = ALL_SKEY_VALUES.copy()
for ele in sorted(unwanted, reverse=True):
del SKEY3S_CHOICES[ele]
del SKEY3S_VALUES[ele]
if _settings.skey3_sp in SKEY3S_VALUES:
idx = SKEY3S_VALUES.index(_settings.skey3_sp)
else:
idx = SKEY3S_VALUES.index(0x0C) # default NOAA
rs = RadioSettingValueList(SKEY3S_CHOICES, current_index=idx)
rset = RadioSetting("skey3_sp", "PF3 Key (Short Press)", rs)
rset.set_apply_callback(apply_skey3s_listvalue, _settings.skey3_sp)
basic.append(rset)
if self.MODEL in ["HI-8811", "RT-470L", "RT-470X", "RT-470X_BT",
"RT-470", "RT-630", "RT-495"]:
# Menu 24: PF3 LONG PRESS (RT-470L)
def apply_skey3l_listvalue(setting, obj):
LOG.debug("Setting value: " + str(setting.value) +
" from list")
val = str(setting.value)
index = SKEY3L_CHOICES.index(val)
val = SKEY2L_VALUES[index]
obj.set_value(val)
if self.MODEL in ["HI-8811", "RT-470L", "RT-470X", "RT-470X_BT",
"RT-470"]:
unwanted = [8, 9, 10, 11, 12]
elif self.MODEL in ["RT-630", "RT-495"]:
unwanted = [5, 9, 10, 11, 12]
else:
unwanted = []
SKEY3L_CHOICES = ALL_SKEY_CHOICES.copy()
SKEY3L_VALUES = ALL_SKEY_VALUES.copy()
for ele in sorted(unwanted, reverse=True):
del SKEY3L_CHOICES[ele]
del SKEY3L_VALUES[ele]
if _settings.skey3_lp in SKEY3L_VALUES:
idx = SKEY3L_VALUES.index(_settings.skey3_lp)
else:
idx = SKEY3L_VALUES.index(0x1D) # default SEARCH
rs = RadioSettingValueList(SKEY3L_CHOICES, current_index=idx)
rset = RadioSetting("skey3_lp", "PF3 Key (Long Press)", rs)
rset.set_apply_callback(apply_skey3l_listvalue,
_settings.skey3_lp)
basic.append(rset)
# Menu 25: TOP KEY (RT-470L)
def apply_skeytop_listvalue(setting, obj):
LOG.debug("Setting value: " + str(setting.value) +
" from list")
val = str(setting.value)
index = SKEYTOP_CHOICES.index(val)
val = SKEYTOP_VALUES[index]
obj.set_value(val)
if self.MODEL in ["HI-8811", "RT-470L", "RT-470X", "RT-470X_BT",
"RT-470"]:
unwanted = [8, 9, 10, 11, 12]
elif self.MODEL in ["UV-A37", "AR-730"]:
unwanted = [0, 5, 7, 8, 9, 10, 11, 12]
elif self.MODEL in ["A36plus", "A36plus_8w"]:
unwanted = [0, 5, 7, 8, 11]
else:
unwanted = []
SKEYTOP_CHOICES = ALL_SKEY_CHOICES.copy()
SKEYTOP_VALUES = ALL_SKEY_VALUES.copy()
for ele in sorted(unwanted, reverse=True):
del SKEYTOP_CHOICES[ele]
del SKEYTOP_VALUES[ele]
if _settings.topkey_sp in SKEYTOP_VALUES:
idx = SKEYTOP_VALUES.index(_settings.topkey_sp)
else:
idx = SKEYTOP_VALUES.index(0x1D) # default SEARCH
rs = RadioSettingValueList(SKEYTOP_CHOICES, current_index=idx)
rset = RadioSetting("topkey_sp", "Top Key (Short Press)", rs)
rset.set_apply_callback(apply_skeytop_listvalue,
_settings.topkey_sp)
basic.append(rset)
# Mneu 36: TONE
rs = RadioSettingValueList(TONE_LIST, current_index=_settings.tone)
rset = RadioSetting("tone", "Tone-burst Frequency", rs)
basic.append(rset)
# Mneu 29: POWER ON MSG
rs = RadioSettingValueList(PONMSG_LIST, current_index=_settings.ponmsg)
rset = RadioSetting("ponmsg", "Power On Message", rs)
basic.append(rset)
if self.MODEL in ["HI-8811", "RT-470L", "RT-470X", "RT-470X_BT",
"RT-630", "RT-495"]:
rs = RadioSettingValueList(TAILCODE_LIST,
current_index=_settings.tailcode)
rset = RadioSetting("tailcode", "Tail Code", rs)
basic.append(rset)
# Menu 46: STE
rs = RadioSettingValueBoolean(_settings.ste)
rset = RadioSetting("ste", "Squelch Tail Eliminate (HT to HT)", rs)
basic.append(rset)
# Menu 40: RP-STE
rs = RadioSettingValueList(RPSTE_LIST, current_index=_settings.rpste)
rset = RadioSetting("rpste", "Squelch Tail Eliminate (repeater)", rs)
basic.append(rset)
# Menu 41: RPT-RL
rs = RadioSettingValueList(RPSTE_LIST, current_index=_settings.rptrl)
rset = RadioSetting("rptrl", "STE Repeater Delay", rs)
basic.append(rset)
# Menu 38: MENU EXIT TIME
rs = RadioSettingValueList(MENUQUIT_LIST,
current_index=_settings.menuquit)
rset = RadioSetting("menuquit", "Menu Auto Quit", rs)
basic.append(rset)
# Menu 34: AUTOLOCK
rs = RadioSettingValueList(AUTOLK_LIST, current_index=_settings.autolk)
rset = RadioSetting("autolk", "Key Auto Lock", rs)
basic.append(rset)
# Menu 28: CDCSS SAVE MODE
rs = RadioSettingValueList(QTSAVE_LIST, current_index=_settings.qtsave)
rset = RadioSetting("qtsave", "QT Save Type", rs)
basic.append(rset)
# Menu 45: TX-A/B
rs = RadioSettingValueList(DUALTX_LIST, current_index=_settings.dualtx)
rset = RadioSetting("dualtx", "Dual TX", rs)
basic.append(rset)
# Menu 47: AL-MODE
rs = RadioSettingValueList(ALMODE_LIST, current_index=_settings.almode)
rset = RadioSetting("almode", "Alarm Mode", rs)
basic.append(rset)
# Menu 11: ROGER
# ==========
# Notice to developers:
# The RT-470 v1.22 firmware expanded the ROGER menu with an additional
# choice, 'TONE1200'. RT-470 radios with a firmware version prior to
# v1.22 will not honor the ROGER menu's 'TONE1200' choice in CHIRP.
# ==========
rs = RadioSettingValueList(ROGER_LIST, current_index=_settings.roger)
rset = RadioSetting("roger", "Roger", rs)
basic.append(rset)
rs = RadioSettingValueBoolean(_settings.alarmsound)
rset = RadioSetting("alarmsound", "Alarm Sound", rs)
basic.append(rset)
# Menu 44: TDR
rs = RadioSettingValueBoolean(_settings.tdr)
rset = RadioSetting("tdr", "TDR", rs)
basic.append(rset)
rs = RadioSettingValueBoolean(not _settings.fmradio)
rset = RadioSetting("fmradio", "FM Radio", rs)
basic.append(rset)
rs = RadioSettingValueBoolean(_settings.kblock)
rset = RadioSetting("kblock", "KB Lock", rs)
basic.append(rset)
# Menu 16: BEEP PROMPT
rs = RadioSettingValueBoolean(_settings.beep)
rset = RadioSetting("beep", "Beep", rs)
basic.append(rset)
if self.MODEL not in ["A36plus", "A36plus_8w", "UV-A37", "AR-730"]:
# Menu 48: RX END TAIL
rs = RadioSettingValueList(TONERXEND_LIST,
current_index=_settings.rxendtail)
rset = RadioSetting("rxendtail", "Tone RX End", rs)
basic.append(rset)
dtmf = RadioSettingGroup("dtmf", "DTMF Settings")
group.append(dtmf)
def apply_code(setting, obj, length):
code = []
for j in range(0, length):
try:
code.append(DTMF_CHARS.index(str(setting.value)[j]))
except IndexError:
code.append(0xFF)
obj.code = code
for i in range(0, 15):
_codeobj = self._memobj.pttid[i].code
_code = "".join([DTMF_CHARS[x] for x in _codeobj if int(x) < 0x1F])
rs = RadioSettingValueString(0, 5, _code, False)
rs.set_charset(DTMF_CHARS)
rset = RadioSetting("pttid/%i.code" % i,
"PTT-ID Code %i" % (i + 1), rs)
rset.set_apply_callback(apply_code, self._memobj.pttid[i], 5)
dtmf.append(rset)
rs = RadioSettingValueList(DTMFSPEED_LIST,
current_index=_dtmf.dtmfon)
rset = RadioSetting("dtmf.dtmfon", "DTMF Speed (on)", rs)
dtmf.append(rset)
rs = RadioSettingValueList(DTMFSPEED_LIST,
current_index=_dtmf.dtmfoff)
rset = RadioSetting("dtmf.dtmfoff", "DTMF Speed (off)", rs)
dtmf.append(rset)
# RT470X Plus Bluetooth does not seem to have correct PTTID setting
if self.MODEL not in ["RT-470X_BT"]:
rs = RadioSettingValueList(
PTTID_LIST, current_index=_settings.pttid)
rset = RadioSetting("pttid", "PTT ID", rs)
dtmf.append(rset)
ani = RadioSettingGroup("ani", "ANI Code List Settings")
group.append(ani)
def _filter(name):
filtered = ""
for char in str(name):
if char in self._valid_chars:
filtered += char
else:
filtered += " "
return filtered
end = 60 - self._aninames # end of immutable ANI names
for i in range(0, end):
_codeobj = self._memobj.anicodes[i].code
_code = "".join([DTMF_CHARS[x] for x in _codeobj if int(x) < 0x1F])
rs = RadioSettingValueString(0, 3, _code, False)
rs.set_charset(DTMF_CHARS)
rset = RadioSetting("anicodes/%i.code" % i,
"ANI Code %i (NUM.%i)" % (i + 1, i + 1), rs)
rset.set_apply_callback(apply_code, self._memobj.anicodes[i], 3)
ani.append(rset)
for i in range(end, 60):
_codeobj = self._memobj.anicodes[i].code
_code = "".join([DTMF_CHARS[x] for x in _codeobj if int(x) < 0x1F])
rs = RadioSettingValueString(0, 3, _code, False)
rs.set_charset(DTMF_CHARS)
rset = RadioSetting("anicodes/%i.code" % (i),
"ANI Code %i" % (i + 1), rs)
rset.set_apply_callback(apply_code,
self._memobj.anicodes[i], 3)
ani.append(rset)
_nameobj = self._memobj.aninames[i - end].name
rs = RadioSettingValueString(0, 10, _filter(_nameobj))
rset = RadioSetting("aninames/%i.name" % (i - end),
"ANI Code %i Name" % (i + 1), rs)
ani.append(rset)
if self.MODEL in ["A36plus", "A36plus_8w"]:
custom = RadioSettingGroup("custom", "Custom Channel Names")
group.append(custom)
for i in range(0, 30):
_nameobj = self._memobj.customnames[i].name
rs = RadioSettingValueString(0, 10, _filter(_nameobj))
rset = RadioSetting("customnames/%i.name" % i,
"Custom Name %i" % (i + 1), rs)
custom.append(rset)
if self.MODEL in ["A36plus", "A36plus_8w"]:
# Menu 21: RX END TAIL
rs = RadioSettingValueList(TONERXEND_LIST,
current_index=_settings.skey3_lp)
rset = RadioSetting("skey3_lp", "RX End Tail", rs)
basic.append(rset)
# Menu 23: TAIL PHASE
rs = RadioSettingValueList(TAILPHASE_LIST,
current_index=_settings.rxendtail)
rset = RadioSetting("rxendtail", "Tail Phase", rs)
basic.append(rset)
# Menu 20: SINGLE MODE
rs = RadioSettingValueBoolean(_settings.single_mode)
rset = RadioSetting("single_mode", "Single Display Mode", rs)
basic.append(rset)
# Menu 48: UNLOCK SW CH
rs = RadioSettingValueBoolean(_settings.unlock_sw_ch)
rset = RadioSetting("unlock_sw_ch",
"Override KB Lock for Channel Keys", rs)
basic.append(rset)
# Menu 49: DIS S TABLE
rs = RadioSettingValueBoolean(_settings.dis_s_table)
rset = RadioSetting("dis_s_table", "Display S Meter", rs)
basic.append(rset)
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()
elif setting == "ani":
setattr(obj, setting, int(element.value) - 1)
elif setting == "fmradio":
setattr(obj, setting, not int(element.value))
elif element.value.get_mutable():
LOG.debug("Setting %s = %s" % (setting, element.value))
setattr(obj, setting, element.value)
except Exception as e:
LOG.debug(element.get_name(), e)
raise
@classmethod
def match_model(cls, filedata, filename):
# This radio has always been post-metadata, so never do
# old-school detection
return False
@directory.register
class RT470Radio(JC8810base):
"""Radtel RT-470"""
VENDOR = "Radtel"
MODEL = "RT-470"
# ==========
# Notice to developers:
# The RT-470 support in this driver is currently based upon...
# - v1.25a firmware (original pcb)
# - v2.11a firmware (pcb2)
# ==========
# original pcb
_fingerprint_pcb1 = [b"\x00\x00\x00\x26\x00\x20\xD8\x04",
b"\x00\x00\x00\x42\x00\x20\xF0\x04",
b"\x00\x00\x00\x4A\x00\x20\xF8\x04",
b"\x00\x00\x00\x3A\x00\x20\xE8\x04", # fw 1.25A
b"\x00\x00\x00\x42\x00\x20\xec\x04", # fw 1.27A
]
# pcb 2
_fingerprint_pcb2 = [b"\x00\x00\x00\x28\x00\x20\xD4\x04", # fw v2.00
b"\x00\x00\x00\x2C\x00\x20\xD8\x04", # fw v2.11A
b"\x00\x00\x00\x36\x00\x20\xDC\x04", # fw v2.13A
]
_fingerprint = _fingerprint_pcb1 + _fingerprint_pcb2
VALID_BANDS = [(16000000, 100000000),
(100000000, 136000000),
(136000000, 200000000),
(200000000, 300000000),
(300000000, 400000000),
(400000000, 560000000),
(740000000, 1000000000),
]
@directory.register
class RT470LRadio(JC8810base):
"""Radtel RT-470L"""
VENDOR = "Radtel"
MODEL = "RT-470L"
# ==========
# Notice to developers:
# The RT-470 support in this driver is currently based upon v1.17 firmware.
# ==========
_fingerprint = [b"\x00\x00\x00\xfe\x00\x20\xAC\x04",
b"\x00\x00\x00\x20\x00\x20\xCC\x04",
b"\x00\x00\x00\x20\x00\x20\x07\x00"]
POWER_LEVELS = [chirp_common.PowerLevel("H", watts=5.00),
chirp_common.PowerLevel("M", watts=4.00),
chirp_common.PowerLevel("L", watts=2.00)]
VALID_BANDS = [(108000000, 136000000),
(136000000, 179000000),
(220000000, 260000000),
(330000000, 400000000),
(400000000, 520000000)]
@directory.register
class RT470XRadio(RT470LRadio):
"""Radtel RT-470X"""
VENDOR = "Radtel"
MODEL = "RT-470X"
# ==========
# Notice to developers:
# The RT-470X support in this driver is currently based upon...
# - v1.18a firmware (original pcb)
# - v2.13a firmware (pcb2)
# ==========
# original pcb
_fingerprint_pcb1 = [b"\x00\x00\x00\x20\x00\x20\xCC\x04",
]
# pcb 2
_fingerprint_pcb2 = [b"\x00\x00\x00\x2C\x00\x20\xD8\x04", # fw v2.10A
b"\x00\x00\x00\x36\x00\x20\xDC\x04", # fw v2.13A
]
_fingerprint = _fingerprint_pcb1 + _fingerprint_pcb2
RT470X_ORIG = False
VALID_BANDS = [(100000000, 136000000),
(136000000, 200000000),
(200000000, 300000000),
(300000000, 400000000),
(400000000, 560000000)]
_AIRBAND = (118000000, 136000000)
def get_features(self):
rf = super().get_features()
rf.valid_modes.append('AM')
rf.valid_modes.append('NAM')
rf.valid_bands = [(18000000, 1000000000)]
return rf
def validate_memory(self, mem):
msgs = []
in_range = chirp_common.in_range
AM_mode = mem.mode == 'AM' or mem.mode == "NAM"
if in_range(mem.freq, [self._AIRBAND]) and not AM_mode:
msgs.append(chirp_common.ValidationWarning(
_('Frequency in this range requires AM mode')))
if not in_range(mem.freq, [self._AIRBAND]) and AM_mode:
msgs.append(chirp_common.ValidationWarning(
_('Frequency in this range must not be AM mode')))
return msgs + super().validate_memory(mem)
def get_memory(self, number):
mem = super().get_memory(number)
_mem = self._memobj.memory[mem.number - 1]
if chirp_common.in_range(mem.freq, [self._AIRBAND]):
mem.mode = _mem.narrow and "NAM" or "AM"
return mem
@directory.register
class RT470XPlusRadio(RT470XRadio):
"""Radtel RT-470X Plus BT"""
VENDOR = "Radtel"
MODEL = "RT-470X_BT"
RT470X_ORIG = False # BT fingerprint will fall automatically here
# BT version
_fingerprint_bt = [b"\x01\x36\x01\x80\x04\x00\x05\x20" # fw v0.15
]
_fingerprint = _fingerprint_bt
@directory.register
class HI8811Radio(RT470LRadio):
"""Hiroyasu HI-8811"""
VENDOR = "Hiroyasu"
MODEL = "HI-8811"
# ==========
# Notice to developers:
# The HI-8811 support in this driver is currently based upon...
# - v1.17 firmware (original pcb)
# - v2.00 firmware (pcb2)
# ==========
# original pcb
_fingerprint_pcb1 = [b"\x00\x00\x00\xfe\x00\x20\xAC\x04",
b"\x00\x00\x00\x20\x00\x20\xCC\x04", # fw v1.17
b"\x00\x00\x00\x20\x00\x20\x07\x00",
]
# pcb 2
_fingerprint_pcb2 = [b"\x00\x00\x00\x28\x00\x20\xD4\x04", # fw v2.00
b"\x00\x00\x00\x28\x00\x20\x07\x00", # fw v2.00
]
_fingerprint = _fingerprint_pcb1 + _fingerprint_pcb2
@directory.register
class UVA37Radio(JC8810base):
"""Anysecu UV-A37"""
VENDOR = "Anysecu"
MODEL = "UV-A37"
# ==========
# Notice to developers:
# The UV-A37 support in this driver is currently based upon v1.24
# firmware.
# ==========
POWER_LEVELS = [chirp_common.PowerLevel("H", watts=5.00),
chirp_common.PowerLevel("L", watts=1.00)]
VALID_BANDS = [(108000000, 136000000),
(136000000, 174000000),
(200000000, 260000000),
(350000000, 390000000),
(400000000, 520000000)]
_magic = b"PROGRAMJC37U"
_fingerprint = [b"\x00\x00\x00\xE4\x00\x20\x94\x04",
b"\x00\x00\x00\xE8\x00\x20\x98\x04"]
_ranges = [
(0x0000, 0x2000),
(0x8000, 0x8040),
(0x9000, 0x9040),
(0xA000, 0xA140),
(0xB000, 0xB440)
]
_memsize = 0xB440
@directory.register
class A36plusRadio(JC8810base):
"""Talkpod A36plus"""
VENDOR = "Talkpod"
MODEL = "A36plus"
# ==========
# Notice to developers:
# The A36plus support in this driver is currently based upon v1.26
# firmware.
# ==========
POWER_LEVELS = [chirp_common.PowerLevel("H", watts=5.00),
chirp_common.PowerLevel("L", watts=1.00)]
VALID_BANDS = [(108000000, 136000000),
(136000000, 180000000),
(200000000, 260000000),
(350000000, 400000000),
(400000000, 520000000),
]
_magic = b"PROGRAMJC37U"
_fingerprint = [b"\x00\x00\x00\x42\x00\x20\xF0\x04",
b"\x00\x00\x00\x5A\x00\x20\x08\x05", # fw 1.18
b"\x00\x00\x00\x9E\x00\x20\x0C\x05", # fw 1.22
b"\x00\x00\x00\xFA\x00\x20\x40\x05", # fw 1.4
b"\x00\x00\x00\x9C\x00\x20\x04\x05", # fw 1.26
]
_ranges = [
(0x0000, 0x4000),
(0x8000, 0x8040),
(0x9000, 0x9040),
(0xA000, 0xA140),
(0xB000, 0xB440)
]
_memsize = 0xB440
_upper = 512 # fw 1.22 expands from 256 to 512 channels
_aninames = 10
_mem_params = (_upper, # number of channels
_aninames, # number of aninames
)
def process_mmap(self):
mem_format = MEM_FORMAT % self._mem_params + MEM_FORMAT_A36PLUS
self._memobj = bitwise.parse(mem_format, self._mmap)
@directory.register
class A36plus8wRadio(A36plusRadio):
"""Talkpod A36plus8w"""
VENDOR = "Talkpod"
MODEL = "A36plus_8w"
# ==========
# Notice to developers:
# The A36plus 8w support in this driver is currently based upon v1.6
# firmware.
# ==========
POWER_LEVELS = [chirp_common.PowerLevel("H", watts=8.00),
chirp_common.PowerLevel("L", watts=1.00)]
_fingerprint = [b"\x00\x00\x00\xFA\x00\x20\x40\x05", # fw 1.4
b"\x00\x00\x00\xD8\x00\x20\x58\x05", # fw 1.6
]
@directory.register
class AR730Radio(UVA37Radio):
"""Abbree AR730"""
VENDOR = "Abbree"
MODEL = "AR-730"
# ==========
# Notice to developers:
# The AR-730 support in this driver is currently based upon v1.24
# firmware.
# ==========
POWER_LEVELS = [chirp_common.PowerLevel("H", watts=5.00),
chirp_common.PowerLevel("L", watts=1.00)]
VALID_BANDS = [(108000000, 136000000),
(136000000, 180000000),
(200000000, 260000000),
(350000000, 390000000),
(400000000, 520000000)]
@directory.register
class RT630Radio(JC8810base):
"""Radtel RT-630"""
VENDOR = "Radtel"
MODEL = "RT-630"
# ==========
# Notice to developers:
# The RT-630 support in this driver is currently based upon v0.07 firmware.
# ==========
_fingerprint = [b"\x00\x00\x00\x32\x00\x20\xD8\x04", # fw 0.07
b"\x00\x00\x00\x36\x00\x20\xDC\x04", # fw V0.09 20250703
]
POWER_LEVELS = [chirp_common.PowerLevel("H", watts=5.00),
chirp_common.PowerLevel("M", watts=4.00),
chirp_common.PowerLevel("L", watts=2.00)]
VALID_BANDS = [(18000000, 108000000),
(108000000, 136000000),
(136000000, 300000000),
(300000000, 660000000),
(840000000, 1000000000)]
_ranges = [
(0x0000, 0x2000),
(0x8000, 0x8040),
(0x9000, 0x9040),
(0xA000, 0xA140),
(0xB000, 0xB2C0),
(0xB500, 0xB740)
]
_memsize = 0xB740
@directory.register
class RT495Radio(RT630Radio):
"""Radtel RT-495"""
VENDOR = "Radtel"
MODEL = "RT-495"
# ==========
# Notice to developers:
# The RT-495 support in this driver is currently based upon v0.07 firmware.
# ==========
_fingerprint = [b"\x00\x00\x00\x24\x00\x20\xD0\x04", # fw 0.06
b"\x00\x00\x00\x32\x00\x20\xD8\x04", # fw 0.07
]
chirp-20261030/chirp/drivers/mursv1.py 0000644 0001750 0001750 00000070573 15264365036 016452 0 ustar hibby hibby # Copyright 2018:
# * Jim Unroe KC9HI,
#
# 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 .
import logging
from chirp.drivers import baofeng_common
from chirp import chirp_common, directory
from chirp import bitwise
from chirp import bandplan_na
from chirp.settings import RadioSettingGroup, RadioSetting, \
RadioSettingValueBoolean, RadioSettingValueList, \
RadioSettingValueString, RadioSettingValueInteger, \
RadioSettingValueFloat, RadioSettings
LOG = logging.getLogger(__name__)
# #### MAGICS #########################################################
# BTECH MURS-V1 magic string
MSTRING_MURSV1 = b"\x50\x5F\x20\x15\x12\x15\x4D"
# #### ID strings #####################################################
# BTECH MURS-V1
MURSV1_fp1 = b"USM2402"
DTMF_CHARS = "0123456789 *#ABCD"
LIST_AB = ["A", "B"]
LIST_ALMOD = ["Off", "Site", "Tone", "Code"]
LIST_BANDWIDTH = ["Wide", "Narrow"]
LIST_COLOR = ["Off", "Blue", "Orange", "Purple"]
LIST_DTMFSPEED = ["%s ms" % x for x in range(50, 2010, 10)]
LIST_DTMFST = ["Off", "DT-ST", "ANI-ST", "DT+ANI"]
LIST_MODE = ["Channel", "Name", "Frequency"]
LIST_OFF1TO9 = ["Off"] + list("123456789")
LIST_OFF1TO10 = LIST_OFF1TO9 + ["10"]
LIST_OFFAB = ["Off"] + LIST_AB
LIST_RESUME = ["TO", "CO", "SE"]
LIST_PONMSG = ["Full", "Message"]
LIST_PTTID = ["Off", "BOT", "EOT", "Both"]
LIST_SCODE = ["%s" % x for x in range(1, 16)]
LIST_RPSTE = ["Off"] + ["%s" % x for x in range(1, 11)]
LIST_RTONE = ["1000 Hz", "1450 Hz", "1750 Hz", "2100 Hz"]
LIST_SAVE = ["Off", "1:1", "1:2", "1:3", "1:4"]
LIST_SHIFTD = ["Off", "+", "-"]
LIST_STEDELAY = ["Off"] + ["%s ms" % x for x in range(100, 1100, 100)]
LIST_TIMEOUT = ["%s sec" % x for x in range(15, 615, 15)]
LIST_TXPOWER = ["High", "Low"]
LIST_VOICE = ["Off", "English", "Chinese"]
LIST_WORKMODE = ["Frequency", "Channel"]
MURS_FREQS = bandplan_na.ALL_MURS_FREQS * 3
def model_match(cls, data):
"""Match the opened/downloaded image to the correct version"""
rid = data[0x1EF0:0x1EF7]
if rid in cls._fileid:
return True
return False
@directory.register
class MURSV1(baofeng_common.BaofengCommonHT):
"""BTech MURS-V1"""
VENDOR = "BTECH"
MODEL = "MURS-V1"
_fileid = [MURSV1_fp1, ]
_magic = [MSTRING_MURSV1, ]
_magic_response_length = 8
_fw_ver_start = 0x1EF0
_recv_block_size = 0x40
_mem_size = 0x2000
_ack_block = True
_ranges = [(0x0000, 0x0DF0),
(0x0E00, 0x1800),
(0x1EE0, 0x1EF0),
(0x1F60, 0x1F70),
(0x1F80, 0x1F90),
(0x1FC0, 0x1FD0)]
_send_block_size = 0x10
MODES = ["NFM", "FM"]
VALID_CHARS = chirp_common.CHARSET_ALPHANUMERIC + \
"!@#$%^&*()+-=[]:\";'<>?,./"
LENGTH_NAME = 7
SKIP_VALUES = ["", "S"]
DTCS_CODES = tuple(sorted(chirp_common.DTCS_CODES + (645,)))
POWER_LEVELS = [chirp_common.PowerLevel("High", watts=2.00),
chirp_common.PowerLevel("Low", watts=.50)]
VALID_BANDS = [(151820000, 154600250)]
PTTID_LIST = LIST_PTTID
SCODE_LIST = LIST_SCODE
def get_features(self):
rf = chirp_common.RadioFeatures()
rf.has_settings = True
rf.has_bank = False
rf.has_tuning_step = False
rf.can_odd_split = False
rf.has_name = True
rf.has_offset = False
rf.has_mode = True
rf.has_dtcs = True
rf.has_rx_dtcs = True
rf.has_dtcs_polarity = True
rf.has_ctone = True
rf.has_cross = True
rf.valid_modes = self.MODES
rf.valid_characters = self.VALID_CHARS
rf.valid_name_length = self.LENGTH_NAME
rf.valid_duplexes = []
rf.valid_tmodes = ['', 'Tone', 'TSQL', 'DTCS', 'Cross']
rf.valid_cross_modes = [
"Tone->Tone",
"DTCS->",
"->DTCS",
"Tone->DTCS",
"DTCS->Tone",
"->Tone",
"DTCS->DTCS"]
rf.valid_skips = self.SKIP_VALUES
rf.valid_dtcs_codes = self.DTCS_CODES
rf.memory_bounds = (1, 15)
rf.valid_power_levels = self.POWER_LEVELS
rf.valid_bands = self.VALID_BANDS
return rf
MEM_FORMAT = """
#seekto 0x0010;
struct {
lbcd rxfreq[4];
lbcd txfreq[4];
ul16 rxtone;
ul16 txtone;
u8 unknown0:4,
scode:4;
u8 unknown1;
u8 unknown2:7,
lowpower:1;
u8 unknown3:1,
wide:1,
unknown4:2,
bcl:1,
scan:1,
pttid:2;
} memory[15];
#seekto 0x0B00;
struct {
u8 code[5];
u8 unused[11];
} pttid[15];
#seekto 0x0C80;
struct {
u8 unknown222[5];
u8 unknown333[5];
u8 alarm[5];
u8 unknown1;
u8 unknown555[5];
u8 unknown666[5];
u8 unknown777[5];
u8 unknown2;
u8 unknown60606[5];
u8 unknown70707[5];
u8 code[5];
u8 unused1:6,
aniid:2;
u8 unknown[2];
u8 dtmfon;
u8 dtmfoff;
} ani;
#seekto 0x0E20;
struct {
u8 unused01:4,
squelch:4;
u8 unused02;
u8 unused03;
u8 unused04:5,
save:3;
u8 unused05:4,
vox:4;
u8 unused06;
u8 unused07:4,
abr:4;
u8 unused08:7,
tdr:1;
u8 unused09:7,
beep:1;
u8 unused10:2,
timeout:6;
u8 unused11[4];
u8 unused12:6,
voice:2;
u8 unused13;
u8 unused14:6,
dtmfst:2;
u8 unused15;
u8 unused16:6,
screv:2;
u8 unused17:6,
pttid:2;
u8 unused18:2,
pttlt:6;
u8 unused19:6,
mdfa:2;
u8 unused20:6,
mdfb:2;
u8 unused21;
u8 unused22:7,
sync:1;
u8 unused23[4];
u8 unused24:6,
wtled:2;
u8 unused25:6,
rxled:2;
u8 unused26:6,
txled:2;
u8 unused27:6,
almod:2;
u8 unused28:7,
dbptt:1;
u8 unused29:6,
tdrab:2;
u8 unused30:7,
ste:1;
u8 unused31:4,
rpste:4;
u8 unused32:4,
rptrl:4;
u8 unused33:7,
ponmsg:1;
u8 unused34:7,
roger:1;
u8 unused35:6,
rtone:2;
u8 unused36;
u8 unused37:6,
rogerrx:2;
u8 unused38;
u8 displayab:1,
unknown1:2,
fmradio:1,
alarm:1,
unknown2:1,
reset:1,
menu:1;
u8 unused39;
u8 workmode;
u8 keylock;
u8 cht;
} settings;
#seekto 0x0E76;
struct {
u8 unused1:1,
mrcha:7;
u8 unused2:1,
mrchb:7;
} wmchannel;
#seekto 0x0F4E;
u16 fm_presets;
#seekto 0x1010;
struct {
char name[7];
u8 unknown1[9];
} names[15];
#seekto 0x1ED0;
struct {
char line1[7];
char line2[7];
} sixpoweron_msg;
#seekto 0x1EE0;
struct {
char line1[7];
char line2[7];
} poweron_msg;
#seekto 0x1EF0;
struct {
char line1[7];
char line2[7];
} firmware_msg;
struct squelch {
u8 sql0;
u8 sql1;
u8 sql2;
u8 sql3;
u8 sql4;
u8 sql5;
u8 sql6;
u8 sql7;
u8 sql8;
u8 sql9;
};
#seekto 0x1F60;
struct {
struct squelch vhf;
} squelch;
"""
@classmethod
def get_prompts(cls):
rp = chirp_common.RadioPrompts()
rp.experimental = \
('The BTech MURS-V1 driver is a beta version.\n'
'\n'
'Please save an unedited copy of your first successful\n'
'download to a CHIRP Radio Images(*.img) file.'
)
rp.pre_download = _(
"Follow these instructions to download your info:\n"
"1 - Turn off your radio\n"
"2 - Connect your interface cable\n"
"3 - Turn on your radio\n"
"4 - Do the download of your radio data\n")
rp.pre_upload = _(
"Follow this instructions to upload your info:\n"
"1 - Turn off your radio\n"
"2 - Connect your interface cable\n"
"3 - Turn on your radio\n"
"4 - Do the upload of your radio data\n")
return rp
def process_mmap(self):
"""Process the mem map into the mem object"""
self._memobj = bitwise.parse(self.MEM_FORMAT, self._mmap)
def _get_mem(self, number):
return self._memobj.memory[number - 1]
def _get_nam(self, number):
return self._memobj.names[number - 1]
def get_memory(self, number):
_mem = self._get_mem(number)
_nam = self._get_nam(number)
mem = chirp_common.Memory()
mem.number = number
if _mem.get_raw()[:1] == b"\xff":
mem.empty = True
return mem
mem.freq = int(_mem.rxfreq) * 10
if int(_mem.rxfreq) == int(_mem.txfreq):
mem.duplex = ""
mem.offset = 0
for char in _nam.name:
if str(char) == "\xFF":
char = " " # The OEM software may have 0xFF mid-name
mem.name += str(char)
mem.name = mem.name.rstrip()
dtcs_pol = ["N", "N"]
if _mem.txtone in [0, 0xFFFF]:
txmode = ""
elif _mem.txtone >= 0x0258:
txmode = "Tone"
mem.rtone = int(_mem.txtone) / 10.0
elif _mem.txtone <= 0x0258:
txmode = "DTCS"
if _mem.txtone > 0x69:
index = _mem.txtone - 0x6A
dtcs_pol[0] = "R"
else:
index = _mem.txtone - 1
mem.dtcs = self.DTCS_CODES[index]
else:
LOG.warn("Bug: txtone is %04x" % _mem.txtone)
if _mem.rxtone in [0, 0xFFFF]:
rxmode = ""
elif _mem.rxtone >= 0x0258:
rxmode = "Tone"
mem.ctone = int(_mem.rxtone) / 10.0
elif _mem.rxtone <= 0x0258:
rxmode = "DTCS"
if _mem.rxtone >= 0x6A:
index = _mem.rxtone - 0x6A
dtcs_pol[1] = "R"
else:
index = _mem.rxtone - 1
mem.rx_dtcs = self.DTCS_CODES[index]
else:
LOG.warn("Bug: rxtone is %04x" % _mem.rxtone)
if txmode == "Tone" and not rxmode:
mem.tmode = "Tone"
elif txmode == rxmode and txmode == "Tone" and mem.rtone == mem.ctone:
mem.tmode = "TSQL"
elif txmode == rxmode and txmode == "DTCS" and mem.dtcs == mem.rx_dtcs:
mem.tmode = "DTCS"
elif rxmode or txmode:
mem.tmode = "Cross"
mem.cross_mode = "%s->%s" % (txmode, rxmode)
mem.dtcs_polarity = "".join(dtcs_pol)
if not _mem.scan:
mem.skip = "S"
levels = self.POWER_LEVELS
try:
mem.power = levels[_mem.lowpower]
except IndexError:
LOG.error("Radio reported invalid power level %s (in %s)" %
(_mem.power, levels))
mem.power = levels[0]
mem.mode = _mem.wide and "FM" or "NFM"
mem.extra = RadioSettingGroup("Extra", "extra")
rs = RadioSetting("bcl", "BCL",
RadioSettingValueBoolean(_mem.bcl))
mem.extra.append(rs)
rs = RadioSetting("pttid", "PTT ID",
RadioSettingValueList(self.PTTID_LIST,
current_index=_mem.pttid))
mem.extra.append(rs)
rs = RadioSetting("scode", "S-CODE",
RadioSettingValueList(self.SCODE_LIST,
current_index=_mem.scode))
mem.extra.append(rs)
immutable = []
if mem.freq in MURS_FREQS:
if mem.number >= 1 and mem.number <= 15:
MURS_FREQ = MURS_FREQS[mem.number - 1]
mem.freq = MURS_FREQ
mem.duplex = ''
mem.offset = 0
immutable = ["empty", "freq", "duplex", "offset"]
if mem.freq in bandplan_na.MURS_NFM:
mem.mode = "NFM"
immutable += ["mode"]
mem.immutable = immutable
return mem
def _set_mem(self, number):
return self._memobj.memory[number - 1]
def _set_nam(self, number):
return self._memobj.names[number - 1]
def set_memory(self, mem):
_mem = self._set_mem(mem.number)
_nam = self._set_nam(mem.number)
if mem.empty:
_mem.set_raw("\xff" * 16)
_nam.set_raw("\xff" * 16)
return
_mem.set_raw("\x00" * 16)
_mem.rxfreq = mem.freq / 10
_mem.txfreq = mem.freq / 10
_namelength = self.get_features().valid_name_length
for i in range(_namelength):
try:
_nam.name[i] = mem.name[i]
except IndexError:
_nam.name[i] = "\xFF"
rxmode = txmode = ""
if mem.tmode == "Tone":
_mem.txtone = int(mem.rtone * 10)
_mem.rxtone = 0
elif mem.tmode == "TSQL":
_mem.txtone = int(mem.ctone * 10)
_mem.rxtone = int(mem.ctone * 10)
elif mem.tmode == "DTCS":
rxmode = txmode = "DTCS"
_mem.txtone = self.DTCS_CODES.index(mem.dtcs) + 1
_mem.rxtone = self.DTCS_CODES.index(mem.dtcs) + 1
elif mem.tmode == "Cross":
txmode, rxmode = mem.cross_mode.split("->", 1)
if txmode == "Tone":
_mem.txtone = int(mem.rtone * 10)
elif txmode == "DTCS":
_mem.txtone = self.DTCS_CODES.index(mem.dtcs) + 1
else:
_mem.txtone = 0
if rxmode == "Tone":
_mem.rxtone = int(mem.ctone * 10)
elif rxmode == "DTCS":
_mem.rxtone = self.DTCS_CODES.index(mem.rx_dtcs) + 1
else:
_mem.rxtone = 0
else:
_mem.rxtone = 0
_mem.txtone = 0
if txmode == "DTCS" and mem.dtcs_polarity[0] == "R":
_mem.txtone += 0x69
if rxmode == "DTCS" and mem.dtcs_polarity[1] == "R":
_mem.rxtone += 0x69
_mem.scan = mem.skip != "S"
_mem.wide = mem.mode == "FM"
if mem.power:
_mem.lowpower = self.POWER_LEVELS.index(mem.power)
else:
_mem.lowpower = 0
# extra settings
if len(mem.extra) > 0:
# there are setting, parse
for setting in mem.extra:
setattr(_mem, setting.get_name(), setting.value)
else:
# there are no extra settings, load defaults
_mem.bcl = 0
_mem.pttid = 0
_mem.scode = 0
def get_settings(self):
"""Translate the bit in the mem_struct into settings in the UI"""
_mem = self._memobj
basic = RadioSettingGroup("basic", "Basic Settings")
advanced = RadioSettingGroup("advanced", "Advanced Settings")
other = RadioSettingGroup("other", "Other Settings")
work = RadioSettingGroup("work", "Work Mode Settings")
fm_preset = RadioSettingGroup("fm_preset", "FM Preset")
dtmfe = RadioSettingGroup("dtmfe", "DTMF Encode Settings")
service = RadioSettingGroup("service", "Service Settings")
top = RadioSettings(basic, advanced, other, work, fm_preset, dtmfe,
service)
# Basic settings
if _mem.settings.squelch > 0x09:
val = 0x00
else:
val = _mem.settings.squelch
rs = RadioSetting("settings.squelch", "Squelch",
RadioSettingValueList(
LIST_OFF1TO9, current_index=val))
basic.append(rs)
if _mem.settings.save > 0x04:
val = 0x00
else:
val = _mem.settings.save
rs = RadioSetting("settings.save", "Battery Saver",
RadioSettingValueList(
LIST_SAVE, current_index=val))
basic.append(rs)
if _mem.settings.vox > 0x0A:
val = 0x00
else:
val = _mem.settings.vox
rs = RadioSetting("settings.vox", "Vox",
RadioSettingValueList(
LIST_OFF1TO10, current_index=val))
basic.append(rs)
if _mem.settings.abr > 0x0A:
val = 0x00
else:
val = _mem.settings.abr
rs = RadioSetting("settings.abr", "Backlight Timeout",
RadioSettingValueList(
LIST_OFF1TO10, current_index=val))
basic.append(rs)
rs = RadioSetting("settings.tdr", "Dual Watch",
RadioSettingValueBoolean(_mem.settings.tdr))
basic.append(rs)
rs = RadioSetting("settings.beep", "Beep",
RadioSettingValueBoolean(_mem.settings.beep))
basic.append(rs)
if _mem.settings.timeout > 0x27:
val = 0x03
else:
val = _mem.settings.timeout
rs = RadioSetting("settings.timeout", "Timeout Timer",
RadioSettingValueList(
LIST_TIMEOUT, current_index=val))
basic.append(rs)
if _mem.settings.voice > 0x02:
val = 0x01
else:
val = _mem.settings.voice
rs = RadioSetting("settings.voice", "Voice Prompt",
RadioSettingValueList(
LIST_VOICE, current_index=val))
basic.append(rs)
rs = RadioSetting(
"settings.dtmfst", "DTMF Sidetone",
RadioSettingValueList(
LIST_DTMFST, current_index=_mem.settings.dtmfst))
basic.append(rs)
if _mem.settings.screv > 0x02:
val = 0x01
else:
val = _mem.settings.screv
rs = RadioSetting("settings.screv", "Scan Resume",
RadioSettingValueList(
LIST_RESUME, current_index=val))
basic.append(rs)
rs = RadioSetting(
"settings.pttid", "When to send PTT ID",
RadioSettingValueList(
LIST_PTTID, current_index=_mem.settings.pttid))
basic.append(rs)
if _mem.settings.pttlt > 0x32:
val = 0x05
else:
val = _mem.settings.pttlt
rs = RadioSetting("pttlt", "PTT ID Delay",
RadioSettingValueInteger(0, 50, val))
basic.append(rs)
rs = RadioSetting(
"settings.mdfa", "Display Mode (A)",
RadioSettingValueList(
LIST_MODE, current_index=_mem.settings.mdfa))
basic.append(rs)
rs = RadioSetting(
"settings.mdfb", "Display Mode (B)",
RadioSettingValueList(
LIST_MODE, current_index=_mem.settings.mdfb))
basic.append(rs)
rs = RadioSetting("settings.sync", "Sync A & B",
RadioSettingValueBoolean(_mem.settings.sync))
basic.append(rs)
rs = RadioSetting("settings.wtled", "Standby LED Color",
RadioSettingValueList(
LIST_COLOR, current_index=_mem.settings.wtled))
basic.append(rs)
rs = RadioSetting("settings.rxled", "RX LED Color",
RadioSettingValueList(
LIST_COLOR, current_index=_mem.settings.rxled))
basic.append(rs)
rs = RadioSetting("settings.txled", "TX LED Color",
RadioSettingValueList(
LIST_COLOR, current_index=_mem.settings.txled))
basic.append(rs)
val = _mem.settings.almod
rs = RadioSetting("settings.almod", "Alarm Mode",
RadioSettingValueList(
LIST_ALMOD, current_index=val))
basic.append(rs)
rs = RadioSetting("settings.dbptt", "Double PTT",
RadioSettingValueBoolean(_mem.settings.dbptt))
basic.append(rs)
rs = RadioSetting("settings.ste", "Squelch Tail Eliminate (HT to HT)",
RadioSettingValueBoolean(_mem.settings.ste))
basic.append(rs)
rs = RadioSetting(
"settings.ponmsg", "Power-On Message",
RadioSettingValueList(
LIST_PONMSG, current_index=_mem.settings.ponmsg))
basic.append(rs)
rs = RadioSetting("settings.roger", "Roger Beep",
RadioSettingValueBoolean(_mem.settings.roger))
basic.append(rs)
rs = RadioSetting(
"settings.rtone", "Tone Burst Frequency",
RadioSettingValueList(
LIST_RTONE, current_index=_mem.settings.rtone))
basic.append(rs)
rs = RadioSetting("settings.rogerrx", "Roger Beep (RX)",
RadioSettingValueList(
LIST_OFFAB, current_index=_mem.settings.rogerrx))
basic.append(rs)
# Advanced settings
rs = RadioSetting("settings.reset", "RESET Menu",
RadioSettingValueBoolean(_mem.settings.reset))
advanced.append(rs)
rs = RadioSetting("settings.menu", "All Menus",
RadioSettingValueBoolean(_mem.settings.menu))
advanced.append(rs)
rs = RadioSetting("settings.fmradio", "Broadcast FM Radio",
RadioSettingValueBoolean(_mem.settings.fmradio))
advanced.append(rs)
rs = RadioSetting("settings.alarm", "Alarm Sound",
RadioSettingValueBoolean(_mem.settings.alarm))
advanced.append(rs)
# Other settings
def _filter(name):
filtered = ""
for char in str(name):
if char in chirp_common.CHARSET_ASCII:
filtered += char
else:
filtered += " "
return filtered
_msg = _mem.firmware_msg
val = RadioSettingValueString(0, 7, _filter(_msg.line1))
val.set_mutable(False)
rs = RadioSetting("firmware_msg.line1", "Firmware Message 1", val)
other.append(rs)
val = RadioSettingValueString(0, 7, _filter(_msg.line2))
val.set_mutable(False)
rs = RadioSetting("firmware_msg.line2", "Firmware Message 2", val)
other.append(rs)
_msg = _mem.sixpoweron_msg
val = RadioSettingValueString(0, 7, _filter(_msg.line1))
val.set_mutable(False)
rs = RadioSetting("sixpoweron_msg.line1", "6+Power-On Message 1", val)
other.append(rs)
val = RadioSettingValueString(0, 7, _filter(_msg.line2))
val.set_mutable(False)
rs = RadioSetting("sixpoweron_msg.line2", "6+Power-On Message 2", val)
other.append(rs)
_msg = _mem.poweron_msg
rs = RadioSetting("poweron_msg.line1", "Power-On Message 1",
RadioSettingValueString(
0, 7, _filter(_msg.line1)))
other.append(rs)
rs = RadioSetting("poweron_msg.line2", "Power-On Message 2",
RadioSettingValueString(
0, 7, _filter(_msg.line2)))
other.append(rs)
# Work mode settings
rs = RadioSetting("settings.displayab", "Display",
RadioSettingValueList(
LIST_AB, current_index=_mem.settings.displayab))
work.append(rs)
rs = RadioSetting("settings.keylock", "Keypad Lock",
RadioSettingValueBoolean(_mem.settings.keylock))
work.append(rs)
rs = RadioSetting("wmchannel.mrcha", "MR A Channel",
RadioSettingValueInteger(1, 15,
_mem.wmchannel.mrcha))
work.append(rs)
rs = RadioSetting("wmchannel.mrchb", "MR B Channel",
RadioSettingValueInteger(1, 15,
_mem.wmchannel.mrchb))
work.append(rs)
# broadcast FM settings
_fm_presets = self._memobj.fm_presets
if _fm_presets <= 108.0 * 10 - 650:
preset = _fm_presets / 10.0 + 65
elif _fm_presets >= 65.0 * 10 and _fm_presets <= 108.0 * 10:
preset = _fm_presets / 10.0
else:
preset = 76.0
rs = RadioSetting("fm_presets", "FM Preset(MHz)",
RadioSettingValueFloat(65, 108.0, preset, 0.1, 1))
fm_preset.append(rs)
# DTMF settings
def apply_code(setting, obj, length):
code = []
for j in range(0, length):
try:
code.append(DTMF_CHARS.index(str(setting.value)[j]))
except IndexError:
code.append(0xFF)
obj.code = code
for i in range(0, 15):
_codeobj = self._memobj.pttid[i].code
_code = "".join([DTMF_CHARS[x] for x in _codeobj if int(x) < 0x1F])
val = RadioSettingValueString(0, 5, _code, False)
val.set_charset(DTMF_CHARS)
pttid = RadioSetting("pttid/%i.code" % i,
"Signal Code %i" % (i + 1), val)
pttid.set_apply_callback(apply_code, self._memobj.pttid[i], 5)
dtmfe.append(pttid)
if _mem.ani.dtmfon > 0xC3:
val = 0x03
else:
val = _mem.ani.dtmfon
rs = RadioSetting("ani.dtmfon", "DTMF Speed (on)",
RadioSettingValueList(LIST_DTMFSPEED,
current_index=val))
dtmfe.append(rs)
if _mem.ani.dtmfoff > 0xC3:
val = 0x03
else:
val = _mem.ani.dtmfoff
rs = RadioSetting("ani.dtmfoff", "DTMF Speed (off)",
RadioSettingValueList(LIST_DTMFSPEED,
current_index=val))
dtmfe.append(rs)
_codeobj = self._memobj.ani.code
_code = "".join([DTMF_CHARS[x] for x in _codeobj if int(x) < 0x1F])
val = RadioSettingValueString(0, 5, _code, False)
val.set_charset(DTMF_CHARS)
rs = RadioSetting("ani.code", "ANI Code", val)
rs.set_apply_callback(apply_code, self._memobj.ani, 5)
dtmfe.append(rs)
rs = RadioSetting("ani.aniid", "When to send ANI ID",
RadioSettingValueList(LIST_PTTID,
current_index=_mem.ani.aniid))
dtmfe.append(rs)
def apply_alarmcode(setting, obj, length):
code = []
for j in range(0, length):
try:
code.append(DTMF_CHARS.index(str(setting.value)[j]))
except IndexError:
code.append(0xFF)
obj.alarm = code
_codeobj = self._memobj.ani.alarm
_code = "".join([DTMF_CHARS[x] for x in _codeobj if int(x) < 0x1F])
val = RadioSettingValueString(0, 5, _code, False)
val.set_charset(DTMF_CHARS)
rs = RadioSetting("ani.alarm", "Alarm Code", val)
rs.set_apply_callback(apply_alarmcode, self._memobj.ani, 5)
dtmfe.append(rs)
# Service settings
for index in range(0, 10):
key = "squelch.vhf.sql%i" % (index)
_obj = self._memobj.squelch.vhf
val = RadioSettingValueInteger(0, 123,
getattr(_obj, "sql%i" % (index)))
if index == 0:
val.set_mutable(False)
name = "Squelch %i" % (index)
rs = RadioSetting(key, name, val)
service.append(rs)
return top
@classmethod
def match_model(cls, filedata, filename):
match_size = False
match_model = False
# testing the file data size
if len(filedata) == 0x2008:
match_size = True
# testing the firmware model fingerprint
match_model = model_match(cls, filedata)
if match_size and match_model:
return True
else:
return False
chirp-20261030/chirp/drivers/nx800.py 0000644 0001750 0001750 00000121555 15242013013 016044 0 ustar hibby hibby # Copyright 2026 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 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 .
import logging
import time
from chirp import bitwise, util
from chirp import checksum
from chirp import chirp_common
from chirp import directory
from chirp import errors
from chirp import memmap
from chirp import settings
from chirp.drivers import tk280
from chirp.drivers import tk8180
LOG = logging.getLogger(__name__)
POWER_LEVELS = [chirp_common.PowerLevel('Low', watts=5),
chirp_common.PowerLevel('High', watts=50)]
CHANNEL_TYPES = ['Analog', 'NXDN', 'Mixed']
DISPLAY_FORMAT_VALUES = {
'CH/GID Name': 0xFF,
'Zone-CH/GID Number': 0x30,
}
SUBLCD_VALUES = {
'None': 0xFF,
'Zone Number': 0x30,
'CH/GID Number': 0x31,
'OST List Number': 0x32,
'Site Number': 0x33,
}
TONE_VOLUME_VALUES = [('Off', 0x00), ('Selectable', 0x30), ('Current', 0xFF)]
TONE_VOLUME_VALUES += [('%i' % i, i) for i in range(1, 32)]
TIMED_POWER_OFF_VALUES = ['%ih%02dm' % divmod(i * 10, 60)
for i in range(0, 49)]
NX800_R_BLOCK_COUNT = 0x276
NX800_S_BLOCKS = [
0x0000,
0x0100, 0x0200, 0x0300, 0x0400, 0x0500, 0x0600, 0x0700,
0x0800, 0x0900, 0x0A00, 0x0B00, 0x0C00, 0x0D00, 0x0E00,
0x0F00, 0x1000, 0x1100,
0x8300, 0x8600, 0x8700, 0x8800, 0x8900, 0x8A00,
0x2D00, 0x2E00, 0x2F00,
]
NX800_S_BASE = 0x27600
NX800_X_BLOCKS = [
0x0000,
0x0100, 0x0200, 0x0300, 0x0400, 0x0500, 0x0600, 0x0700,
0x0800, 0x0900, 0x0A00, 0x0B00, 0x0C00, 0x0D00, 0x0E00,
0x0F00, 0x1000, 0x1100,
0x2D00, 0x2E00, 0x2F00,
]
MODE_ANALOG = 0xFD
MODE_NXDN = 0xF8
MODE_MIXED_DIGITAL_PREF = 0xFA
MODE_MIXED_ANALOG_PREF = 0xFE
MODE_MIXED = (MODE_MIXED_DIGITAL_PREF, MODE_MIXED_ANALOG_PREF)
def _sanitize_ran(value):
value = int(value)
if value < 1:
return 1
if value > 63:
return 63
return value
def _xor_wire(data, key):
return bytes([b ^ key for b in data])
def _send_xor(radio, data):
radio.pipe.write(_xor_wire(data, radio._wire_xor_key))
def _read_xor(radio, count):
data = radio.pipe.read(count)
if len(data) != count:
raise errors.RadioError('Short read from radio')
return _xor_wire(data, radio._wire_xor_key)
def _exchange_xor_ack(radio):
_send_xor(radio, b'\x06')
if _read_xor(radio, 1) != b'\x06':
raise errors.RadioError('Post-block exchange failed')
def do_ident(radio):
radio.pipe.baudrate = 9600
radio.pipe.stopbits = 2
radio.pipe.timeout = 1
radio.pipe.write(b'PROGRAM')
ack = radio.pipe.read(1)
if not ack:
raise errors.RadioNoResponse()
if ack != b'\x16':
raise errors.RadioError('Radio refused hi-speed program mode')
radio.pipe.baudrate = 19200
ack = radio.pipe.read(1)
if not ack:
raise errors.RadioNoResponse()
if ack != b'\x06':
raise errors.RadioError('Radio refused program mode')
radio.pipe.write(b'\x02')
ident = radio.pipe.read(48)
if len(ident) != 48:
raise errors.RadioError('Radio did not return ident')
if ident[:8] != radio._model:
LOG.warning('Radio model mismatch:\n%s' % util.hexprint(ident))
raise errors.RadioError('Unsupported radio model %r' % ident)
# This can be anything. We choose it to be 0x00 so that the rest of the
# communication happens in the clear for easier debugging of traces.
# The OEM software seems to pick a random value for each session,
# obfuscating the traces. Setting this differently would achieve the
# same result here, but there is no reason for us to do that.
radio._wire_xor_key = 0x00
# We tell the radio what the key is by sending it XOR'd with magic
# value 0xB2, followed by cleartext ACK (0x06). The radio will then
# ack in the clear and all future communications will be XOR'd.
radio.pipe.write(bytes([radio._wire_xor_key ^ 0xB2, 0x06]))
if radio.pipe.read(1) != b'\x06':
raise errors.RadioError('Radio refused obfuscated mode')
# Initial exchange observed before first R/S requests.
_send_xor(radio, b'P')
_read_xor(radio, 12)
_exchange_xor_ack(radio)
# Optional model string read seen in OEM trace.
_send_xor(radio, b'URK')
_read_xor(radio, 34)
_exchange_xor_ack(radio)
def do_download(radio):
do_ident(radio)
data = bytearray(b'\xFF' * radio._memsize)
read_count = 0
def status():
st = chirp_common.Status()
st.cur = read_count
st.max = radio._memsize
st.msg = 'Cloning from radio'
radio.status_fn(st)
for block in range(0, NX800_R_BLOCK_COUNT):
_send_xor(radio, tk8180.make_frame('R', block))
cmd = _read_xor(radio, 1)
chunk = b''
addr = block * 0x100
if cmd == b'Z':
read_count += 0x100
elif cmd == b'W':
chunk = _read_xor(radio, 256)
data[addr:addr + 0x100] = chunk
read_count += 0x100
else:
raise errors.RadioError('Unexpected response %r for block %02x' %
(cmd, block))
chksum = _read_xor(radio, 1)
calc = checksum.checksum_8bit(chunk)
if chunk and calc != chksum[0]:
raise errors.RadioError('Checksum failure while reading block')
_exchange_xor_ack(radio)
status()
s_index = {addr: idx for idx, addr in enumerate(NX800_S_BLOCKS)}
for block in NX800_S_BLOCKS:
_send_xor(radio, tk8180.make_frame('S', block, b'\x00'))
cmd = _read_xor(radio, 1)
if cmd != b'X':
raise errors.RadioError('Radio did not send block for %04x' %
block)
chunk = _read_xor(radio, 256)
soff = NX800_S_BASE + (s_index[block] * 0x100)
data[soff:soff + 0x100] = chunk
read_count += 0x100
_exchange_xor_ack(radio)
status()
_send_xor(radio, b'E')
if _read_xor(radio, 1) != b'\x06':
raise errors.RadioError('Radio failed to acknowledge completion')
return bytes(data)
def do_upload(radio):
do_ident(radio)
progress = 0
def status(cur):
st = chirp_common.Status()
st.cur = cur
st.max = radio._memsize
st.msg = 'Cloning to radio'
radio.status_fn(st)
mmap = radio._mmap.get_packed()
for block in range(0, NX800_R_BLOCK_COUNT):
addr = block * 0x100
chunk = mmap[addr:addr + 0x100]
if len(chunk) != 0x100:
raise errors.RadioError('Invalid memory map while uploading')
if all(byte == 0xFF for byte in chunk):
_send_xor(radio, tk8180.make_frame('Z', block, b'\xFF'))
else:
cs = checksum.checksum_8bit(chunk)
_send_xor(radio, tk8180.make_frame(
'W', block, chunk + bytes([cs])))
if _read_xor(radio, 1) != b'\x06':
raise errors.RadioError('Radio refused data block %04x' % block)
progress += 0x100
status(progress)
for i, addr in enumerate(NX800_X_BLOCKS):
src = NX800_S_BASE + (i * 0x100)
chunk = mmap[src:src + 0x100]
if len(chunk) != 0x100:
raise errors.RadioError('Invalid memory map while uploading')
_send_xor(radio, tk8180.make_frame('X', addr, b'\x00' + chunk))
if _read_xor(radio, 1) != b'\x06':
raise errors.RadioError('Radio refused data block %04x' % addr)
progress += 0x100
status(progress)
_send_xor(radio, b'E')
if _read_xor(radio, 1) != b'\x06':
raise errors.RadioError('Radio failed to acknowledge completion')
def _best_effort_reset(radio):
try:
radio.pipe.baudrate = 9600
radio.pipe.write(b'E')
time.sleep(0.3)
radio.pipe.baudrate = 19200
radio.pipe.write(b'E')
except Exception:
LOG.exception('Unable to send reset sequence')
BASE_MEM_FORMAT = """
// No idea why, but if this is set to 0xFF KPG won't open it (but radio
// does not seem to care). Error is something about the missing/mismatched
// "system key", even for purely analog radios. Not sure when this would
// get unset or need to be reset, but enumerate it here for easy
// identification at least.
#seekto 0x0031;
u8 systemkey;
struct memory {
u8 number;
u8 unknown01_03[3];
ul32 rx_freq;
ul32 tx_freq;
ul16 rx_tone;
ul16 tx_tone;
u8 ran_dec;
u8 unknown12;
u8 ran_enc;
u8 unknown14;
char name[14];
u8 unknown22_3c[26];
u8 tx_upper_f:5,
analog_tx:1,
mixed:1,
analog_rx:1;
u8 modeunknown:5,
highpower:1,
modeunknown2:2;
u8 unknown3e_1:4,
unknown3e_2:4;
u8 unknown3f_1:3, // always 0b111
digital_narrow:1,
unknown3f_2:2, // always 0b11 on nx, 0b00 on analog
analog_wide:1,
unknown3f_3:1; // always 1
};
#seekto 0x5D00;
ul16 zone_starts[128];
struct zoneinfo {
u8 number;
u8 zonetype;
ul16 flagoffset;
u8 count;
u8 unknown06;
char name[14];
u8 unknown15_18[4];
ul16 tot_timeout;
ul16 tot_prealert;
ul16 tot_rekey;
ul16 tot_reset;
u8 unknown21_2c[12];
u8 unknown2d;
u8 unknown2e_2f[2];
u8 unknown30_3f[17];
};
#seekto 0x201;
u8 sublcd;
#seekto 0x220;
char pon_msgtext[14];
#seekto 0x22E;
u8 pon_msgtype;
#seekto 0x232;
u8 low_volume_level;
#seekto 0x233;
u8 high_volume_level;
#seekto 0x234;
u8 tone_volume_offset;
#seekto 0x235;
u8 poweron_tone;
#seekto 0x236;
u8 control_tone;
#seekto 0x237;
u8 warning_tone;
#seekto 0x238;
u8 alert_tone;
#seekto 0x239;
u8 sidetone;
#seekto 0x23A;
u8 locator_tone;
#seekto 0x23C;
u8 timed_power_off;
#seekto 0x23B;
u8 ignition_mode;
#seekto 0x26C;
struct {
u8 ignition_sense:1,
unknown26d_0:2,
signal_strength_indicator:1,
power_switch_memory:1,
unknown26d_1:2,
off_hook_decode:1;
} conv_settings;
#seekto 0x26E;
struct {
u8 unknown26f_0:3,
clone:1,
firmware_programming:1,
firmware_version_information:1,
panel_tuning:1,
panel_test:1;
} panel_settings;
#seekto 0x269;
struct {
u8 unknown270_0:5,
zone_name_display:1,
unknown270_1:2;
} conv_settings2;
#seekto 0x28D;
struct {
u8 unknown28e_0:6,
tone_off:1,
ost_memory:1;
} settings;
#seekto 0x2FF;
struct {
u8 unknown00;
char name[14];
u8 unknown0f_10[2];
ul16 rxtone;
ul16 txtone;
u8 unknown15_1f[11];
} ost_tones[40];
#seekto 0x27A00;
u8 squelch;
#seekto 0x27A02;
u8 display_format;
#seekto 0x27A05;
u8 clock_display;
#seekto 0x27B00;
lbit skipflags[512];
"""
class KenwoodNXx00Radio(tk8180.ZoneMemoryMixin, tk8180.KenwoodOSTMixin,
chirp_common.CloneModeRadio):
"""Kenwood NX-x00 file-format bootstrap."""
VENDOR = 'Kenwood'
MODEL = 'NX-x00'
BAUD_RATE = 9600
FORMATS = [directory.register_format('Kenwood KPG-111D', '*.dat')]
_dat_size = 0x40
_dat_variant = None
_default_dat_key = 0x00
_memsize = 0x29100
_system_start = 0x5E00
_max_per_zone = 250
_zone_header_size = 0x40
_memory_size = 0x40
OST_NAME_LENGTH = 14
def _compute_zone_layout(self, zone_sizes):
zones = []
addr = self._system_start
for count in zone_sizes:
zones.append((addr, count))
addr += self._zone_header_size + (count * self._memory_size)
return zones
def _set_zone_flagoffset(self, zoneinfo, scan_index):
zoneinfo.flagoffset = scan_index
def __init__(self, *a, **k):
self._wire_xor_key = 0x00
self._zones = []
self._channel_map = []
self._zone = None
super().__init__(*a, **k)
dat_variant = self._dat_variant
if dat_variant is None:
dat_variant = 0x06
self._dat_header = (b'KPG111D\xFF\xFF\xFFV2.00\xFF' + self._model +
b'\xFF\xFF\x00' + bytes([dat_variant]) +
b'\xFF\xFF\xFF\xFFD' + (b'\xFF' * 31))
@staticmethod
def _xor_payload(payload, key):
return bytes([byte ^ key for byte in payload])
@classmethod
def _get_dat_xor_key(cls, payload):
if not payload:
raise errors.RadioError('DAT file is empty')
return payload[0] ^ 0xFF
@classmethod
def match_model(cls, filedata, filename):
if filename.lower().endswith('.dat'):
if len(filedata) < cls._dat_size + cls._memsize:
return False
if not filedata.startswith(b'KPG111D'):
return False
if filedata[0x10:0x18] != cls._model:
return False
if cls._dat_variant is None:
return True
return filedata[0x1B] == cls._dat_variant
return False
def sync_in(self):
try:
data = do_download(self)
self._mmap = memmap.MemoryMapBytes(data)
except errors.RadioError:
raise
except Exception as e:
LOG.exception('General failure')
raise errors.RadioError('Failed to download from radio: %s' % e)
finally:
_best_effort_reset(self)
self.process_mmap()
def sync_out(self):
try:
do_upload(self)
except errors.RadioError:
_best_effort_reset(self)
raise
except Exception as e:
_best_effort_reset(self)
LOG.exception('General failure')
raise errors.RadioError('Failed to upload to radio: %s' % e)
finally:
_best_effort_reset(self)
def load_mmap(self, filename):
if filename.lower().endswith('.dat'):
with open(filename, 'rb') as f:
self._dat_header = f.read(self._dat_size)
obfuscated = f.read()
if len(obfuscated) == self._memsize + 1:
dat_xor_key = self._get_dat_xor_key(obfuscated)
payload = self._xor_payload(obfuscated[1:], dat_xor_key)
elif len(obfuscated) == self._memsize:
dat_xor_key = obfuscated[-1] ^ 0xFF
payload = self._xor_payload(obfuscated, dat_xor_key)
else:
raise errors.RadioError(
'Unexpected DAT payload size %i' % len(obfuscated))
self._mmap = memmap.MemoryMapBytes(payload)
self.process_mmap()
LOG.info('Loaded DAT file with xor key 0x%02X',
dat_xor_key)
else:
super().load_mmap(filename)
def save_mmap(self, filename):
if filename.lower().endswith('.dat'):
# Per above, we choose a key of 0x00 so we end up with no
# obfuscation in the DAT file for easier debugging. If we set
# this to something non-zero we would obfuscate the file in the
# same way OEM does.
key = self._default_dat_key
payload = self._mmap.get_packed()
if len(payload) != self._memsize:
raise errors.RadioError(
'Invalid memory size %i' % len(payload))
obfuscated = bytes([0xFF ^ key]) + self._xor_payload(payload, key)
with open(filename, 'wb') as f:
f.write(self._dat_header)
f.write(obfuscated)
LOG.info('Wrote DAT file with xor key 0x%02X', key)
else:
super().save_mmap(filename)
def process_mmap(self):
self._zones = self.probe_layout()
self._memobj = bitwise.parse(
self._build_mem_format_from_zone_sizes(
[x[1] for x in self._zones]),
self._mmap)
self._channel_map = []
for zoneindex, (_addr, count) in enumerate(self._zones):
for chanindex in range(count):
self._channel_map.append((zoneindex, chanindex))
def _build_mem_format_from_zone_sizes(self, zone_sizes):
mem_format = BASE_MEM_FORMAT
for index, (addr, count) in enumerate(self._compute_zone_layout(
zone_sizes)):
mem_format += tk8180.SYSTEM_MEM_FORMAT % {
'addr': addr,
'count': max(count, 1),
'index': index,
}
return mem_format
def probe_layout(self):
static = bitwise.parse(BASE_MEM_FORMAT, self._mmap)
zone_addresses = []
for i in range(0, 128):
addr = int(static.zone_starts[i])
if addr == 0xFFFF:
break
zone_addresses.append(addr)
probe_format = BASE_MEM_FORMAT
for i, addr in enumerate(zone_addresses):
probe_format += '#seekto 0x%x; struct zoneinfo zone%i;' % (addr, i)
probe = bitwise.parse(probe_format, self._mmap)
zones = []
for i, addr in enumerate(zone_addresses):
zone = getattr(probe, 'zone%i' % i)
if int(zone.zonetype) != 0x31:
LOG.warning(
'Unsupported non-conventional zone %i type %02x at 0x%04x',
i, int(zone.zonetype), addr)
raise errors.RadioError('Unsupported non-conventional zone')
zones.append((addr, int(zone.count)))
LOG.debug('Zones: %s', zones)
return zones
def get_features(self):
rf = chirp_common.RadioFeatures()
rf.has_bank = False
rf.has_tuning_step = False
rf.has_settings = self._zone is None
if self._zone is None:
rf.memory_bounds = (1, max(1, len(self._channel_map)))
rf.has_sub_devices = True
rf.has_dynamic_subdevices = True
else:
rf.memory_bounds = (1, self._max_per_zone)
rf.has_sub_devices = False
rf.has_dynamic_subdevices = False
rf.can_odd_split = True
rf.valid_modes = ['FM', 'NFM', 'NXDN']
rf.valid_tmodes = ['', 'Tone', 'TSQL', 'DTCS', 'Cross']
rf.valid_cross_modes = ['Tone->Tone', 'DTCS->', '->DTCS',
'Tone->DTCS', 'DTCS->Tone', '->Tone',
'DTCS->DTCS']
rf.has_ctone = True
rf.has_cross = True
rf.has_rx_dtcs = True
rf.valid_duplexes = ['', '-', '+', 'split', 'off']
rf.valid_skips = ['', 'S']
rf.valid_name_length = 14
rf.valid_bands = self.VALID_BANDS
rf.valid_power_levels = POWER_LEVELS
rf.valid_characters = chirp_common.CHARSET_ASCII
rf.valid_tuning_steps = [5.0, 12.5, 6.25]
return rf
def _resolve_channel(self, number):
if self._zone is None:
if number < 1 or number > len(self._channel_map):
return None
return self._channel_map[number - 1]
if number < 1 or number > self._max_per_zone:
return None
chanindex = self._get_zone_channel_index(self._zone, number)
if chanindex is None:
return None
return self._zone, chanindex
def _get_zone_channel_index(self, zoneindex, number):
zone = getattr(self._memobj, 'zone%i' % zoneindex)
count = self._zones[zoneindex][1]
for i in range(count):
if int(zone.memories[i].number) == number:
return i
return None
def _init_new_zoneinfo(self, dest_zoneinfo, zone_number, count,
old_memobj):
if self._zones:
z0info = getattr(old_memobj, 'zone0').zoneinfo
dest_zoneinfo.set_raw(z0info.get_raw())
else:
dest_zoneinfo.set_raw(b'\xFF' * 0x40)
dest_zoneinfo.number = zone_number + 1
dest_zoneinfo.zonetype = 0x31
dest_zoneinfo.count = count
dest_zoneinfo.name = (' %i' % (zone_number + 1)).ljust(14, '\x00')
def expand_mmap(self, zone_sizes):
super().expand_mmap(zone_sizes)
def get_raw_memory(self, number):
resolved = self._resolve_channel(number)
if resolved is None:
raise errors.RadioError('Invalid memory %s' % number)
zoneindex, chanindex = resolved
zone = getattr(self._memobj, 'zone%i' % zoneindex)
return repr(zone.memories[chanindex])
def get_memory(self, number):
mem = chirp_common.Memory()
mem.number = number
resolved = self._resolve_channel(number)
if resolved is None:
mem.empty = True
return mem
zoneindex, chanindex = resolved
zobj = getattr(self._memobj, 'zone%i' % zoneindex)
zone = zobj.zoneinfo
_mem = zobj.memories[chanindex]
if int(_mem.number) == 0xFF or int(_mem.rx_freq) in (0, 0xFFFFFFFF):
mem.empty = True
return mem
mem.freq = int(_mem.rx_freq)
mem.name = str(_mem.name).rstrip('\x00').rstrip()
chirp_common.split_tone_decode(
mem,
tk8180.KenwoodTKx180Radio._decode_tone(int(_mem.tx_tone)),
tk8180.KenwoodTKx180Radio._decode_tone(int(_mem.rx_tone)))
tx = int(_mem.tx_freq)
if tx == 0xFFFFFFFF:
mem.duplex = 'off'
mem.offset = 0
else:
offset = tx - mem.freq
if offset == 0:
mem.duplex = ''
mem.offset = 0
elif abs(offset) < 10000000:
mem.duplex = offset < 0 and '-' or '+'
mem.offset = abs(offset)
else:
mem.duplex = 'split'
mem.offset = tx
verynarrow = not _mem.digital_narrow
if _mem.mixed:
channel_type = 'Mixed'
if _mem.analog_tx:
mem.mode = 'FM' if _mem.analog_wide else 'NFM'
else:
mem.mode = 'NXDN'
elif _mem.analog_rx:
# Analog only
channel_type = 'Analog'
mem.mode = 'FM' if _mem.analog_wide else 'NFM'
else:
# Digital only
channel_type = 'NXDN'
mem.mode = 'NXDN'
ran_dec = _sanitize_ran(int(_mem.ran_dec))
ran_enc = _sanitize_ran(int(_mem.ran_enc))
mem.extra = settings.RadioSettingGroup('extra', 'Extra')
val = settings.RadioSettingValueList(
CHANNEL_TYPES,
current_index=CHANNEL_TYPES.index(channel_type))
mem.extra.append(settings.RadioSetting('channel_type',
'Channel Type', val))
val = settings.RadioSettingValueBoolean(_mem.analog_wide)
val.set_mutable(channel_type == 'Mixed' and mem.mode == 'NXDN')
mem.extra.append(settings.RadioSetting('analog_wide',
'Analog Wide', val))
val = settings.RadioSettingValueBoolean(verynarrow)
val.set_mutable(channel_type != 'Analog')
mem.extra.append(settings.RadioSetting('verynarrow',
'NXDN Very Narrow', val))
val = settings.RadioSettingValueInteger(1, 63, ran_dec)
val.set_mutable(channel_type != 'Analog')
mem.extra.append(settings.RadioSetting('ran_dec',
'RAN Dec', val))
val = settings.RadioSettingValueInteger(1, 63, ran_enc)
val.set_mutable(channel_type != 'Analog')
mem.extra.append(settings.RadioSetting('ran_enc',
'RAN Enc', val))
mem.power = POWER_LEVELS[int(_mem.highpower)]
scan_index = int(zone.flagoffset) + chanindex - 1
if 0 <= scan_index < len(self._memobj.skipflags):
mem.skip = 'S' if bool(self._memobj.skipflags[scan_index]) else ''
else:
mem.skip = ''
return mem
def set_memory(self, mem):
resolved = self._resolve_channel(mem.number)
if resolved is None:
if mem.empty:
return
if self._zone is None:
raise errors.RadioError('Only discovered channels are '
'writable')
if mem.number > self._max_per_zone:
raise errors.RadioError('Maximum channels per zone is %i' %
self._max_per_zone)
parent = getattr(self, '_parent', self)
zoneindex = self._zone
new_sizes = [x[1] for x in parent._zones]
new_sizes[zoneindex] = new_sizes[zoneindex] + 1
parent.expand_mmap(new_sizes)
zobj = getattr(self._memobj, 'zone%i' % zoneindex)
_mem = zobj.memories[parent._zones[zoneindex][1] - 1]
_mem.number = mem.number
self.shuffle_zone()
chanindex = parent._get_zone_channel_index(zoneindex, mem.number)
if chanindex is None:
raise errors.RadioError('Failed to allocate memory')
_mem = zobj.memories[chanindex]
zone = zobj.zoneinfo
else:
zoneindex, chanindex = resolved
zobj = getattr(self._memobj, 'zone%i' % zoneindex)
zone = zobj.zoneinfo
_mem = zobj.memories[chanindex]
if mem.empty:
parent = getattr(self, '_parent', self)
_mem.number = 0xFF
self.shuffle_zone()
new_sizes = [x[1] for x in parent._zones]
new_sizes[zoneindex] = new_sizes[zoneindex] - 1
parent.expand_mmap(new_sizes)
return
_mem.number = mem.number
_mem.rx_freq = mem.freq
txtone, rxtone = chirp_common.split_tone_encode(mem)
_mem.tx_tone = tk8180.KenwoodTKx180Radio._encode_tone(*txtone)
_mem.rx_tone = tk8180.KenwoodTKx180Radio._encode_tone(*rxtone)
_mem.name = mem.name[:14].ljust(14)
_mem.highpower = mem.power == POWER_LEVELS[1]
# Default in case we don't have mem.extra:
channel_type = 'NXDN' if mem.mode == 'NXDN' else 'Analog'
verynarrow = False
analog_wide = mem.mode == 'FM'
ran_dec = ran_enc = 0
for setting in mem.extra or []:
if setting.get_name() == 'channel_type':
value = str(setting.value)
if value in CHANNEL_TYPES:
channel_type = value
else:
try:
channel_type = CHANNEL_TYPES[int(setting.value)]
except (TypeError, ValueError, IndexError):
pass
elif setting.get_name() == 'verynarrow':
verynarrow = bool(int(setting.value))
elif (setting.get_name() == 'analog_wide' and
channel_type == 'Mixed' and
mem.mode == 'NXDN'):
analog_wide = bool(int(setting.value))
elif setting.get_name() == 'ran_dec':
ran_dec = _sanitize_ran(setting.value)
elif setting.get_name() == 'ran_enc':
ran_enc = _sanitize_ran(setting.value)
_mem.analog_rx = channel_type == 'Analog'
_mem.mixed = channel_type == 'Mixed'
_mem.analog_tx = mem.mode != 'NXDN'
_mem.digital_narrow = not verynarrow
_mem.analog_wide = analog_wide
_mem.unknown3f_2 = 0x3 if channel_type == 'NXDN' else 0
_mem.unknown3f_1 = 0x0 if channel_type == 'NXDN' else 0x7
if channel_type != 'Analog':
_mem.ran_dec = ran_dec
_mem.ran_enc = ran_enc
else:
_mem.ran_dec = 0xFF
_mem.ran_enc = 0xFF
if mem.duplex == '':
_mem.tx_freq = mem.freq
elif mem.duplex == 'split':
_mem.tx_freq = mem.offset
elif mem.duplex == 'off':
_mem.tx_freq = 0xFFFFFFFF
elif mem.duplex == '-':
_mem.tx_freq = mem.freq - mem.offset
elif mem.duplex == '+':
_mem.tx_freq = mem.freq + mem.offset
else:
raise errors.RadioError('Unsupported duplex mode %r' % mem.duplex)
scan_index = int(zone.flagoffset) + chanindex - 1
if 0 <= scan_index < len(self._memobj.skipflags):
self._memobj.skipflags[scan_index] = int(mem.skip == 'S')
def get_sub_devices(self):
zones = []
to_copy = ('VENDOR', 'MODEL', 'VALID_BANDS', '_model')
for i, _ in enumerate(self._zones):
zone = getattr(self._memobj, 'zone%i' % i)
zone_cls = tk280.TKx80SubdevMeta.make_subdev(
self, KenwoodNXx00RadioZone, i, to_copy,
VARIANT='Zone %s' % (
str(zone.zoneinfo.name).rstrip('\x00').rstrip()))
zones.append(zone_cls(self, i))
return zones
def _get_zones(self):
zones = settings.RadioSettingGroup('zones', 'Zones')
zone_count = settings.RadioSetting(
'_zonecount',
'Number of Zones',
settings.RadioSettingValueInteger(1, 128, len(self._zones)))
zone_count.set_doc('Number of zones in the radio. Reducing this '
'number will DELETE memories in affected zones!')
zone_count.set_volatile(True)
zones.append(zone_count)
for i in range(len(self._zones)):
zone = settings.RadioSettingSubGroup('zone%i' % i,
'Zone %i' % (i + 1))
_zone = getattr(self._memobj, 'zone%i' % i).zoneinfo
_name = str(_zone.name).rstrip('\x00')
name = settings.MemSetting(
'zone%i.zoneinfo.name' % i,
'Name',
settings.RadioSettingValueString(0, 14, _name,
mem_pad_char='\x00'))
zone.append(name)
def apply_timer(setting, key, zone_number):
val = int(setting.value)
if key == 'tot_timeout':
if val < 15:
val = 15
elif val > 1200:
val = 1200
val = (val // 15) * 15
elif val == 0:
val = 0xFFFF
_zone = getattr(self._memobj,
'zone%i' % zone_number).zoneinfo
setattr(_zone, key, val)
def collapse_timer(val):
val = int(val)
if val == 0xFFFF:
val = 0
return val
timeout = settings.RadioSetting(
'z%itot_timeout' % i,
'Time-out Timer',
settings.RadioSettingValueInteger(
15, 1200,
max(15, min(1200, int(_zone.tot_timeout))), 15))
timeout.set_apply_callback(apply_timer, 'tot_timeout', i)
zone.append(timeout)
prealert = settings.RadioSetting(
'z%itot_prealert' % i,
'TOT Pre-Alert',
settings.RadioSettingValueInteger(
0, 10, collapse_timer(_zone.tot_prealert)))
prealert.set_apply_callback(apply_timer, 'tot_prealert', i)
zone.append(prealert)
rekey = settings.RadioSetting(
'z%itot_rekey' % i,
'TOT Re-Key Time',
settings.RadioSettingValueInteger(
0, 60, collapse_timer(_zone.tot_rekey)))
rekey.set_apply_callback(apply_timer, 'tot_rekey', i)
zone.append(rekey)
reset = settings.RadioSetting(
'z%itot_reset' % i,
'TOT Reset Time',
settings.RadioSettingValueInteger(
0, 15, collapse_timer(_zone.tot_reset)))
reset.set_apply_callback(apply_timer, 'tot_reset', i)
zone.append(reset)
zones.append(zone)
return zones
def _get_ost(self):
ostgroup = super()._get_ost()
for key, name in (('ost_memory', 'OST Status Memory'),
('tone_off', 'Tone Off')):
ostgroup.append(settings.MemSetting(
'settings.%s' % key, name,
settings.RadioSettingValueInvertedBoolean(
not bool(getattr(self._memobj.settings, key)))))
# RadioSettingGroup only supports appending, so move the
# settings we added to the front of the group, ahead of the
# individual OSTs appended by the parent.
for i, key in enumerate(('ost_memory', 'tone_off')):
name = 'settings_%s' % key
ostgroup.keys().remove(name)
ostgroup.keys().insert(i, name)
return ostgroup
def _get_conventional(self):
conv = settings.RadioSettingGroup('conv', 'Conventional')
conv.append(settings.MemSetting(
'conv_settings.power_switch_memory',
'Power Switch Status Memory',
settings.RadioSettingValueInvertedBoolean(
not bool(self._memobj.conv_settings.power_switch_memory))))
conv.append(settings.MemSetting(
'conv_settings.ignition_sense', 'Ignition Sense',
settings.RadioSettingValueInvertedBoolean(
not bool(self._memobj.conv_settings.ignition_sense))))
conv.append(settings.MemSetting(
'conv_settings.signal_strength_indicator',
'Signal Strength Indicator',
settings.RadioSettingValueInvertedBoolean(
not bool(
self._memobj.conv_settings.signal_strength_indicator))))
conv.append(settings.MemSetting(
'conv_settings2.zone_name_display', 'Zone Name Display',
settings.RadioSettingValueInvertedBoolean(
not bool(self._memobj.conv_settings2.zone_name_display))))
conv.append(settings.MemSetting(
'panel_settings.panel_test', 'Panel Test',
settings.RadioSettingValueInvertedBoolean(
not bool(self._memobj.panel_settings.panel_test))))
conv.append(settings.MemSetting(
'panel_settings.panel_tuning', 'Panel Tuning',
settings.RadioSettingValueInvertedBoolean(
not bool(self._memobj.panel_settings.panel_tuning))))
conv.append(settings.MemSetting(
'panel_settings.clone', 'Clone',
settings.RadioSettingValueInvertedBoolean(
not bool(self._memobj.panel_settings.clone))))
conv.append(settings.MemSetting(
'panel_settings.firmware_programming',
'Firmware Programming',
settings.RadioSettingValueInvertedBoolean(
not bool(self._memobj.panel_settings.firmware_programming))))
conv.append(settings.MemSetting(
'panel_settings.firmware_version_information',
'Firmware Version Information',
settings.RadioSettingValueInvertedBoolean(
not bool(
self._memobj.panel_settings.firmware_version_information)
)))
conv.append(settings.MemSetting(
'conv_settings.off_hook_decode', 'Off-hook Decode',
settings.RadioSettingValueInvertedBoolean(
not bool(self._memobj.conv_settings.off_hook_decode))))
conv.append(settings.MemSetting(
'squelch', 'Squelch Level',
settings.RadioSettingValueInteger(0, 9,
int(self._memobj.squelch))))
conv.append(settings.MemSetting(
'display_format', 'Display Format',
settings.RadioSettingValueMap(
DISPLAY_FORMAT_VALUES.items(),
int(self._memobj.display_format))))
conv.append(settings.MemSetting(
'sublcd', 'Sub-LCD Display',
settings.RadioSettingValueMap(SUBLCD_VALUES.items(),
int(self._memobj.sublcd))))
conv.append(settings.MemSetting(
'clock_display', 'Clock Display',
settings.RadioSettingValueBoolean(
bool(self._memobj.clock_display == 0),
mem_vals=(0xFF, 0x00))))
conv.append(settings.MemSetting(
'low_volume_level', 'Low Volume Level (Fixed Volume)',
settings.RadioSettingValueInteger(
0, 30,
int(self._memobj.low_volume_level))))
conv.append(settings.MemSetting(
'high_volume_level', 'High Volume Level (Fixed Volume)',
settings.RadioSettingValueInteger(
1, 31,
int(self._memobj.high_volume_level))))
conv.append(settings.MemSetting(
'pon_msgtype', 'Power-on Message Type',
settings.RadioSettingValueMap(
[('Off', 0xFF), ('Text', 0x30), ('Clock', 0x31),
('FleetSync ID', 0x32), ('NXDN Unit ID', 0x33),
('NXDN Unit ID Name', 0x34)],
int(self._memobj.pon_msgtype))))
conv.append(settings.MemSetting(
'pon_msgtext', 'Power-on Message Text',
settings.RadioSettingValueString(
0, 14, str(self._memobj.pon_msgtext).rstrip('\x00'))))
_tvo = int(self._memobj.tone_volume_offset)
if _tvo & 0x80:
_tvo = _tvo - 0x100
conv.append(settings.MemSetting(
'tone_volume_offset', 'Tone Volume Offset',
settings.RadioSettingValueInteger(-5, 5, _tvo)))
conv.append(settings.MemSetting(
'poweron_tone', 'Power-on Tone',
settings.RadioSettingValueMap(
TONE_VOLUME_VALUES,
int(self._memobj.poweron_tone))))
conv.append(settings.MemSetting(
'control_tone', 'Control Tone',
settings.RadioSettingValueMap(
TONE_VOLUME_VALUES,
int(self._memobj.control_tone))))
conv.append(settings.MemSetting(
'warning_tone', 'Warning Tone',
settings.RadioSettingValueMap(
TONE_VOLUME_VALUES,
int(self._memobj.warning_tone))))
conv.append(settings.MemSetting(
'alert_tone', 'Alert Tone',
settings.RadioSettingValueMap(
TONE_VOLUME_VALUES,
int(self._memobj.alert_tone))))
conv.append(settings.MemSetting(
'sidetone', 'Sidetone',
settings.RadioSettingValueMap(
TONE_VOLUME_VALUES,
int(self._memobj.sidetone))))
conv.append(settings.MemSetting(
'locator_tone', 'Locator Tone',
settings.RadioSettingValueMap(
TONE_VOLUME_VALUES,
int(self._memobj.locator_tone))))
conv.append(settings.MemSetting(
'ignition_mode', 'Ignition Sense Type',
settings.RadioSettingValueMap(
[('Ignition and Switch', 0x30), ('Ignition Only', 0x31)],
int(self._memobj.ignition_mode))))
conv.append(settings.MemSetting(
'timed_power_off', 'Timed Power Off',
settings.RadioSettingValueList(
TIMED_POWER_OFF_VALUES,
current_index=int(self._memobj.timed_power_off))))
return conv
def get_settings(self):
return settings.RadioSettings(self._get_zones(), self._get_ost(),
self._get_conventional())
def set_settings(self, settings_obj):
for element in settings_obj.apply_to(self._memobj):
if not element.changed():
continue
if element.get_name() == '_zonecount':
new_zone_count = int(element.value)
zone_sizes = [x[1] for x in self._zones[:new_zone_count]]
if len(self._zones) > new_zone_count:
self.expand_mmap(zone_sizes[:new_zone_count])
elif len(self._zones) < new_zone_count:
self.expand_mmap(zone_sizes +
([0] * (new_zone_count -
len(self._zones))))
elif element.has_apply_callback():
element.run_apply_callback()
class KenwoodNXx00RadioZone:
_zone = None
def __init__(self, parent, zone=0):
if isinstance(parent, KenwoodNXx00Radio):
self._parent = parent
else:
LOG.warning('Parent was not actually our parent, expect failure')
self._zone = zone
@property
def _zones(self):
return self._parent._zones
@property
def _memobj(self):
return self._parent._memobj
@property
def _mmap(self):
return self._parent._mmap
def load_mmap(self, filename):
self._parent.load_mmap(filename)
def save_mmap(self, filename):
self._parent.save_mmap(filename)
def get_sub_devices(self):
return []
# Need a test to confirm
# @directory.register
class KenwoodNX700Radio(KenwoodNXx00Radio):
MODEL = 'NX-700'
VALID_BANDS = [(136000000, 174000000)]
_dat_variant = 0x04
_model = b'MNX 0700'
@directory.register
class KenwoodNX800Radio(KenwoodNXx00Radio):
MODEL = 'NX-800'
VALID_BANDS = [(400000000, 520000000)]
_dat_variant = 0x06
_model = b'MNX 0800'
chirp-20261030/chirp/drivers/puxing.py 0000644 0001750 0001750 00000037371 15172631605 016522 0 ustar hibby hibby # Copyright 2011 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 .
"""Puxing radios management module"""
import time
import logging
from chirp import util, chirp_common, bitwise, errors, directory
from chirp.drivers.wouxun import wipe_memory, do_download, do_upload
LOG = logging.getLogger(__name__)
def _puxing_prep(radio):
radio.pipe.write(b"\x02PROGRA")
ack = radio.pipe.read(1)
if ack != b"\x06":
raise Exception("Radio did not ACK first command")
radio.pipe.write(b"M\x02")
ident = radio.pipe.read(8)
if len(ident) != 8:
LOG.debug(util.hexprint(ident))
raise Exception("Radio did not send identification")
radio.pipe.write(b"\x06")
if radio.pipe.read(1) != b"\x06":
raise Exception("Radio did not ACK ident")
def puxing_prep(radio):
"""Do the Puxing PX-777 identification dance"""
ex = None
for _i in range(0, 10):
try:
return _puxing_prep(radio)
except Exception as e:
time.sleep(1)
ex = e
raise ex
def puxing_download(radio):
"""Talk to a Puxing PX-777 and do a download"""
try:
puxing_prep(radio)
return do_download(radio, 0x0000, 0x0C60, 0x0008)
except errors.RadioError:
raise
except Exception as e:
raise errors.RadioError("Failed to communicate with radio: %s" % e)
def puxing_upload(radio):
"""Talk to a Puxing PX-777 and do an upload"""
try:
puxing_prep(radio)
return do_upload(radio, 0x0000, 0x0C40, 0x0008)
except errors.RadioError:
raise
except Exception as e:
raise errors.RadioError("Failed to communicate with radio: %s" % e)
POWER_LEVELS = [chirp_common.PowerLevel("High", watts=5.00),
chirp_common.PowerLevel("Low", watts=1.00)]
PUXING_CHARSET = list("0123456789") + \
[chr(x + ord("A")) for x in range(0, 26)] + \
list("- ")
PUXING_MEM_FORMAT = """
#seekto 0x0000;
struct {
lbcd rx_freq[4];
lbcd tx_freq[4];
lbcd rx_tone[2];
lbcd tx_tone[2];
u8 _3_unknown_1;
u8 _2_unknown_1:2,
power_high:1,
iswide:1,
skip:1,
bclo:2,
_2_unknown_2:1;
u8 _4_unknown1:7,
pttid:1;
u8 unknown;
} memory[128];
#seekto 0x080A;
struct {
u8 limits;
u8 model;
} model;
#seekto 0x0850;
struct {
u8 name[6];
u8 pad[2];
} names[128];
"""
# Limits
# 67- 72: 0xEE
# 136-174: 0xEF
# 240-260: 0xF0
# 350-390: 0xF1
# 400-430: 0xF2
# 430-450: 0xF3
# 450-470: 0xF4
# 470-490: 0xF5
# 400-470: 0xF6
# 460-520: 0xF7
PUXING_MODELS = {
328: 0x38,
338: 0x39,
777: 0x3A,
}
PUXING_777_BANDS = [
(67000000, 72000000),
(136000000, 174000000),
(240000000, 260000000),
(350000000, 390000000),
(400000000, 430000000),
(430000000, 450000000),
(450000000, 470000000),
(470000000, 490000000),
(400000000, 470000000),
(460000000, 520000000),
]
@directory.register
class Puxing777Radio(chirp_common.CloneModeRadio):
"""Puxing PX-777"""
VENDOR = "Puxing"
MODEL = "PX-777"
def sync_in(self):
self._mmap = puxing_download(self)
self.process_mmap()
def sync_out(self):
puxing_upload(self)
def get_features(self):
rf = chirp_common.RadioFeatures()
rf.valid_tmodes = ["", "Tone", "TSQL", "DTCS"]
rf.valid_modes = ["FM", "NFM"]
rf.valid_power_levels = POWER_LEVELS
rf.valid_characters = ''.join(set(PUXING_CHARSET))
rf.valid_name_length = 6
rf.valid_tuning_steps = [2.5, 5.0, 6.25, 10.0, 12.5, 15.0, 20.0,
25.0, 30.0, 50.0, 100.0]
rf.has_ctone = False
rf.has_tuning_step = False
rf.has_bank = False
rf.memory_bounds = (1, 128)
if not hasattr(self, "_memobj") or self._memobj is None:
rf.valid_bands = [PUXING_777_BANDS[1]]
elif self._memobj.model.model == PUXING_MODELS[777]:
limit_idx = self._memobj.model.limits - 0xEE
try:
rf.valid_bands = [PUXING_777_BANDS[limit_idx]]
except IndexError:
LOG.error("Invalid band index %i (0x%02x)" %
(limit_idx, self._memobj.model.limits))
rf.valid_bands = [PUXING_777_BANDS[1]]
elif self._memobj.model.model == PUXING_MODELS[328]:
# There are PX-777 that says to be model 328 ...
# for them we only know this freq limits till now
if self._memobj.model.limits in (0xEE, 0xEF):
rf.valid_bands = [PUXING_777_BANDS[1]]
else:
raise Exception("Unsupported band limits 0x%02x for PX-777" %
(self._memobj.model.limits) + " submodel 328"
" - PLEASE REPORT THIS ERROR TO DEVELOPERS!!")
return rf
def process_mmap(self):
self._memobj = bitwise.parse(PUXING_MEM_FORMAT, self._mmap)
def get_raw_memory(self, number):
return repr(self._memobj.memory[number - 1]) + "\r\n" + \
repr(self._memobj.names[number - 1])
@classmethod
def match_model(cls, filedata, filename):
# There are PX-777 that says to be model 328 ...
return (len(filedata) == 3168 and
(util.byte_to_int(filedata[0x080B]) == PUXING_MODELS[777] or
(util.byte_to_int(filedata[0x080B]) == PUXING_MODELS[328] and
util.byte_to_int(filedata[0x080A]) == 0xEE)))
def get_memory(self, number):
_mem = self._memobj.memory[number - 1]
_nam = self._memobj.names[number - 1]
def _is_empty():
for i in range(0, 4):
if _mem.rx_freq[i].get_raw(asbytes=False) != "\xFF":
return False
return True
def _is_no_tone(field):
return field.get_raw(asbytes=False) in ["\x00\x00", "\xFF\xFF"]
def _get_dtcs(value):
# Upper nibble 0x80 -> DCS, 0xC0 -> Inv. DCS
if value > 12000:
return "R", value - 12000
elif value > 8000:
return "N", value - 8000
else:
raise Exception("Unable to convert DCS value")
def _do_dtcs(mem, txfield, rxfield):
if int(txfield) < 8000 or int(rxfield) < 8000:
raise Exception("Split tone not supported")
if txfield[0].get_raw(asbytes=False) == "\xFF":
tp, tx = "N", None
else:
tp, tx = _get_dtcs(int(txfield))
if rxfield[0].get_raw(asbytes=False) == "\xFF":
rp, rx = "N", None
else:
rp, rx = _get_dtcs(int(rxfield))
if not rx:
rx = tx
if not tx:
tx = rx
if tx != rx:
raise Exception("Different RX and TX DCS codes not supported")
mem.dtcs = tx
mem.dtcs_polarity = "%s%s" % (tp, rp)
mem = chirp_common.Memory()
mem.number = number
if _is_empty():
mem.empty = True
return mem
mem.freq = int(_mem.rx_freq) * 10
mem.offset = (int(_mem.tx_freq) * 10) - mem.freq
if mem.offset < 0:
mem.duplex = "-"
elif mem.offset:
mem.duplex = "+"
mem.offset = abs(mem.offset)
if not _mem.skip:
mem.skip = "S"
if not _mem.iswide:
mem.mode = "NFM"
if _is_no_tone(_mem.tx_tone):
pass # No tone
elif int(_mem.tx_tone) > 8000 or \
(not _is_no_tone(_mem.rx_tone) and int(_mem.rx_tone) > 8000):
mem.tmode = "DTCS"
_do_dtcs(mem, _mem.tx_tone, _mem.rx_tone)
else:
mem.rtone = int(_mem.tx_tone) / 10.0
mem.tmode = _is_no_tone(_mem.rx_tone) and "Tone" or "TSQL"
mem.power = POWER_LEVELS[not _mem.power_high]
for i in _nam.name:
if i == 0xFF:
break
mem.name += PUXING_CHARSET[i]
mem.name = mem.name.rstrip()
return mem
def set_memory(self, mem):
_mem = self._memobj.memory[mem.number - 1]
_nam = self._memobj.names[mem.number - 1]
if mem.empty:
wipe_memory(_mem, "\xFF")
return
_mem.rx_freq = mem.freq / 10
if mem.duplex == "+":
_mem.tx_freq = (mem.freq / 10) + (mem.offset / 10)
elif mem.duplex == "-":
_mem.tx_freq = (mem.freq / 10) - (mem.offset / 10)
else:
_mem.tx_freq = (mem.freq / 10)
_mem.skip = mem.skip != "S"
_mem.iswide = mem.mode != "NFM"
_mem.rx_tone[0].set_raw("\xFF")
_mem.rx_tone[1].set_raw("\xFF")
_mem.tx_tone[0].set_raw("\xFF")
_mem.tx_tone[1].set_raw("\xFF")
if mem.tmode == "DTCS":
_mem.tx_tone = int("%x" % int("%i" % (mem.dtcs), 16))
_mem.rx_tone = int("%x" % int("%i" % (mem.dtcs), 16))
# Argh. Set the high order two bits to signal DCS or Inv. DCS
txm = mem.dtcs_polarity[0] == "N" and 0x80 or 0xC0
rxm = mem.dtcs_polarity[1] == "N" and 0x80 or 0xC0
_mem.tx_tone[1].set_raw(
chr(ord(_mem.tx_tone[1].get_raw(asbytes=False)) | txm))
_mem.rx_tone[1].set_raw(
chr(ord(_mem.rx_tone[1].get_raw(asbytes=False)) | rxm))
elif mem.tmode:
_mem.tx_tone = int(mem.rtone * 10)
if mem.tmode == "TSQL":
_mem.rx_tone = int(_mem.tx_tone)
if mem.power:
_mem.power_high = not POWER_LEVELS.index(mem.power)
else:
_mem.power_high = True
# Default to disabling the busy channel lockout
# 00 == Close
# 01 == Carrier
# 10 == QT/DQT
_mem.bclo = 0
_nam.name = [0xFF] * 6
for i in range(0, len(mem.name)):
try:
_nam.name[i] = PUXING_CHARSET.index(mem.name[i])
except IndexError:
raise Exception("Character `%s' not supported")
def puxing_2r_prep(radio):
"""Do the Puxing 2R identification dance"""
radio.pipe.timeout = 0.2
radio.pipe.write(b"PROGRAM\x02")
ack = radio.pipe.read(1)
if not ack:
raise errors.RadioNoResponse()
if ack != b"\x06":
raise Exception("Unexpected response from radio")
radio.pipe.write(ack)
ident = radio.pipe.read(16)
LOG.info("Radio ident: %s (%i)" % (repr(ident), len(ident)))
def puxing_2r_download(radio):
"""Talk to a Puxing 2R and do a download"""
try:
puxing_2r_prep(radio)
return do_download(radio, 0x0000, 0x0FE0, 0x0010)
except errors.RadioError:
raise
except Exception as e:
raise errors.RadioError("Failed to communicate with radio: %s" % e)
def puxing_2r_upload(radio):
"""Talk to a Puxing 2R and do an upload"""
try:
puxing_2r_prep(radio)
return do_upload(radio, 0x0000, 0x0FE0, 0x0010)
except errors.RadioError:
raise
except Exception as e:
raise errors.RadioError("Failed to communicate with radio: %s" % e)
PUXING_2R_MEM_FORMAT = """
#seekto 0x0010;
struct {
lbcd freq[4];
lbcd offset[4];
u8 rx_tone;
u8 tx_tone;
u8 duplex:2,
txdtcsinv:1,
rxdtcsinv:1,
simplex:1,
unknown2:1,
iswide:1,
ishigh:1;
u8 name[5];
} memory[128];
"""
PX2R_DUPLEX = ["", "+", "-", ""]
PX2R_POWER_LEVELS = [chirp_common.PowerLevel("Low", watts=1.0),
chirp_common.PowerLevel("High", watts=2.0)]
PX2R_CHARSET = "0123456789- ABCDEFGHIJKLMNOPQRSTUVWXYZ +"
@directory.register
class Puxing2RRadio(chirp_common.CloneModeRadio):
"""Puxing PX-2R"""
VENDOR = "Puxing"
MODEL = "PX-2R"
NEEDS_COMPAT_SERIAL = True
_memsize = 0x0FE0
def get_features(self):
rf = chirp_common.RadioFeatures()
rf.valid_tmodes = ["", "Tone", "TSQL", "DTCS"]
rf.valid_modes = ["FM", "NFM"]
rf.valid_power_levels = PX2R_POWER_LEVELS
rf.valid_bands = [(400000000, 500000000)]
rf.valid_characters = PX2R_CHARSET
rf.valid_name_length = 5
rf.valid_duplexes = ["", "+", "-"]
rf.valid_skips = []
rf.has_ctone = False
rf.has_tuning_step = False
rf.has_bank = False
rf.memory_bounds = (1, 128)
rf.can_odd_split = False
return rf
@classmethod
def match_model(cls, filedata, filename):
return (len(filedata) == cls._memsize) and \
filedata[-16:] not in ("IcomCloneFormat3",
b'IcomCloneFormat3')
def sync_in(self):
self._mmap = puxing_2r_download(self)
self.process_mmap()
def sync_out(self):
puxing_2r_upload(self)
def process_mmap(self):
self._memobj = bitwise.parse(PUXING_2R_MEM_FORMAT, self._mmap)
def get_memory(self, number):
_mem = self._memobj.memory[number-1]
mem = chirp_common.Memory()
mem.number = number
if _mem.get_raw(asbytes=False)[0:4] == "\xff\xff\xff\xff":
mem.empty = True
return mem
mem.freq = int(_mem.freq) * 10
mem.offset = int(_mem.offset) * 10
mem.mode = _mem.iswide and "FM" or "NFM"
mem.duplex = PX2R_DUPLEX[_mem.duplex]
mem.power = PX2R_POWER_LEVELS[_mem.ishigh]
if _mem.tx_tone >= 0x33:
mem.dtcs = chirp_common.DTCS_CODES[_mem.tx_tone - 0x33]
mem.tmode = "DTCS"
mem.dtcs_polarity = \
(_mem.txdtcsinv and "R" or "N") + \
(_mem.rxdtcsinv and "R" or "N")
elif _mem.tx_tone:
mem.rtone = chirp_common.TONES[_mem.tx_tone - 1]
mem.tmode = _mem.rx_tone and "TSQL" or "Tone"
count = 0
for i in _mem.name:
if i == 0xFF:
break
try:
mem.name += PX2R_CHARSET[i]
except Exception:
LOG.error("Unknown name char %i: 0x%02x (mem %i)" %
(count, i, number))
mem.name += " "
count += 1
mem.name = mem.name.rstrip()
return mem
def set_memory(self, mem):
_mem = self._memobj.memory[mem.number-1]
if mem.empty:
_mem.set_raw("\xff" * 16)
return
_mem.freq = mem.freq / 10
_mem.offset = mem.offset / 10
_mem.iswide = mem.mode == "FM"
_mem.duplex = PX2R_DUPLEX.index(mem.duplex)
_mem.ishigh = mem.power == PX2R_POWER_LEVELS[1]
if mem.tmode == "DTCS":
_mem.tx_tone = chirp_common.DTCS_CODES.index(mem.dtcs) + 0x33
_mem.rx_tone = chirp_common.DTCS_CODES.index(mem.dtcs) + 0x33
_mem.txdtcsinv = mem.dtcs_polarity[0] == "R"
_mem.rxdtcsinv = mem.dtcs_polarity[1] == "R"
elif mem.tmode in ["Tone", "TSQL"]:
_mem.tx_tone = chirp_common.TONES.index(mem.rtone) + 1
_mem.rx_tone = mem.tmode == "TSQL" and int(_mem.tx_tone) or 0
else:
_mem.tx_tone = 0
_mem.rx_tone = 0
for i in range(0, 5):
try:
_mem.name[i] = PX2R_CHARSET.index(mem.name[i])
except IndexError:
_mem.name[i] = 0xFF
def get_raw_memory(self, number):
return repr(self._memobj.memory[number-1])
chirp-20261030/chirp/drivers/puxing_px888k.py 0000644 0001750 0001750 00000204304 14665027664 017656 0 ustar hibby hibby # Copyright 2016 Leo Barring
#
# 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 .
from chirp import chirp_common, directory, memmap, \
bitwise, settings, errors, util
from struct import pack
import logging
LOG = logging.getLogger(__name__)
SUPPORT_NONSPLIT_DUPLEX_ONLY = False
SUPPORT_SPLIT_BUT_DEFAULT_TO_NONSPLIT_ALWAYS = True
UNAMBIGUOUS_CROSS_MODES_ONLY = True
# With this setting enabled, some CHIRP settings are stored in
# thought-to-be junk/padding data of the channel memories.
# Enabling this feature while using CHIRP with an actual radio
# and any effects thereof is entirely the responsibility of the user.
ENABLE_DANGEROUS_EXPERIMENTAL_FEATURES = False
MEM_FORMAT = """
// data fields are generally written 0xff if they are unset
struct {
#seekto 0x0000;
struct {
struct {
// 0-3
bbcd rx_freq[4];
// 4-7
bbcd tx_freq[4];
// 8-9 A-B
struct {
u8 digital:1,
invert:1,
high:6;
u8 low;
} tone[2];
// tx_squelch on 0, rx_squelch on 1
// C
// the duplex sign is not used for memories,
// but is kept for interface consistency
u8 duplex_sign:2,
compander:1,
txpower:1,
modulation_width:1,
txrx_reverse:1,
bcl:2;
// D
u8 scrambler_type:3,
use_scrambler:1,
opt_signal:2,
ptt_id_edge:2;
// E-F
// u8 _unknown_000E[2];
%s
} data[128];
struct {
// 0-5, alt 8-D
char entry[6];
// 6-8, alt E-F
char _unknown_0806[2];
} names[128];
#seekto 0x0c20;
bit present[128];
#seekto 0x0c30;
bit priority[128];
} channel_memory;
#seekto 0x0c00;
struct {
// 0-3
bbcd rx_freq[4];
// 4-7
bbcd tx_freq[4]; // actually offset, but kept for name consistency
// 8
struct {
u8 digital:1,
invert:1,
high:6;
u8 low;
} tone[2]; // tx_squelch on 0, rx_squelch on 1
// C
u8 duplex_sign:2,
compander:1,
txpower:1,
modulation_width:1,
txrx_reverse:1,
bcl:2;
// D
u8 scrambler_type:3,
use_scrambler:1,
opt_signal:2,
ptt_id_edge:2;
// E-F
// u8 _unknown_0C0E[2];
%s
} vfo_data[2];
#seekto 0xc40;
struct {
// 0-5
char model_string[6]; // typically PX888D, unknown if rw or ro
// 6-7
u8 _unknown_0C46[2];
// 8-9
struct {
bbcd lower_freq[2];
bbcd upper_freq[2];
} band_limits[2];
} model_information;
#seekto 0x0c50;
char radio_information_string[16];
#seekto 0x0c60;
struct {
// 0
u8 ptt_cancel_sq:1,
dis_ptt_id:1,
workmode_b:2,
use_roger_beep:1,
msk_reverse:1,
workmode_a:2;
// 1
u8 backlight_color:2,
backlight_mode:2,
dual_single_watch:1,
auto_keylock:1,
scan_mode:2;
// 2
u8 rx_stun:1,
tx_stun:1,
boot_message_mode:2,
battery_save:1,
key_beep:1,
voice_announce:2;
// 3
bbcd squelch_level;
// 4
bbcd tx_timeout;
// 5
u8 allow_keypad:1,
relay_without_disable_tail:1,
_unknown_0C65:1,
call_channel_active:1,
vox_gain:4;
// 6
bbcd vox_delay;
// 7
bbcd vfo_step;
// 8
bbcd ptt_id_type;
// 9
u8 keypad_lock:1,
_unknown_0C69_1:1,
side_button_hold_mode:2,
dtmf_sidetone:1,
_unknown_0C69_2:1,
side_button_click_mode:2;
// A
u8 roger_beep:4,
main_watch:1,
_unknown_0C6A:3;
// B
u8 channel_a;
// C
u8 channel_b;
// D
u8 priority_channel;
// E
u8 wait_time;
// F
u8 _unknown_0C6F;
// 0-7 on next block
u8 _unknown_0C70[8];
// 8-D on next block
char boot_message[6];
} opt_settings;
#seekto 0x0c80;
struct {
// these fields are used for all ptt id forms (msk/dtmf/5t)
// (only one can be active and stored at a time)
// and different constraints are applied depending
// on the ptt id type
u8 entry[7];
u8 length;
} ptt_id_data[2];
// 0 is BOT, 1 is EOT
#seekto 0x0c90;
struct {
// 0
u8 _unknown_0C90;
// 1
u8 _unknown_0C91_1:3,
channel_stepping:1,
unknown_0C91_2:1,
receive_range:2,
unknown_0C91_3:1;
// 2-3
u8 _unknown_0C92[2];
// 4-7
u8 vfo_freq[4];
// 8-F and two more blocks
struct {
u8 entry[4];
} memory[10];
} fm_radio;
#seekto 0x0cc0;
struct {
char id_code[4];
struct {
char entry[4];
} phone_book[9];
} msk_settings;
#seekto 0x0cf0;
struct {
// 0-3
bbcd rx_freq[4];
// 4-7
bbcd tx_freq[4];
// 8
struct {
u8 digital:1,
invert:1,
high:6;
u8 low;
} tone[2];
// tx_squelch on 0, rx_squelch on 1
// C
// the duplex sign is not used for the CALL,
// channel but is kept for interface consistency
u8 duplex_sign:2,
compander:1,
txpower:1,
modulation_width:1,
txrx_reverse:1,
bcl:2;
// D
u8 scrambler_type:3,
use_scrambler:1,
opt_signal:2,
ptt_id_edge:2;
// E-F
// u8 _unknown_0CFE[2];
%s
} call_channel;
#seekto 0x0d00;
struct {
// DTMF codes are stored as hex half-bytes,
// 0-9 A-D are mapped straight
// DTMF '*' is HEX E, DTMF '#' is HEX F
// 0x0d00
struct {
u8 digit_length; // 0x05 to 0x14 corresponding to 50-200ms
u8 inter_digit_pause; // same
u8 first_digit_length; // same
u8 first_digit_delay; // 0x02 to 0x14 corresponding to 100-1000ms
} timing;
#seekto 0x0d30;
u8 _unknown_0D30[2]; // 0-1
u8 group_code; // 2
u8 reset_time; // 3
u8 alert_transpond; // 4
u8 id_code[4]; // 5-8
u8 _unknown_0D39[4]; // 9-C
u8 id_code_length; // D
u8 _unknown_0d3e[2]; // E-F
// 0x0d40
u8 tx_stun_code[4];
u8 _unknown_0D44[4];
u8 tx_stun_code_length;
u8 cancel_tx_stun_code_length;
u8 cancel_tx_stun_code[4];
u8 _unknown_0D4E[2];
// 0x0d50
u8 rxtx_stun_code[4];
u8 _unknown_0D54[4];
u8 rxtx_stun_code_length;
u8 cancel_rxtx_stun_code_length;
u8 cancel_rxtx_stun_code[4];
u8 _unknown_0D5E[2];
// 0x0d60
struct {
u8 entry[5];
u8 _unknown_0D65[3];
u8 length;
u8 _unknown_0D69[7];
} phone_book[9];
} dtmf_settings;
#seekto 0x0e00;
struct {
u8 delay;
u8 _unknown_0E01[5];
u8 alert_transpond;
u8 reset_time;
u8 tone_standard;
u8 id_code[3];
#seekto 0x0e20;
struct {
u8 period;
u8 group_code:4,
repeat_code:4;
} tone_settings[4];
// the order is ZVEI1 ZVEI2 CCIR1 CCITT
#seekto 0x0e40;
// 5-Tone tone standard frequency table
// unknown use, changing the values does not seem to have
// any effect on the produced sound, but the values are not
// overwritten either.
il16 tone_frequency_table[16];
// 0xe60
u8 tx_stun_code[5];
u8 _unknown_0E65[3];
u8 tx_stun_code_length;
u8 cancel_tx_stun_code_length;
u8 cancel_tx_stun_code[5];
u8 _unknown_0E6F;
// 0xe70
u8 rxtx_stun_code[5];
u8 _unknown_0E75[3];
u8 rxtx_stun_code_length;
u8 cancel_rxtx_stun_code_length;
u8 cancel_rxtx_stun_code[5];
u8 _unknown_0E7F;
// 0xe80
struct {
u8 entry[3];
} phone_book[9];
} five_tone_settings;
} mem;"""
# various magic numbers and strings, apart from the memory format
if ENABLE_DANGEROUS_EXPERIMENTAL_FEATURES:
LOG.warn("ENABLE_DANGEROUS_EXPERIMENTAL_FEATURES"
"AND/OR DANGEROUS FEATURES ENABLED")
MEM_FORMAT = MEM_FORMAT % (
"u8 _unknown_000E_1: 6,\n"
" experimental_duplex_mode_indicator: 1,\n"
" experimental_cross_mode_indicator: 1;\n"
"u8 _unknown_000F;",
"u8 _unknown_0C0E_1: 6,\n"
" experimental_duplex_mode_indicator: 1,\n"
" experimental_cross_mode_indicator: 1;\n"
"u8 _unknown_0C0F;",
"u8 _unknown_0CFE_1: 6,\n"
" experimental_duplex_mode_indicator: 1,\n"
" experimental_cross_mode_indicator: 1;\n"
"u8 _unknown_0CFF;")
# we don't need these settings anymore, because it's exactly what the
# experimental features are about
SUPPORT_SPLIT_BUT_DEFAULT_TO_NONSPLIT_ALWAYS = False
SUPPORT_NONSPLIT_DUPLEX_ONLY = False
UNAMBIGUOUS_CROSS_MODES_ONLY = False
else:
MEM_FORMAT = MEM_FORMAT % (
"u8 _unknown_000E[2];",
"u8 _unknown_0C0E[2];",
"u8 _unknown_0CFE[2];")
FILE_MAGIC = [0xc40, 0xc50,
b'\x50\x58\x38\x38\x38\x44\x00\xff'
b'\x13\x40\x17\x60\x40\x00\x48\x00']
HANDSHAKE_OUT = b'XONLINE'
HANDSHAKE_IN = [b'PX888D\x00\xff']
LOWER_READ_BOUND = 0
UPPER_READ_BOUND = 0x1000
LOWER_WRITE_BOUND = 0
UPPER_WRITE_BOUND = 0x0fc0
BLOCKSIZE = 64
OFF_INT = ["Off"] + [str(x+1) for x in range(100)]
OFF_ON = ["Off", "On"]
INACTIVE_ACTIVE = ["Inactive", "Active"]
NO_YES = ["No", "Yes"]
YES_NO = ["Yes", "No"]
BANDS = [(134000000, 176000000), # VHF
(400000000, 480000000)] # UHF
SPECIAL_CHANNELS = {'VFO-A': -2, 'VFO-B': -1, 'CALL': 0}
SPECIAL_NUMBERS = {-2: 'VFO-A', -1: 'VFO-B', 0: 'CALL'}
DUPLEX_MODES = ['', '+', '-', 'split']
if SUPPORT_NONSPLIT_DUPLEX_ONLY:
DUPLEX_MODES = ['', '+', '-']
TONE_MODES = ["", "Tone", "TSQL", "DTCS", "Cross"]
CROSS_MODES = ["Tone->Tone",
"DTCS->",
"->DTCS",
"Tone->DTCS",
"DTCS->Tone",
"->Tone",
"DTCS->DTCS",
"Tone->"]
if UNAMBIGUOUS_CROSS_MODES_ONLY:
CROSS_MODES = ["Tone->Tone",
"DTCS->",
"->DTCS",
"Tone->DTCS",
"DTCS->Tone",
"->Tone",
"DTCS->DTCS"]
MODES = ["NFM", "FM"]
# Only the 'High' power level is quantified in the manual for
# the radio (4W for VHF, 5W for UHF), a web search turned
# up numbers for the 'Low' power level (0.5W for VHF and
# 0.7W for UHF), but they are not official to my knowledge
# and should be taken with a grain of salt or two.
# Numbers used in code is the averages
POWER_LEVELS = [chirp_common.PowerLevel("Low", watts=0.6),
chirp_common.PowerLevel("High", watts=4.5)]
SKIP_MODES = ["", "S"]
BCL_MODES = ["Off", "Carrier", "QT/DQT"]
SCRAMBLER_MODES = OFF_INT[0:9]
PTT_ID_EDGES = ["Off", "BOT", "EOT", "Both"]
OPTSIGN_MODES = ["None", "DTMF", "5-Tone", "MSK"]
VFO_STRIDE = ['5 kHz', '6.25 kHz', '10 kHz', '12.5 kHz', '25 kHz']
AB = ['A', 'B']
WATCH_MODES = ['Single watch', 'Dual watch']
AB_MODES = ['VFO', 'Memory index', 'Memory name', 'Memory frequency']
SCAN_MODES = ["Time", "Carrier", "Seek"]
WAIT_TIMES = [("0.3s", 6), ("0.5s", 10)] +\
[("%ds" % t, t*20) for t in range(1, 13)]
BUTTON_MODES = ["Send call list data",
"Emergency alarm",
"Send 1750 Hz signal",
"Open squelch"]
BOOT_MESSAGE_TYPES = ["Off", "Battery voltage", "Custom message"]
TALKBACK = ['Off', 'Chinese', 'English']
BACKLIGHT_COLORS = list(zip(["Blue", "Orange", "Purple"], range(1, 4)))
VOX_GAIN = OFF_INT[0:10]
VOX_DELAYS = ['1s', '2s', '3s', '4s']
TRANSMIT_ALARMS = ['Off', '30s', '60s', '90s', '120s',
'150s', '180s', '210s', '240s', '270s']
DATA_MODES = ['MSK', 'DTMF', '5-Tone']
ASCIIPART = ''.join([chr(x) for x in range(0x20, 0x7f)])
DTMF = "0123456789ABCD*#"
HEXADECIMAL = "0123456789ABCDEF"
ROGER_BEEP = OFF_INT[0:11]
BACKLIGHT_MODES = ["Off", "Auto", "On"]
TONE_RESET_TIME = ['Off'] + ['%ds' % x for x in range(1, 256)]
DTMF_TONE_RESET_TIME = TONE_RESET_TIME[0:16]
DTMF_GROUPS = list(zip(["Off", "A", "B", "C", "D", "*", "#"], [255] +
list(range(10, 16))))
FIVE_TONE_STANDARDS = ['ZVEI1', 'ZVEI2', 'CCIR1', 'CCITT']
# should mimic the defaults in the memedit MemoryEditor somewhat
# 0 1 2 3 4 5 6 7
SANE_MEMORY_DEFAULT = b"\x14\x61\x00\x00\x14\x61\x00\x00" + \
b"\xff\xff\xff\xff\xc8\x00\xff\xff"
# 8 9 A B C D E F
# these two option sets are listed differently like this in the stock software,
# so I'm keeping them separate for now if they are in fact identical
# in behaviour, that should probably be amended
DTMF_ALERT_TRANSPOND = list(zip(['Off', 'Call alert',
'Transpond-alert',
'Transpond-ID code'],
[255] + list(range(1, 4))))
FIVE_TONE_ALERT_TRANSPOND = list(zip(['Off', 'Alert tone',
'Transpond', 'Transpond-ID code'],
[255] + list(range(1, 4))))
BFM_BANDS = ['87.5-108 MHz', '76.0-91.0 MHz', '76.0-108.0 MHz', '65.0-76.0 MHz']
BFM_STRIDE = ['100 kHz', '50 kHz']
def piperead(pipe, amount):
"""read some data, catch exceptions, validate length of data read"""
try:
d = pipe.read(amount)
except Exception as e:
raise errors.RadioError(
"Tried to read %d bytes, but got an exception: %s" %
(amount, repr(e)))
if d is None:
raise errors.RadioError(
"Tried to read %d bytes, but read operation returned ." %
(amount))
if d is None or len(d) != amount:
raise errors.RadioError(
"Tried to read %d bytes, but got %d bytes instead." %
(amount, len(d)))
return d
def pipewrite(pipe, data):
"""write some data, catch exceptions, validate length of data written"""
try:
n = pipe.write(data)
except Exception as e:
raise errors.RadioError(
"Tried to write %d bytes, but got an exception: %s." %
(len(data), repr(e)))
if n is None:
raise errors.RadioError(
"Tried to write %d bytes, but operation returned ." %
(len(data)))
if n != len(data):
raise errors.RadioError(
"Tried to write %d bytes, but wrote %d bytes instead." %
(len(data), n))
def attempt_initial_handshake(pipe):
"""try to do the initial handshake"""
pipewrite(pipe, HANDSHAKE_OUT)
x = piperead(pipe, len(HANDSHAKE_IN[0]))
if x in HANDSHAKE_IN:
return True
LOG.debug("Handshake failed: received: %s expected one of: %s" %
(repr(x), repr(HANDSHAKE_IN)))
return False
def initial_handshake(pipe, tries):
"""do an initial handshake attempt up to tries times"""
x = False
for i in range(tries):
x = attempt_initial_handshake(pipe)
if x:
break
if not x:
raise errors.RadioError("Initial handshake failed all ten tries.")
def mk_writecommand(addr):
"""makes a write command from an address specification"""
return pack('>cHc', b'W', addr, b'@')
def mk_readcommand(addr):
"""makes a read command from an address specification"""
return pack('>cHc', b'R', addr, b'@')
def expect_ack(pipe):
x = piperead(pipe, 1)
if x != b'\x06':
LOG.debug(
"Did not get ACK. received: %s, expected: '\\x06'" %
repr(x))
raise errors.RadioError("Did not get ACK when expected.")
def end_communications(pipe):
"""tell the radio that we are done"""
pipewrite(pipe, b'E')
expect_ack(pipe)
def read_block(pipe, addr):
"""read and return a chunk of data at specified address"""
r = mk_readcommand(addr)
w = mk_writecommand(addr)
pipewrite(pipe, r)
x = piperead(pipe, len(w))
if x != w:
raise errors.RadioError("Received data not following protocol.")
block = piperead(pipe, BLOCKSIZE)
return block
def write_block(pipe, addr, block):
"""write a chunk of data at specified address"""
w = mk_writecommand(addr)
pipewrite(pipe, w)
pipewrite(pipe, block)
expect_ack(pipe)
def show_progress(radio, blockaddr, upper, msg):
"""relay read/write information to the user through the gui"""
if radio.status_fn:
status = chirp_common.Status()
status.cur = blockaddr
status.max = upper
status.msg = msg
radio.status_fn(status)
def do_download(radio):
"""download from the radio to the memory map"""
initial_handshake(radio.pipe, 10)
memory = memmap.MemoryMapBytes(b'\xff'*0x1000)
for blockaddr in range(LOWER_READ_BOUND, UPPER_READ_BOUND, BLOCKSIZE):
LOG.debug("Reading block "+str(blockaddr))
block = read_block(radio.pipe, blockaddr)
memory.set(blockaddr, block)
show_progress(radio, blockaddr, UPPER_READ_BOUND,
"Reading radio memory... %04x" % blockaddr)
end_communications(radio.pipe)
return memory
def do_upload(radio):
"""upload from the memory map to the radio"""
memory = radio.get_mmap()
initial_handshake(radio.pipe, 10)
for blockaddr in range(LOWER_WRITE_BOUND, UPPER_WRITE_BOUND, BLOCKSIZE):
LOG.debug("Writing block "+str(blockaddr))
block = memory[blockaddr:blockaddr+BLOCKSIZE]
write_block(radio.pipe, blockaddr, block)
show_progress(radio, blockaddr, UPPER_WRITE_BOUND,
"Writing radio memory... % 04x" % blockaddr)
end_communications(radio.pipe)
def parse_tone(t):
"""
parse the tone (ctss, dtcs) part of the mmap
into more easily handled data types
"""
# [ mode, value, polarity ]
if int(t.high) == 0x3f and int(t.low) == 0xff:
return [None, None, None]
elif bool(t.digital):
t = ['DTCS',
(int(t.high) & 0x0f)*100 +
((int(t.low) & 0xf0) >> 4)*10 +
(int(t.low) & 0x0f),
['N', 'R'][bool(t.invert)]]
if t[1] not in chirp_common.DTCS_CODES:
return [None, None, None]
else:
t = ['Tone',
((int(t.high) & 0xf0) >> 4)*100 +
(int(t.high) & 0x0f)*10 +
((int(t.low) & 0xf0) >> 4) +
(int(t.low) & 0x0f)/10.0,
None]
if t[1] not in chirp_common.TONES:
return [None, None, None]
return t
def unparse_tone(t):
"""parse tone data back into the format used by the radio"""
# [ mode, value, polarity ]
if t[0] == 'Tone':
tint = int(t[1]*10)
t0, tint = tint % 10, tint // 10
t1, tint = tint % 10, tint // 10
t2, tint = tint % 10, tint // 10
high = (tint << 4) | t2
low = (t1 << 4) | t0
digital = False
invert = False
return digital, invert, high, low
elif t[0] == 'DTCS':
tint = int(t[1])
t0, tint = tint % 10, tint // 10
t1, tint = tint % 10, tint // 10
high = tint
low = (t1 << 4) | t0
digital = True
invert = t[2] == 'R'
return digital, invert, high, low
return None
def decode_halfbytes(data, mapping, length):
"""
construct a string from a datatype
where each half-byte maps to a character
"""
s = ''
for i in range(length):
if i & 1 == 0:
s += mapping[(int(data[i >> 1]) & 0xf0) >> 4]
else:
s += mapping[int(data[i >> 1]) & 0x0f]
return s
def encode_halfbytes(data, datapad, mapping, fillvalue, fieldlen):
"""encode data from a string where each character maps to a half-byte"""
if len(data) & 1:
# pad to an even length
data += datapad
o = [fillvalue] * fieldlen
for i in range(0, len(data), 2):
v = (mapping.index(data[i]) << 4) | mapping.index(data[i+1])
o[i >> 1] = v
return bytearray(util.byte_to_int(b) for b in o)
def decode_ffstring(data):
"""decode a string delimited by 0xff"""
s = ''
for b in data:
if int(b) == 0xff:
break
s += chr(int(b))
return s
def encode_ffstring(data, fieldlen):
"""right-pad to specified length with 0xff bytes"""
extra = fieldlen-len(data)
if extra > 0:
data += '\xff'*extra
return bytearray([util.byte_to_int(x) for x in data])
def decode_dtmf(data, length):
"""decode a field containing dtmf data into a string"""
if length == 0xff:
return ''
return decode_halfbytes(data, DTMF, length)
def encode_dtmf(data, length, fieldlen):
"""encode a string containing dtmf characters into a data field"""
return encode_halfbytes(data, '0', DTMF, b'\xff', fieldlen)
def decode_5tone(data):
"""decode a field containing 5-tone data into a string"""
if (int(data[2]) & 0x0f) != 0:
return ''
return decode_halfbytes(data, HEXADECIMAL, 5)
def encode_5tone(data, fieldlen):
"""encode a string containing 5-tone characters into a data field"""
return encode_halfbytes(data, '0', HEXADECIMAL, b'\xff', fieldlen)
def decode_freq(data):
"""decode frequency data for the broadcast FM radio memories"""
data_out = ''
if data[0] != 0xff:
data_out = chirp_common.format_freq(
int(decode_halfbytes(data, "0123456789", len(data)))*100000)
return data_out
def encode_freq(data, fieldlen):
"""encode frequency data for the broadcast FM radio memories"""
data_out = bytearray(b'\xff')*fieldlen
if data != '':
data_out = encode_halfbytes((('%%0%di' % (fieldlen << 1)) %
int(chirp_common.parse_freq(data)/10)),
'', '0123456789', '', fieldlen)
return data_out
def sbyn(s, n):
"""setting by name"""
return list(filter(lambda x: x.get_name() == n, s))[0]
# These helper classes provide a direct link between the value
# of the widget shown in the ui, and the setting in the memory
# map of the radio, lessening the need to write large chunks
# of code, first for populating the ui from the memory map,
# then secondly for parsing the values back.
# By supplying the memory map entry to the setting instance,
# it is possible to automatically 1) initialize the value of
# the setting, as well as 2) automatically update the memory
# value when the user changes it in the ui, without adding
# any code outside the class.
class MappedIntegerSettingValue(settings.RadioSettingValueInteger):
""""
Integer setting, with the possibility to add translation
functions between memory map <-> integer setting
"""
def __init__(self, val_mem, minval, maxval, step=1,
int_from_mem=lambda x: int(x),
mem_from_int=lambda x: x,
autowrite=True):
"""
val_mem - memory map entry for the value
minval - the minimum value allowed
maxval - maximum value allowed
step - value stepping
int_from_mem - function to convert memory entry to integer
mem_from_int - function to convert integer to memory entry
autowrite - automatically write the memory map entry
when the value is changed
"""
self._val_mem = val_mem
self._int_from_mem = int_from_mem
self._mem_from_int = mem_from_int
self._autowrite = autowrite
settings.RadioSettingValueInteger.__init__(
self,
minval, maxval, self._int_from_mem(val_mem), step)
def set_value(self, x):
settings.RadioSettingValueInteger.set_value(self, x)
if self._autowrite:
self.write_mem()
def write_mem(self):
if self.get_mutable() and self._mem_from_int is not None:
self._val_mem.set_value(self._mem_from_int(
settings.RadioSettingValueInteger.get_value(self)))
class MappedListSettingValue(settings.RadioSettingValueMap):
"""Mapped list setting"""
def __init__(self, val_mem, options, autowrite=True):
"""
val_mem - memory map entry for the value
options - either a list of strings options to present,
mapped to integers 0...n
in the memory map entry, or a list of tuples
("option description", memory map value)
int_from_mem - function to convert memory entry to integer
mem_from_int - function to convert integer to memory entry
autowrite - automatically write the memory map entry when
the value is changed
"""
self._val_mem = val_mem
self._autowrite = autowrite
options = list(options)
if not isinstance(options[0], tuple):
options = list(zip(options, range(len(options))))
settings.RadioSettingValueMap.__init__(
self,
options, mem_val=int(val_mem))
def set_value(self, value):
settings.RadioSettingValueMap.set_value(self, value)
if self._autowrite:
self.write_mem()
def write_mem(self):
if self.get_mutable():
self._val_mem.set_value(
settings.RadioSettingValueMap.get_mem_val(self))
class MappedCodedStringSettingValue(settings.RadioSettingValueString):
"""
generic base class for a number of mapped presented-as-strings
values which may need conversion between mem and string,
and may store a length value in a separate mem field
"""
def __init__(self, val_mem, len_mem, min_length, max_length,
charset=ASCIIPART, padchar=' ', autowrite=True,
str_from_mem=lambda mve, lve: str(mve[0:int(lve)]),
mem_val_from_str=lambda s, fl: s[0:fl],
mem_len_from_int=lambda l: l):
"""
val_mem - memory map entry for the value
len_mem - memory map entry for the length (or None)
min_length - length that the string will be right-padded to
max_length - maximum length of the string, set as maxlength
for the RadioSettingValueString
charset - the allowed charset
padchar - the character that will be used to pad short
strings, if not in the charset, charset[0] is used
autowrite - automatically call write_mem when the ui value
change
str_from_mem - function to convert from memory entry to string
value, form:
func(value_entry, length_entry or none) -> string
mem_val_from_str - function to convert from string value to
memory-fitting value, form:
func(string, value_entry_length) ->
value to store in value entry
mem_len_from_int - function to convert from string length to
memory-fitting value, form:
func(stringlength) -> value to store in length entry
"""
self._min_length = min_length
self._val_mem = val_mem
self._len_mem = len_mem
self._padchar = padchar
if padchar not in charset:
self._padchar = charset[0]
self._autowrite = autowrite
self._str_from_mem = str_from_mem
self._mem_val_from_str = mem_val_from_str
self._mem_len_from_int = mem_len_from_int
settings.RadioSettingValueString.__init__(
self,
0, max_length,
self._str_from_mem(self._val_mem, self._len_mem),
charset=charset, autopad=False)
def set_value(self, value):
"""
Set the value of the string, pad if below minimum length,
unless it's '' to provide a distinction between
uninitialized/reset data and needs-to-be-padded data
"""
while len(value) < self._min_length and len(value) != 0:
value += self._padchar
settings.RadioSettingValueString.set_value(self, value)
if self._autowrite:
self.write_mem()
def write_mem(self):
"""update the memory"""
if not self.get_mutable() or self._mem_val_from_str is None:
return
v = self.get_value()
l = len(v)
self._val_mem.set_value(self._mem_val_from_str(v, len(self._val_mem)))
if self._len_mem is not None and self._mem_len_from_int is not None:
self._len_mem.set_value(self._mem_len_from_int(l))
class MappedFFStringSettingValue(MappedCodedStringSettingValue):
"""
Mapped string setting, tailored for the puxing px888k,
which uses 0xff terminated strings.
"""
def __init__(self, val_mem, min_length, max_length,
charset=ASCIIPART, padchar=' ', autowrite=True):
MappedCodedStringSettingValue.__init__(
self,
val_mem, None, min_length, max_length,
charset=charset, padchar=padchar, autowrite=autowrite,
str_from_mem=lambda mve, lve: decode_ffstring(mve),
mem_val_from_str=lambda s, fl: encode_ffstring(s, fl),
mem_len_from_int=None)
class MappedDTMFStringSettingValue(MappedCodedStringSettingValue):
"""
Mapped string setting, tailored for the puxing px888k
field pairs (value and length) storing DTMF codes
"""
def __init__(self, val_mem, len_mem, min_length, max_length,
autowrite=True):
MappedCodedStringSettingValue.__init__(
self,
val_mem, len_mem, min_length, max_length,
charset=DTMF, padchar='0', autowrite=autowrite,
str_from_mem=lambda mve, lve: decode_dtmf(mve, lve),
mem_val_from_str=lambda s, fl: encode_dtmf(s, len(s), fl))
class MappedFiveToneStringSettingValue(MappedCodedStringSettingValue):
"""
Mapped string setting, tailored for the puxing px888k
fields storing 5-Tone codes
"""
def __init__(self, val_mem, autowrite=True):
MappedCodedStringSettingValue.__init__(
self,
val_mem, None, 0, 5, charset=HEXADECIMAL, padchar='0',
autowrite=autowrite,
str_from_mem=lambda mve, lve: decode_5tone(mve),
mem_val_from_str=lambda s, fl: encode_5tone(s, fl),
mem_len_from_int=None)
class MappedFreqStringSettingValue(MappedCodedStringSettingValue):
"""
Mapped string setting, tailored for the puxing px888k
fields for the broadcast FM radio frequencies
"""
def __init__(self, val_mem, autowrite=True):
MappedCodedStringSettingValue.__init__(
self,
val_mem, None, 0, 128, charset=ASCIIPART, padchar=' ',
autowrite=autowrite,
str_from_mem=lambda mve, lve: decode_freq(mve),
mem_val_from_str=lambda s, fl: encode_freq(s, fl))
# These functions lessen the amount of boilerplate on the form
# x = RadioSetting("AAA", "BBB", SomeKindOfRadioSettingValue( ... ))
# to
# x = some_kind_of_setting("AAA", "BBB", ... )
def integer_setting(k, n, *args, **kwargs):
return settings.RadioSetting(
k, n,
MappedIntegerSettingValue(*args, **kwargs))
def list_setting(k, n, *args, **kwargs):
return settings.RadioSetting(
k, n,
MappedListSettingValue(*args, **kwargs))
def ff_string_setting(k, n, *args, **kwargs):
return settings.RadioSetting(
k, n,
MappedFFStringSettingValue(*args, **kwargs))
def dtmf_string_setting(k, n, *args, **kwargs):
return settings.RadioSetting(
k, n,
MappedDTMFStringSettingValue(*args, **kwargs))
def five_tone_string_setting(k, n, *args, **kwargs):
return settings.RadioSetting(
k, n,
MappedFiveToneStringSettingValue(*args, **kwargs))
def frequency_setting(k, n, *args, **kwargs):
return settings.RadioSetting(
k, n,
MappedFreqStringSettingValue(*args, **kwargs))
@directory.register
class Puxing_PX888K_Radio(chirp_common.CloneModeRadio):
"""Puxing PX-888K"""
VENDOR = "Puxing"
MODEL = "PX-888K"
BAUD_RATE = 9600
@classmethod
def match_model(cls, filedata, filename):
if len(filedata) == UPPER_READ_BOUND:
if filedata[FILE_MAGIC[0]:FILE_MAGIC[1]] == FILE_MAGIC[2]:
return True
return False
def get_features(self):
rf = chirp_common.RadioFeatures()
rf.has_bank_index = False
rf.has_dtcs = True
rf.has_ctone = True
rf.has_rx_dtcs = True
rf.has_dtcs_polarity = True
rf.has_mode = True
rf.has_offset = True
rf.has_name = True
rf.has_bank = False
rf.has_bank_names = False
rf.has_tuning_step = False
rf.has_cross = True
rf.has_infinite_number = False
rf.has_nostep_tuning = False
rf.has_comment = False
rf.has_settings = True
if SUPPORT_NONSPLIT_DUPLEX_ONLY:
rf.can_odd_split = False
else:
rf.can_odd_split = True
rf.valid_modes = MODES
rf.valid_tmodes = TONE_MODES
rf.valid_duplexes = DUPLEX_MODES
rf.valid_bands = BANDS
rf.valid_skips = SKIP_MODES
rf.valid_power_levels = POWER_LEVELS
rf.valid_characters = ASCIIPART
rf.valid_name_length = 6
rf.valid_cross_modes = CROSS_MODES
rf.memory_bounds = (1, 128)
rf.valid_special_chans = list(SPECIAL_CHANNELS.keys())
rf.valid_tuning_steps = [5.0, 6.25, 10.0, 12.5, 25.0]
return rf
def sync_in(self):
self._mmap = do_download(self)
self.process_mmap()
def process_mmap(self):
self._memobj = bitwise.parse(MEM_FORMAT, self._mmap)
def sync_out(self):
do_upload(self)
def _set_sane_defaults(self, data):
# thanks thayward!
data.set_raw(SANE_MEMORY_DEFAULT)
def _uninitialize(self, data, n):
if isinstance(data, bitwise.arrayDataElement):
data.set_value(b"\xff"*n)
else:
data.set_raw(b"\xff"*n)
def _get_memory_structs(self, number):
"""
fetch the correct data structs no matter
if its regular or special channels,
no matter if they're referred by name or channel index
"""
index = 2501
i = -42
designator = 'INVALID'
isregular = False
iscall = False
isvfo = False
_data = None
_name = None
_present = None
_priority = None
if number in SPECIAL_NUMBERS.keys():
index = number
# special by index
designator = SPECIAL_NUMBERS[number]
elif number in SPECIAL_CHANNELS.keys():
# special by name
index = SPECIAL_CHANNELS[number]
designator = number
elif number > 0:
# regular by number
index = number
designator = number
if index < 0:
isvfo = True
_data = self._memobj.mem.vfo_data[index+2]
elif index == 0:
iscall = True
_data = self._memobj.mem.call_channel
elif index > 0:
isregular = True
i = number - 1
_data = self._memobj.mem.channel_memory.data[i]
_name = self._memobj.mem.channel_memory.names[i].entry
_present = self._memobj.mem.channel_memory.present[
(i & 0x78) | (7-(i & 0x07))]
_priority = self._memobj.mem.channel_memory.priority[
(i & 0x78) | (7-(i & 0x07))]
if _data == bytearray(0xff)*16:
self._set_sane_defaults(_data)
return (index, designator,
_data, _name, _present, _priority,
isregular, isvfo, iscall)
def get_raw_memory(self, number):
x = self._get_memory_structs(number)
return repr(x[2])
def get_memory(self, number):
mem = chirp_common.Memory()
(index, designator,
_data, _name, _present, _priority,
isregular, isvfo, iscall) = self._get_memory_structs(number)
mem.number = index
# handle empty channels
if isregular:
if bool(_present):
mem.empty = False
mem.name = str(decode_ffstring(_name))
mem.skip = SKIP_MODES[1-int(_priority)]
else:
mem.empty = True
mem.name = ''
return mem
else:
mem.empty = False
mem.name = ''
mem.extd_number = designator
# get frequency data
mem.freq = int(_data.rx_freq)*10
mem.offset = int(_data.tx_freq)*10
# interpret frequency data
# only the vfo channels support duplex,
# memory channels operate in split mode all the time
if isvfo:
mem.duplex = DUPLEX_MODES[int(_data.duplex_sign)]
if mem.duplex == '-':
mem.offset = mem.freq - mem.offset
elif mem.duplex == '':
mem.offset = 0
elif mem.duplex == '+':
mem.offset = mem.offset - mem.freq
else:
if mem.freq == mem.offset:
mem.duplex = ''
mem.offset = 0
elif SUPPORT_NONSPLIT_DUPLEX_ONLY or \
SUPPORT_SPLIT_BUT_DEFAULT_TO_NONSPLIT_ALWAYS:
if mem.freq > mem.offset:
mem.offset = mem.freq - mem.offset
mem.duplex = '-'
elif mem.freq < mem.offset:
mem.offset = mem.offset - mem.freq
mem.duplex = '+'
else:
mem.duplex = 'split'
# get tone data
txtone = parse_tone(_data.tone[0])
rxtone = parse_tone(_data.tone[1])
chirp_common.split_tone_decode(mem, txtone, rxtone)
######################################################################
if ENABLE_DANGEROUS_EXPERIMENTAL_FEATURES:
# override certain settings based on flags
# that we have set in junk areas of the memory
# or basically, we BELIEVE this to be junk memory,
# hence why it's experimental and dangerous
if bool(_data.experimental_cross_mode_indicator) is False:
if mem.tmode == 'Tone':
mem.cross_mode = 'Tone->'
elif mem.tmode == 'TSQL':
mem.cross_mode = 'Tone->Tone'
elif mem.tmode == 'DTCS':
mem.cross_mode = 'DTCS->DTCS'
mem.tmode = 'Cross'
######################################################################
# transmit mode and power level
mem.mode = MODES[bool(_data.modulation_width)]
mem.power = POWER_LEVELS[_data.txpower]
# extra channel settings
mem.extra = settings.RadioSettingGroup(
"extra",
"extra",
list_setting("Busy channel lockout",
"BCL",
_data.bcl,
BCL_MODES),
list_setting("Swap transmit and receive frequencies",
"Tx Rx freq swap",
_data.txrx_reverse,
OFF_ON),
list_setting("Use compander",
"Use compander",
_data.compander,
OFF_ON),
list_setting("Use scrambler", "Use scrambler",
_data.use_scrambler,
NO_YES),
list_setting("Scrambler selection",
"Voice Scrambler",
_data.scrambler_type,
SCRAMBLER_MODES),
list_setting("Send ID code before and/or after transmitting",
"PTT ID",
_data.ptt_id_edge,
PTT_ID_EDGES),
list_setting("Optional signal before/after transmission, " +
"this setting overrides the PTT ID setting",
"Opt Signal",
_data.opt_signal,
OPTSIGN_MODES))
######################################################################
if ENABLE_DANGEROUS_EXPERIMENTAL_FEATURES:
# override certain settings based on flags
# that we have set in junk areas of the memory
# or basically, we BELIEVE this to be junk memory,
# hence why it's experimental and dangerous
if bool(_data.experimental_duplex_mode_indicator) is False:
# if this flag is set, this means that we in the gui
# have set the duplex mode to something
# the channel does not really support,
# such as split modes for vfo channels,
# and non-split modes for the memory channels
mem.duplex = DUPLEX_MODES[int(_data.duplex_sign)]
mem.freq = int(_data.rx_freq)*10
mem.offset = int(_data.tx_freq)*10
if isvfo:
# we want split, so we have to reconstruct it
# from -/0/+ modes
if mem.duplex == '-':
mem.offset = mem.freq - mem.offset
elif mem.duplex == '':
mem.offset = mem.freq
elif mem.duplex == '+':
mem.offset = mem.freq + mem.offset
mem.duplex = 'split'
else:
# we want -/0/+, so we have to reconstruct it
# from split modes
if mem.freq > mem.offset:
mem.offset = mem.freq - mem.offset
mem.duplex = '-'
elif mem.freq < mem.offset:
mem.offset = mem.offset - mem.freq
mem.duplex = '+'
else:
mem.offset = 0
mem.duplex = ''
######################################################################
return mem
def set_memory(self, mem):
(index, designator,
_data, _name, _present, _priority,
isregular, isvfo, iscall) = self._get_memory_structs(mem.number)
# handle empty channels
if mem.empty:
if isregular:
_present.set_value(False)
_priority.set_value(False)
self._uninitialize(_data, 16)
self._uninitialize(_name, 6)
else:
raise errors.InvalidValueError(
"Can't remove CALL and/or VFO channels!")
return
# handle regular channel stuff like name and present+priority flags
if isregular:
if not bool(_present):
self._set_sane_defaults(_data)
_name.set_value(
encode_ffstring(self.filter_name(mem.name.rstrip()),
len(_name)))
_present.set_value(True)
_priority.set_value(1-SKIP_MODES.index(mem.skip))
# frequency data
rxf = int(mem.freq/10)
txf = int(mem.offset/10)
_data.rx_freq.set_value(rxf)
if isvfo:
# fake split modes on write, for channels
# that do not support it, which are some
# (the two vfo channels)
if mem.duplex == 'split':
for band in BANDS:
rb = mem.freq in range(band[0], band[1])
tb = mem.offset in range(band[0], band[1])
if rb != tb:
raise errors.InvalidValueError(
"VFO frequencies should be on the same band")
if rxf < txf:
_data.duplex_sign.set_value(1)
_data.tx_freq.set_value(txf - rxf)
elif rxf > txf:
_data.duplex_sign.set_value(2)
_data.tx_freq.set_value(rxf-txf)
else:
_data.duplex_sign.set_value(0)
_data.tx_freq.set_value(0)
else:
_data.duplex_sign.set_value(DUPLEX_MODES.index(mem.duplex))
_data.tx_freq.set_value(txf)
######################################################################
if ENABLE_DANGEROUS_EXPERIMENTAL_FEATURES:
if mem.duplex == 'split':
_data.experimental_duplex_mode_indicator.set_value(0)
else:
_data.experimental_duplex_mode_indicator.set_value(1)
######################################################################
else:
# fake duplex modes on write, for channels
# that do not support it, which are most
# (all the memory channels)
if mem.duplex == '' or mem.duplex is None:
_data.tx_freq.set_value(rxf)
elif mem.duplex == '+':
_data.tx_freq.set_value(rxf + txf)
elif mem.duplex == '-':
_data.tx_freq.set_value(rxf - txf)
else:
_data.tx_freq.set_value(txf)
######################################################################
if ENABLE_DANGEROUS_EXPERIMENTAL_FEATURES:
if mem.duplex != 'split':
_data.experimental_duplex_mode_indicator.set_value(0)
else:
_data.experimental_duplex_mode_indicator.set_value(1)
######################################################################
# tone data
tonedata = chirp_common.split_tone_encode(mem)
for i in range(2):
dihl = unparse_tone(tonedata[i])
if dihl is not None:
_data.tone[i].digital.set_value(dihl[0])
_data.tone[i].invert.set_value(dihl[1])
_data.tone[i].high.set_value(dihl[2])
_data.tone[i].low.set_value(dihl[3])
else:
_data.tone[i].digital.set_value(1)
_data.tone[i].invert.set_value(1)
_data.tone[i].high.set_value(0x3f)
_data.tone[i].low.set_value(0xff)
######################################################################
if ENABLE_DANGEROUS_EXPERIMENTAL_FEATURES:
if mem.tmode == 'Cross' and mem.cross_mode in ['Tone->',
'Tone->Tone',
'DTCS->DTCS']:
_data.experimental_cross_mode_indicator.set_value(0)
else:
_data.experimental_cross_mode_indicator.set_value(1)
######################################################################
# transmit mode and power level
_data.modulation_width.set_value(MODES.index(mem.mode))
if str(mem.power) == 'High':
_data.txpower.set_value(1)
else:
_data.txpower = 0
def get_settings(self):
_model = self._memobj.mem.model_information
_settings = self._memobj.mem.opt_settings
_ptt_id_data = self._memobj.mem.ptt_id_data
_msk_settings = self._memobj.mem.msk_settings
_dtmf_settings = self._memobj.mem.dtmf_settings
_5tone_settings = self._memobj.mem.five_tone_settings
_broadcast = self._memobj.mem.fm_radio
# for safety reasons we are showing these as read-only
model_unit_settings = [
integer_setting("vhflo", "VHF lower bound",
_model.band_limits[0].lower_freq,
134, 176,
int_from_mem=lambda x:int(int(x)/10),
mem_from_int=None),
integer_setting("vhfhi", "VHF upper bound",
_model.band_limits[0].upper_freq,
134, 176,
int_from_mem=lambda x:int(int(x)/10),
mem_from_int=None),
integer_setting("uhflo", "UHF lower bound",
_model.band_limits[1].lower_freq,
400, 480,
int_from_mem=lambda x:int(int(x)/10),
mem_from_int=None),
integer_setting("uhfhi", "UHF upper bound",
_model.band_limits[1].upper_freq,
400, 480,
int_from_mem=lambda x:int(int(x)/10),
mem_from_int=None),
ff_string_setting("model", "Model string",
_model.model_string,
0, 6)
]
for s in model_unit_settings:
s.value.set_mutable(False)
model_unit_settings.append(ff_string_setting(
"info", "Unit Information",
self._memobj.mem.radio_information_string,
0, 16))
# tx/rx related settings
radio_channel_settings = [
list_setting("vfostep", "VFO step size",
_settings.vfo_step,
VFO_STRIDE),
list_setting("abwatch", "Main watch",
_settings.main_watch,
AB),
list_setting("watchmade", "Watch mode",
_settings.main_watch,
WATCH_MODES),
list_setting("amode", "A mode",
_settings.workmode_a,
AB_MODES),
list_setting("bmode", "B mode",
_settings.workmode_b,
AB_MODES),
integer_setting("achan", "A channel index",
_settings.channel_a,
1, 128,
int_from_mem=lambda i:i+1,
mem_from_int=lambda i:i-1),
integer_setting("bchan", "B channel index",
_settings.channel_b,
1, 128,
int_from_mem=lambda i:i+1,
mem_from_int=lambda i:i-1),
integer_setting("pchan", "Priority channel index",
_settings.priority_channel,
1, 128, int_from_mem=lambda i:i+1,
mem_from_int=lambda i:i-1),
list_setting("cactive", "Call channel active?",
_settings.call_channel_active,
NO_YES),
list_setting("scanm", "Scan mode",
_settings.scan_mode,
SCAN_MODES),
list_setting("swait", "Wait time",
_settings.wait_time,
WAIT_TIMES),
# it is unclear what this option below does,
# possibly squelch tail elimination?
list_setting("tail", "Relay without disable tail (?)",
_settings.relay_without_disable_tail,
NO_YES),
list_setting("batsav", "Battery saving mode",
_settings.battery_save,
OFF_ON),
]
# user interface related settings
interface_settings = [
list_setting("sidehold", "Side button hold action",
_settings.side_button_hold_mode,
BUTTON_MODES),
list_setting("sideclick", "Side button click action",
_settings.side_button_click_mode,
BUTTON_MODES),
list_setting("bootmt", "Boot message type",
_settings.boot_message_mode,
BOOT_MESSAGE_TYPES),
ff_string_setting("bootm", "Boot message",
_settings.boot_message,
0, 6),
list_setting("beep", "Key beep",
_settings.key_beep,
OFF_ON),
list_setting("talkback", "Menu talkback",
_settings.voice_announce,
TALKBACK),
list_setting("sidetone", "DTMF sidetone",
_settings.dtmf_sidetone,
OFF_ON),
list_setting("roger", "Roger beep",
_settings.use_roger_beep,
ROGER_BEEP),
list_setting("backlm", "Backlight mode",
_settings.backlight_mode,
BACKLIGHT_MODES),
list_setting("backlc", "Backlight color",
_settings.backlight_color,
BACKLIGHT_COLORS),
integer_setting("squelch", "Squelch level",
_settings.squelch_level,
0, 9),
list_setting("voxg", "Vox gain",
_settings.vox_gain,
VOX_GAIN),
list_setting("voxd", "Vox delay",
_settings.vox_delay,
VOX_DELAYS),
list_setting("txal", "Trinsmit time alarm",
_settings.tx_timeout,
TRANSMIT_ALARMS),
]
# settings related to tone/data sending and interpretation
data_general_settings = [
list_setting("disptt", "Display PTT ID",
_settings.dis_ptt_id,
NO_YES),
list_setting("pttidt", "PTT ID signal type",
_settings.ptt_id_type,
DATA_MODES)
]
data_msk_settings = [
ff_string_setting("bot", "MSK PTT ID (BOT)",
_ptt_id_data[0].entry,
0, 6, autowrite=False),
ff_string_setting("eot", "MSK PTT ID (EOT)",
_ptt_id_data[1].entry,
0, 6, autowrite=False),
ff_string_setting("id", "MSK ID code",
_msk_settings.id_code,
0, 4, charset=HEXADECIMAL),
list_setting("mskr", "MSK reverse",
_settings.msk_reverse,
NO_YES)
]
data_dtmf_settings = [
dtmf_string_setting("bot", "DTMF PTT ID (BOT)",
_ptt_id_data[0].entry,
_ptt_id_data[0].length,
0, 8, autowrite=False),
dtmf_string_setting("eot", "DTMF PTT ID (EOT)",
_ptt_id_data[1].entry,
_ptt_id_data[1].length,
0, 8, autowrite=False),
dtmf_string_setting("id", "DTMF ID code",
_dtmf_settings.id_code,
_dtmf_settings.id_code_length,
3, 8),
integer_setting("time", "Digit time (ms)",
_dtmf_settings.timing.digit_length,
50, 200, step=10,
int_from_mem=lambda x:x*10,
mem_from_int=lambda x:int(x/10)),
integer_setting("pause", "Inter digit time (ms)",
_dtmf_settings.timing.digit_length,
50, 200, step=10,
int_from_mem=lambda x:x*10,
mem_from_int=lambda x:int(x/10)),
integer_setting("time1", "First digit time (ms)",
_dtmf_settings.timing.digit_length,
50, 200, step=10,
int_from_mem=lambda x:x*10,
mem_from_int=lambda x:int(x/10)),
integer_setting("pause1", "First digit delay (ms)",
_dtmf_settings.timing.digit_length,
100, 1000, step=50,
int_from_mem=lambda x:x*50,
mem_from_int=lambda x:int(x/50)),
list_setting("arst", "Auto reset time",
_dtmf_settings.reset_time,
DTMF_TONE_RESET_TIME),
list_setting("grp", "Group code",
_dtmf_settings.group_code,
DTMF_GROUPS),
dtmf_string_setting("stunt", "TX Stun code",
_dtmf_settings.tx_stun_code,
_dtmf_settings.tx_stun_code_length,
3, 8),
dtmf_string_setting("cstunt", "TX Stun cancel code",
_dtmf_settings.cancel_tx_stun_code,
_dtmf_settings.cancel_tx_stun_code_length,
3, 8),
dtmf_string_setting("stunrt", "RX/TX Stun code",
_dtmf_settings.rxtx_stun_code,
_dtmf_settings.rxtx_stun_code_length,
3, 8),
dtmf_string_setting("cstunrt", "RX/TX Stun cancel code",
_dtmf_settings.cancel_rxtx_stun_code,
_dtmf_settings.cancel_rxtx_stun_code_length,
3, 8),
list_setting("altr", "Alert/Transpond",
_dtmf_settings.alert_transpond,
DTMF_ALERT_TRANSPOND),
]
data_5tone_settings = [
five_tone_string_setting("bot",
"5-Tone PTT ID (BOT)",
_ptt_id_data[0].entry,
autowrite=False),
five_tone_string_setting("eot",
"5-Tone PTT ID (EOT)",
_ptt_id_data[1].entry,
autowrite=False),
five_tone_string_setting("id",
"5-tone ID code",
_5tone_settings.id_code),
list_setting("arst", "Auto reset time",
_5tone_settings.reset_time,
TONE_RESET_TIME),
five_tone_string_setting("stunt",
"TX Stun code",
_5tone_settings.tx_stun_code),
five_tone_string_setting("cstunt",
"TX Stun cancel code",
_5tone_settings.cancel_tx_stun_code),
five_tone_string_setting("stunrt",
"RX/TX Stun code",
_5tone_settings.rxtx_stun_code),
five_tone_string_setting("cstunrt",
"RX/TX Stun cancel code",
_5tone_settings.cancel_rxtx_stun_code),
list_setting("altr", "Alert/Transpond",
_5tone_settings.alert_transpond,
FIVE_TONE_ALERT_TRANSPOND),
list_setting("std", "5-Tone standard",
_5tone_settings.tone_standard,
FIVE_TONE_STANDARDS),
]
for i in range(4):
s = ['z1', 'z2', 'c1', 'ct'][i]
l = FIVE_TONE_STANDARDS[i]
data_5tone_settings.append(
settings.RadioSettingGroup(
s, '%s settings' % l,
integer_setting("%speriod" % s, "%s Period (ms)" % l,
_5tone_settings.tone_settings[i].period,
20, 255),
list_setting("%sgrp" % s, "%s Group code" % l,
_5tone_settings.tone_settings[i].group_code,
HEXADECIMAL),
list_setting("%srpt" % s, "%s Repeat code" % l,
_5tone_settings.tone_settings[i].repeat_code,
HEXADECIMAL)))
data_msk_call_list = []
data_dtmf_call_list = []
data_5tone_call_list = []
for i in range(9):
j = i+1
data_msk_call_list.append(
ff_string_setting("ce%d" % i,
"MSK call entry %d" % j,
_msk_settings.phone_book[i].entry,
0, 4,
charset=HEXADECIMAL))
data_dtmf_call_list.append(
dtmf_string_setting("ce%d" % i,
"DTMF call entry %d" % j,
_dtmf_settings.phone_book[i].entry,
_dtmf_settings.phone_book[i].length,
0, 10))
data_5tone_call_list.append(
five_tone_string_setting("ce%d" % i,
"5-Tone call entry %d" % j,
_5tone_settings.phone_book[i].entry)),
data_settings = data_general_settings
data_settings.extend([
settings.RadioSettingGroup("MSK_s", "MSK settings",
*data_msk_settings),
settings.RadioSettingGroup("MSK_c", "MSK call list",
*data_msk_call_list),
settings.RadioSettingGroup("DTMF_s", "DTMF settings",
*data_dtmf_settings),
settings.RadioSettingGroup("DTMF_c", "DTMF call list",
*data_dtmf_call_list),
settings.RadioSettingGroup("5-Tone_s", "5-tone settings",
*data_5tone_settings),
settings.RadioSettingGroup("5-Tone_c", "5-tone call list",
*data_5tone_call_list)
])
# settings related to the various ways the radio can be locked down
locking_settings = [
list_setting("autolock", "Automatic timed keypad lock",
_settings.auto_keylock,
OFF_ON),
list_setting("lockon", "Current status of keypad lock",
_settings.keypad_lock,
INACTIVE_ACTIVE),
list_setting("nokeypad", "Disable keypad",
_settings.allow_keypad,
YES_NO),
list_setting("rxstun", "Disable receiver (rx stun)",
_settings.rx_stun,
NO_YES),
list_setting("txstun", "Disable transmitter (tx stun)",
_settings.tx_stun,
NO_YES),
]
# broadcast FM radio settings
broadcast_settings = [
list_setting("band", "Frequency interval",
_broadcast.receive_range,
BFM_BANDS),
list_setting("stride", "VFO step",
_broadcast.channel_stepping,
BFM_STRIDE),
frequency_setting("vfo", "VFO frequency (MHz)",
_broadcast.vfo_freq)
]
for i in range(10):
broadcast_settings.append(
frequency_setting("bcd%d" % i, "Memory %d frequency" % i,
_broadcast.memory[i].entry))
return settings.RadioSettings(
settings.RadioSettingGroup("model", "Model/Unit information",
*model_unit_settings),
settings.RadioSettingGroup("radio", "Radio/Channel settings",
*radio_channel_settings),
settings.RadioSettingGroup("interface", "Interface",
*interface_settings),
settings.RadioSettingGroup("data", "Data",
*data_settings),
settings.RadioSettingGroup("locking", "Locking",
*locking_settings),
settings.RadioSettingGroup("broadcast",
"Broadcast FM radio settings",
*broadcast_settings)
)
def set_settings(self, s, parent=''):
# The helper classes take care of all settings except these below,
# since it is a single instance of settings having interdependencies,
# i.e., which value that gets written to the memory depends on
# the value of another setting. The value of the ptt id type setting
# decides which of the msk/dtmf/5-tone ptt id strings are
# actually written to memory.
ds = sbyn(s, 'data')
idts = sbyn(ds, 'pttidt').value
idtv = idts.get_value()
cs = sbyn(ds, idtv+'_s')
tss = [sbyn(cs, e).value for e in ['bot', 'eot']]
for ts in tss:
ts.write_mem()
chirp-20261030/chirp/drivers/radioddity_db40g.py 0000644 0001750 0001750 00000032365 15157207431 020321 0 ustar hibby hibby # Copyright 2026 Tony Gies
#
# 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 .
import logging
import struct
from chirp import bandplan_na
from chirp import bitwise
from chirp import chirp_common
from chirp import directory
from chirp import errors
from chirp import memmap
from chirp import util
from chirp.settings import MemSetting, RadioSettingGroup, \
RadioSettingValueInteger, RadioSettingValueList, \
RadioSettings
LOG = logging.getLogger(__name__)
# Serial protocol constants
CMD_ACK = b"\x06"
CMD_PROGRAM = b"\x02PROGRAM"
CMD_EXIT = b"\x62"
IDENT_PREFIX = b"SMP558"
# Memory layout constants
NUM_CHANNELS = 199
CH_START_ADDR = 0x0010
PAGE_STRIDE = 0x10
PAGE_SIZE = 0x20 # 32 bytes per page read
SETTINGS_ADDR = 0xE000
SETTINGS_OFFSET = NUM_CHANNELS * PAGE_SIZE # 0x18E0
MEM_SIZE = (NUM_CHANNELS + 1) * PAGE_SIZE # 6400
POWER_LEVELS = [chirp_common.PowerLevel("Low", watts=5),
chirp_common.PowerLevel("Mid", watts=20),
chirp_common.PowerLevel("High", watts=40)]
# Tone encoding flags (big-endian u16 bitfield):
# bit 15: DCS inverted polarity
# bit 13: DCS mode
# bit 11: set for DCS (not CTCSS)
# bits 0-10: DCS code as int(octal, 8), or full u16 = CTCSS Hz*10
TONE_OFF = 0xFFFF
TONE_DCS_FLAG = 0x2000 # bit 13
TONE_INV_FLAG = 0x8000 # bit 15
TONE_DCS_MASK = 0x07FF # bits 0-10: DCS code value
TOT_OPTIONS = ["Off", "30s", "60s", "90s", "120s", "150s",
"180s", "210s", "240s", "270s", "300s"]
MEM_FORMAT = """
struct channel {
u32 rxfreq;
u32 txfreq;
u16 rxtone;
u16 txtone;
u8 bandwidth;
u8 txpower;
char name[8];
u8 pad[10];
};
struct settings {
u8 busylock;
u8 keytone;
u8 tot;
u8 squelch;
u8 pad[28];
};
#seekto 0x0000;
struct channel memory[%d];
#seekto 0x%04X;
struct settings settings;
"""
# --- Serial protocol ---
def _enter_programming(radio):
"""Three-step handshake to enter programming mode."""
serial = radio.pipe
try:
serial.write(CMD_PROGRAM)
ack = serial.read(1)
except Exception:
raise errors.RadioError(
"Error communicating with radio")
if ack != CMD_ACK:
raise errors.RadioError(
"Radio did not respond to program mode request")
try:
serial.write(b"\x02")
ident = serial.read(8)
except Exception:
raise errors.RadioError(
"Error communicating with radio")
LOG.debug("Ident: %s", util.hexprint(ident))
if not ident.startswith(IDENT_PREFIX):
raise errors.RadioError(
"Unsupported radio (ident %s)" % ident.hex())
try:
serial.write(CMD_ACK)
ack = serial.read(1)
except Exception:
raise errors.RadioError(
"Error communicating with radio")
if ack != CMD_ACK:
raise errors.RadioError(
"Radio refused programming mode")
def _exit_programming(radio):
try:
radio.pipe.write(CMD_EXIT)
except Exception:
raise errors.RadioError(
"Failed to exit programming mode")
def _read_page(radio, addr):
"""Read one 32-byte page. Response echoes R prefix (not W)."""
serial = radio.pipe
cmd = struct.pack(">cHB", b"R", addr, PAGE_SIZE)
LOG.debug("Read page 0x%04X", addr)
try:
serial.write(cmd)
response = serial.read(4 + PAGE_SIZE)
except Exception:
raise errors.RadioError(
"Failed to read from radio at 0x%04X" % addr)
if len(response) != 4 + PAGE_SIZE:
raise errors.RadioError(
"Short read at 0x%04X (%d bytes)" % (
addr, len(response)))
expected = struct.pack(">cHB", b"R", addr, PAGE_SIZE)
if response[:4] != expected:
raise errors.RadioError(
"Bad read response at 0x%04X" % addr)
return response[4:]
def _write_page(radio, addr, data):
"""Write one 32-byte page. Expects ACK."""
serial = radio.pipe
cmd = struct.pack(">cHB", b"W", addr, PAGE_SIZE)
LOG.debug("Write page 0x%04X", addr)
try:
serial.write(cmd + data)
ack = serial.read(1)
except Exception:
raise errors.RadioError(
"Failed to write to radio at 0x%04X" % addr)
if ack != CMD_ACK:
raise errors.RadioError(
"No ACK after write at 0x%04X" % addr)
def do_download(radio):
_enter_programming(radio)
data = b""
status = chirp_common.Status()
status.msg = "Cloning from radio"
status.cur = 0
status.max = NUM_CHANNELS + 1
for i in range(NUM_CHANNELS):
addr = CH_START_ADDR + i * PAGE_STRIDE
data += _read_page(radio, addr)
status.cur = i + 1
radio.status_fn(status)
data += _read_page(radio, SETTINGS_ADDR)
status.cur = status.max
radio.status_fn(status)
_exit_programming(radio)
return memmap.MemoryMapBytes(data)
def do_upload(radio):
_enter_programming(radio)
status = chirp_common.Status()
status.msg = "Uploading to radio"
status.cur = 0
status.max = NUM_CHANNELS + 1
raw = radio.get_mmap().get_packed()
for i in range(NUM_CHANNELS):
addr = CH_START_ADDR + i * PAGE_STRIDE
offset = i * PAGE_SIZE
page = raw[offset:offset + PAGE_SIZE]
_write_page(radio, addr, page)
status.cur = i + 1
radio.status_fn(status)
_write_page(radio, SETTINGS_ADDR,
raw[SETTINGS_OFFSET:SETTINGS_OFFSET + PAGE_SIZE])
status.cur = status.max
radio.status_fn(status)
_exit_programming(radio)
# --- Tone encoding ---
def _decode_tone(value):
"""Decode 16-bit tone to (mode, value, polarity).
Bit layout of the u16 tone value:
bit 15: DCS inverted polarity
bit 13: DCS mode flag
bits 0-10: DCS code (as int(octal, 8))
If DCS flag clear: full u16 is CTCSS Hz * 10
0xFFFF = Off
"""
if value == TONE_OFF:
return '', None, None
if value & TONE_DCS_FLAG:
code_dec = value & TONE_DCS_MASK
code_oct = int(oct(code_dec)[2:])
pol = 'R' if value & TONE_INV_FLAG else 'N'
return 'DTCS', code_oct, pol
return 'Tone', value / 10.0, None
def _encode_tone(mode, value, polarity):
"""Encode tone to 16-bit value for radio."""
if mode == '':
return TONE_OFF
elif mode == 'Tone':
return int(value * 10)
elif mode == 'DTCS':
code_dec = int(str(value), 8)
result = TONE_DCS_FLAG | 0x0800 | code_dec
if polarity == 'R':
result |= TONE_INV_FLAG
return result
raise errors.RadioError(
"Unsupported tone mode: %s" % mode)
# --- Driver class ---
@directory.register
class RadioddityDB40G(chirp_common.CloneModeRadio):
"""Radioddity DB40-G"""
VENDOR = "Radioddity"
MODEL = "DB40-G"
BAUD_RATE = 19200
_memsize = MEM_SIZE
def get_features(self):
rf = chirp_common.RadioFeatures()
rf.has_settings = True
rf.has_bank = False
rf.has_tuning_step = False
rf.has_rx_dtcs = True
rf.has_ctone = True
rf.has_cross = True
rf.can_odd_split = True
rf.valid_modes = ["FM", "NFM"]
rf.valid_tmodes = ["", "Tone", "TSQL", "DTCS", "Cross"]
rf.valid_cross_modes = [
"Tone->Tone",
"DTCS->",
"->DTCS",
"Tone->DTCS",
"DTCS->Tone",
"->Tone",
"DTCS->DTCS"]
rf.valid_duplexes = ["", "-", "+", "split", "off"]
rf.valid_skips = []
rf.valid_characters = chirp_common.CHARSET_ASCII
rf.valid_name_length = 8
rf.valid_power_levels = POWER_LEVELS
rf.valid_bands = [(400000000, 480000000)]
rf.memory_bounds = (1, NUM_CHANNELS)
rf.valid_tuning_steps = [5.0, 6.25, 12.5, 25.0]
return rf
def process_mmap(self):
self._memobj = bitwise.parse(
MEM_FORMAT % (NUM_CHANNELS, SETTINGS_OFFSET),
self._mmap)
def sync_in(self):
self._mmap = do_download(self)
self.process_mmap()
def sync_out(self):
do_upload(self)
def get_raw_memory(self, number):
return repr(self._memobj.memory[number - 1])
def get_memory(self, number):
_mem = self._memobj.memory[number - 1]
mem = chirp_common.Memory()
mem.number = number
rxfreq = int(_mem.rxfreq)
if rxfreq == 0 or rxfreq == 0xFFFFFFFF:
mem.empty = True
return mem
mem.freq = rxfreq
txfreq = int(_mem.txfreq)
if txfreq == 0:
mem.duplex = "off"
mem.offset = 0
elif txfreq == rxfreq:
mem.duplex = ""
mem.offset = 0
elif txfreq > rxfreq:
mem.duplex = "+"
mem.offset = txfreq - rxfreq
else:
mem.duplex = "-"
mem.offset = rxfreq - txfreq
mem.mode = "FM" if _mem.bandwidth else "NFM"
pwr = int(_mem.txpower)
if 0 <= pwr < len(POWER_LEVELS):
mem.power = POWER_LEVELS[pwr]
else:
mem.power = POWER_LEVELS[0]
mem.name = str(_mem.name).rstrip("\x00 ")
txtone = _decode_tone(int(_mem.txtone))
rxtone = _decode_tone(int(_mem.rxtone))
chirp_common.split_tone_decode(mem, txtone, rxtone)
return mem
def validate_memory(self, mem):
msgs = super().validate_memory(mem)
if mem.empty:
return msgs
if mem.duplex == "split":
tx_freq = mem.offset
elif mem.duplex == "+":
tx_freq = mem.freq + mem.offset
elif mem.duplex == "-":
tx_freq = mem.freq - mem.offset
elif mem.duplex == "off":
tx_freq = 0
else:
tx_freq = mem.freq
if tx_freq:
rpt = [f + 5000000
for f in bandplan_na.GMRS_HIRPT]
all_tx = (list(bandplan_na.ALL_GMRS_FREQS)
+ rpt)
if tx_freq not in all_tx:
msgs.append(
chirp_common.ValidationWarning(
"TX frequency %s is not a "
"standard GMRS frequency"
% chirp_common.format_freq(
tx_freq)))
if tx_freq in bandplan_na.GMRS_HHONLY:
if mem.power and \
mem.power != POWER_LEVELS[0]:
msgs.append(
chirp_common.ValidationWarning(
"GMRS 467 MHz channels "
"require low power"))
if mem.mode != "NFM":
msgs.append(
chirp_common.ValidationWarning(
"GMRS 467 MHz channels "
"require narrowband"))
return msgs
def set_memory(self, mem):
_mem = self._memobj.memory[mem.number - 1]
if mem.empty:
_mem.set_raw(b"\x00" * PAGE_SIZE)
return
_mem.rxfreq = mem.freq
if mem.duplex == "off":
_mem.txfreq = 0
elif mem.duplex == "+":
_mem.txfreq = mem.freq + mem.offset
elif mem.duplex == "-":
_mem.txfreq = mem.freq - mem.offset
elif mem.duplex == "split":
_mem.txfreq = mem.offset
else:
_mem.txfreq = mem.freq
_mem.bandwidth = 1 if mem.mode == "FM" else 0
if mem.power:
_mem.txpower = POWER_LEVELS.index(mem.power)
else:
_mem.txpower = 0
_mem.name = mem.name.ljust(8, "\x00")[:8]
((txmode, txval, txpol),
(rxmode, rxval, rxpol)) = \
chirp_common.split_tone_encode(mem)
_mem.txtone = _encode_tone(txmode, txval, txpol)
_mem.rxtone = _encode_tone(rxmode, rxval, rxpol)
def get_settings(self):
_s = self._memobj.settings
basic = RadioSettingGroup("basic", "Basic Settings")
rs = RadioSettingValueList(
["Off", "On"], current_index=int(_s.busylock))
basic.append(MemSetting(
"settings.busylock", "Busy Lock", rs))
rs = RadioSettingValueList(
["Off", "On"], current_index=int(_s.keytone))
basic.append(MemSetting(
"settings.keytone", "Key Tone", rs))
tot_val = int(_s.tot)
if tot_val >= len(TOT_OPTIONS):
tot_val = 0
rs = RadioSettingValueList(
TOT_OPTIONS, current_index=tot_val)
basic.append(MemSetting(
"settings.tot", "TX Timeout", rs))
rs = RadioSettingValueInteger(
0, 9, int(_s.squelch))
basic.append(MemSetting(
"settings.squelch", "Squelch Level", rs))
return RadioSettings(basic)
def set_settings(self, settings):
settings.apply_to(self._memobj)
chirp-20261030/chirp/drivers/radioddity_gm30.py 0000644 0001750 0001750 00000113501 15210511266 020151 0 ustar hibby hibby # Copyright 2025 Mike Iacovacci
#
# 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 .
import struct
import logging
from chirp import chirp_common, directory, memmap
from chirp import bitwise, errors
from chirp.settings import RadioSetting, RadioSettingGroup, \
RadioSettingValueInteger, RadioSettingValueList, \
RadioSettingValueBoolean, RadioSettingValueString, RadioSettings
LOG = logging.getLogger(__name__)
MEM_FORMAT = """
struct{
u8 unknown02[256];
} unknown02[16];
struct{
u8 bootscrmode;
u8 bsmodepad[15];
u8 bootscreen1[10];
u8 bs1pad[6];
u8 bootscreen2[10];
u8 bs2pad[6];
u8 unused[16];
u8 timeout;
u8 squelch;
u8 vox_level;
u8 batt_save:4,
unk_bits:2,
work_mode:1,
voice_alert:1;
u8 backlight;
u8 beep_tone:1,
auto_key_lock:1,
unk_bit_2:1,
ctcss_revert:1,
scan_type:2,
side_tone:2;
u8 unk_bit_3:1,
standby:1,
roger:1,
alarm_mode:2,
alarm_sound:1,
fm_radio:1,
unk_bit_4:1;
u8 tail_revert;
u8 tail_delay;
u8 tbst;
u8 unk_sett[6];
u8 unk_bits_5:6,
b_ch_disp:1,
a_ch_disp:1;
u8 unk_sett_2[24];
u8 passw_w_ena;
u8 passw_r_ena;
u8 passw_w_val[8];
u8 passw_r_val[8];
u8 unk_sett_3[5];
} settings;
struct{
u8 unusedsettings[32];
} unusedsettings[124];
struct{
u8 entry[5];
} dtmf_list[15];
struct{
u8 dtmf_l_pad[5];
u8 radio_id[5];
u8 unk_dtmf[11];
u8 unk_dtmf_bits:6,
press_send:1,
release_send:1;
u8 delay_time;
u8 digit_dur;
u8 inter_dur;
u8 unk_dtmf_2[28];
} dtmf;
struct{
u8 unkdtmf[128];
} unuseddtmf[31];
struct{
u8 unk_mem[16];
} tuning;
struct {
lbcd rx_freq[4];
lbcd tx_freq[4];
u8 mem_changed;
lbcd rx_tone[2];
lbcd tx_tone[2];
u8 unk_mem2:1,
busy_lock:1,
ptt_id:2,
unk_mem3:1,
mode:1,
power:1,
unk_mem4:1;
u8 signal:4,
unk_mem5:2,
freq_hop:1,
scan:1;
u8 step;
} vfomemory[2];
struct {
lbcd rx_freq[4];
lbcd tx_freq[4];
u8 mem_changed;
lbcd rx_tone[2];
lbcd tx_tone[2];
u8 unk_mem2:1,
busy_lock:1,
ptt_id:2,
unk_mem3:1,
mode:1,
power:1,
unk_mem4:1;
u8 signal:4,
unk_mem5:2,
freq_hop:1,
scan:1;
u8 step;
} memory[250];
struct{
u8 unused_memch[16];
} unused_mem[3];
struct{
u8 unknown17[256];
} unknown17[16];
struct{
u8 unknown18[256];
} unknown18[16];
struct{
u8 unknown19[256];
} unknown19[16];
struct{
char name[6];
u8 pad[5];
} memnames[250];
struct{
u8 unknames[64];
} unknownnames [21];
struct{
u8 unknown25[256];
} unknown25[16];
struct{
u8 unknown26[256];
} unknown26[16];
"""
# radio defines these types
# query each type to retrieve
# the mem location it is stored
TYPE_MAP = [
(0x02, "unknown02"),
(0x04, "settings"),
(0x06, "dtmf"),
(0x16, "memories"),
(0x17, "unknown17"),
(0x18, "unknown18"),
(0x19, "unknown19"),
(0x24, "chan_names"),
(0x25, "unknown25"),
(0x26, "unknown26")
]
# we only write these data types
# back to the radio. The 3rd tuple
# being the offset in _memobj
WRITE_MAP = [
(0x04, "settings", 0x1000),
(0x06, "dtmf", 0x2000),
(0x16, "memories", 0x3000),
(0x24, "chan_names", 0x7000)
]
def do_ack_ack(serial):
serial.write(b'\x06')
ack = serial.read(1)
if ack != b'\x06':
err = f"Error expected 06 ack got {ack}"
LOG.debug(err)
raise errors.RadioError(err)
def raw_send(serial, data, exlen):
serial.write(data)
return serial.read(exlen)
def do_read_cmd(serial, cmd, exlen):
echo_ack = len(cmd) + 1 # ack 0x57 + echo of cmd
resp = raw_send(serial, b'\x52' + cmd, exlen + echo_ack)
if resp[0] != 0x57:
raise errors.RadioError(f"Read CMD resp failed got {resp}")
if len(resp[echo_ack:]) != exlen:
raise errors.RadioError(f"Read CMD resp expect len={exlen} got {resp}")
do_ack_ack(serial)
return resp[echo_ack:]
def enter_prog(serial):
resp = raw_send(serial, b'PSEARCH', 8)
if len(resp) != 8:
err = ("Enter programming failed" +
f" Resp : {resp}")
raise errors.RadioError(err)
if resp[0] != 0x06:
raise errors.RadioError("Enter programming: " +
f"Radio Bad Ack: {resp}")
return resp[1:]
def exit_prog(serial):
try:
serial.write(b'\x06')
serial.write(b'\x06')
serial.write(b'\x00')
serial.close()
LOG.debug("Exited programming")
except Exception as e:
raise errors.RadioError(f"Error exiting programming {e}")
def check_ident(data):
if data == b'P13GMRS':
LOG.info(f"Radio is: {data}")
else:
err = (f"Ident returned unknown Radio: {data}")
LOG.debug(err)
raise errors.RadioError(err)
def do_sysinfo(serial):
resp = raw_send(serial, b'PASSSTA', 3)
if resp != b'\x50\x00\x00':
raise errors.RadioError(f"Expected 0x500000 got {resp}")
resp = raw_send(serial, b'SYSINFO', 1)
if resp != b'\x06':
raise errors.RadioError(f"ACK expected got {resp}")
LOG.debug(f"SYSINFO: {resp}")
def do_readconfig(serial):
# cmds start with 56, and expect 06 ack after recv ack
for addr, _len in [(0x00000a0d, 13), (0x00100a0d, 13),
(0x00200a0d, 13), (0x0000000a, 11)]:
cmd = struct.pack('>BL', 0x56, addr)
resp = raw_send(serial, cmd, _len)
if len(resp) != _len:
raise errors.RadioError(f"Expected (_len) Bytes got {resp}")
do_ack_ack(serial)
return True
def do_prog2(serial):
cmd = struct.pack('>LB', 0xffffffff, 0x0c)
serial.write(cmd) # no resp expected
resp = raw_send(serial, b'P13GMRS', 1)
if resp != b'\x06':
raise errors.RadioError(f"Error expected 06 ack got {resp}")
resp = raw_send(serial, b'\x02', 8)
if len(resp) != 8:
raise errors.RadioError(f"Error expected len 8 got {resp}")
do_ack_ack(serial)
def do_read_tlmap(serial):
""" The radio has defined types of config data.
Before reading or writing, we ask the radio where
The data type is stored in memory. The radio
moves memory locations possibly for durability
Build ephemeral map of type to location """
tl_map = {m[0]: 0 for m in TYPE_MAP}
for i in range(1, 16):
addr = (i << 4 | 0x0f)
# ask the radio where each data type is in mem
cmd = struct.pack('>4B', 0xff, addr, 0x00, 0x01)
resp = do_read_cmd(serial, cmd, 1)
# mem location is for this prog session only
r_int = struct.unpack('>B', resp)[0]
if r_int in tl_map:
tl_map[r_int] = (i << 4)
return tl_map
def do_read_ranges(serial, loc, radio, status):
# read each data/config type from the (loc)ation
# obtained by tlmap query. each loc has 4 (pre)fix
# block numbers. Each block is 64 bytes
data = b''
for i in range(16):
for pre in range(0x0, 0xc1, 0x40):
addr = struct.pack('>BBBB', pre, loc + i, 0x00, 0x40)
data += do_read_cmd(serial, addr, 0x40)
status.cur += 0x40
radio.status_fn(status)
return data
def do_upload_block(serial, loc, offset, radio, status):
# write each config type to loc retrieved from tlmap
# 4 defined (pre)fixes/blocks per (loc)ation
# pre = 0x00 , 0x40 , 0x80, 0xc0
# full command example [57][80][4d]0040
# 57 = write , 80 prefix/block , 4d location to write
_mem = radio._memobj.get_raw()
for i in range(16):
for pre in range(0x0, 0xc1, 0x40):
x = offset + (i * 0x100) + pre
block = _mem[x:x + 0x40]
addr = struct.pack('>BBBBB', 0x57, pre, loc + i, 0x00, 0x40)
data = addr + block
ack = raw_send(serial, data, 1)
if ack != b'\x06':
err = f"Bad ack on write expect 0x06 got {ack}"
LOG.debug(err)
raise errors.RadioError(err)
status.cur += len(block)
radio.status_fn(status)
def do_download(radio):
data = b''
try:
status = chirp_common.Status()
serial = radio.pipe
serial.flush()
ident = enter_prog(serial)
check_ident(ident)
do_sysinfo(serial)
do_readconfig(serial)
do_prog2(serial)
tl_map = do_read_tlmap(serial)
status.max = len(tl_map) * 0x1000
status.msg = "Downloading..."
for t, loc in tl_map.items():
if loc == 0:
raise errors.RadioError(f"TL Map failed {t} {loc}")
data += do_read_ranges(serial, loc, radio, status)
except Exception as e:
raise errors.RadioError(f"Error during download {e}")
finally:
exit_prog(serial)
return memmap.MemoryMapBytes(data)
def do_upload(radio):
try:
status = chirp_common.Status()
status.max = len(WRITE_MAP) * 0x1000
serial = radio.pipe
serial.flush()
ident = enter_prog(serial)
check_ident(ident)
do_sysinfo(serial)
do_readconfig(serial)
do_prog2(serial)
tl_map = do_read_tlmap(serial)
for wt, label, offset in WRITE_MAP:
status.msg = f"Uploading: {label}..."
loc = tl_map[wt]
do_upload_block(serial, loc, offset, radio, status)
except errors.RadioError:
raise
except Exception as e:
raise errors.RadioError(f"Error during upload {e}")
finally:
exit_prog(serial)
@directory.register
class RadioddityGM30(chirp_common.CloneModeRadio):
"""Radioddity GM-30"""
VENDOR = "Radioddity"
MODEL = "GM-30"
BAUD_RATE = 57600
POWER_LEVELS = [chirp_common.PowerLevel("Low", watts=0.50),
chirp_common.PowerLevel("High", watts=3.00)]
VALID_MODES = ["NFM", "FM"]
_range = [(136000000, 174000000), (400000000, 470000000)]
GMRS_RPTR = [462550000, 462575000, 462600000, 462625000, 462650000,
462675000, 462700000, 462725000]
VALID_TONES = chirp_common.TONES
VALID_DCS = [i for i in range(
0, 778) if '9' not in str(i) and '8' not in str(i)]
DCS_N = 0x80
DCS_R = 0x40
VALID_CHARSET = chirp_common.CHARSET_ASCII
VALID_DTMF = [str(i) for i in range(0, 10)] + \
["A", "B", "C", "D", "*", "#"]
ASCII_NUM = [str(i) for i in range(10)] + [' ']
VALID_STEPS = [2.5, 5.0, 6.25, 10, 12.5, 20, 25, 50]
def get_features(self):
rf = chirp_common.RadioFeatures()
rf.has_settings = True
rf.has_bank = False
rf.has_tuning_step = False
rf.valid_tuning_steps = self.VALID_STEPS
rf.has_name = True
rf.valid_characters = self.VALID_CHARSET
rf.valid_name_length = 6
rf.has_offset = True
rf.has_mode = True
rf.has_dtcs = True
rf.has_rx_dtcs = True
rf.has_dtcs_polarity = True
rf.has_ctone = True
rf.has_cross = True
rf.can_odd_split = False
rf.can_delete = True
rf.valid_modes = self.VALID_MODES
rf.valid_duplexes = ["", "-", "+", "off"]
rf.valid_tmodes = ["", "Tone", "TSQL", "DTCS", "Cross"]
rf.valid_cross_modes = [
"Tone->DTCS",
"DTCS->Tone",
"->Tone",
"Tone->Tone",
"->DTCS",
"DTCS->",
"DTCS->DTCS"]
rf.valid_power_levels = self.POWER_LEVELS
rf.valid_skips = ["", "S"]
rf.valid_bands = self._range
rf.memory_bounds = (1, 250)
return rf
def process_mmap(self):
self._memobj = bitwise.parse(MEM_FORMAT, self._mmap)
def sync_in(self):
try:
data = do_download(self)
except errors.RadioError:
raise
except Exception as e:
err = f'Error during download {e}'
LOG.error(err)
raise errors.RadioError(err)
self._mmap = data
self.process_mmap()
def sync_out(self):
try:
do_upload(self)
except Exception as e:
err = f'Error during upload {e}'
LOG.error(err)
raise errors.RadioError(err)
def val_or_def(self, memidx, _list):
try:
return _list[memidx]
except IndexError:
return _list[0]
def idx_or_def(self, memitem, _list):
try:
return _list.index(memitem)
except ValueError:
return 0
def get_str_name(self, _name):
return ''.join(filter
(lambda x: x in self.VALID_CHARSET,
str(_name)))
def get_raw_memory(self, number):
return repr(self._memobj.memory[number - 1]) + \
repr(self._memobj.memnames[number - 1])
def get_memory(self, number):
mem = chirp_common.Memory()
mem.number = number
_mem = self._memobj.memory[number-1]
_name = self._memobj.memnames[number-1]["name"]
str_name = self.get_str_name(_name)
mem.name = str_name.rstrip()
if _mem.rx_freq.get_raw() == b'\xff\xff\xff\xff':
mem.empty = True
return mem
mem.freq = int(_mem.rx_freq) * 10
if _mem.tx_freq.get_raw() == b'\xff\xff\xff\xff':
mem.duplex = 'off'
elif int(_mem.tx_freq) - int(_mem.rx_freq) > 0:
# '+' duplex
mem.duplex = '+'
mem.offset = (int(_mem.tx_freq) - int(_mem.rx_freq)) * 10
elif int(_mem.tx_freq) - int(_mem.rx_freq) < 0:
# '-' duplex
mem.duplex = '-'
mem.offset = (int(_mem.rx_freq) - int(_mem.tx_freq)) * 10
mem.mode = self.val_or_def(_mem.mode, self.VALID_MODES)
mem.power = self.val_or_def(_mem.power, self.POWER_LEVELS)
mem.skip = "" if _mem.scan else "S"
if 1 <= mem.number <= 7 or 15 <= mem.number <= 22:
mem.duplex = ""
mem.immutable = ['duplex', 'offset', 'empty']
elif 8 <= mem.number <= 14:
mem.duplex = "off"
mem.immutable += ['duplex', 'offset', 'empty', 'mode', 'power']
elif 23 <= mem.number <= 54:
mem.offset = 5 * 1000000
mem.duplex = '+'
mem.immutable = ['duplex', 'offset', 'empty']
else:
mem.offset = 0
mem.duplex = "off"
mem.immutable += ['offset', 'duplex']
if 0 < mem.number <= 30:
mem.immutable = mem.immutable + ['freq']
txtone = self.get_tone(_mem.tx_tone)
rxtone = self.get_tone(_mem.rx_tone)
chirp_common.split_tone_decode(mem, txtone, rxtone)
mem.extra = RadioSettingGroup("Extra", "extra")
rs = RadioSettingValueBoolean(_mem.busy_lock)
rset = RadioSetting("busy_lock", "Busy Lock", rs)
mem.extra.append(rset)
rs = RadioSettingValueBoolean(_mem.freq_hop)
rset = RadioSetting("freq_hop", "Freq. Hop", rs)
mem.extra.append(rset)
_current = _mem.signal if _mem.signal else 1
rs = RadioSettingValueInteger(1, 15, current=_current)
rset = RadioSetting("signal", "DTMF ID", rs)
mem.extra.append(rset)
options = ['Off', 'BOT', 'EOT', 'BOTH']
rs = RadioSettingValueList(options, current_index=_mem.ptt_id)
rset = RadioSetting("ptt_id", "PTT ID", rs)
mem.extra.append(rset)
return mem
def validate_memory(self, mem):
if 31 <= mem.number <= 54:
if mem.freq not in self.GMRS_RPTR:
return [chirp_common.ValidationError(
'Only GMRS repeater freq. permitted'
' on channels 31 - 54')]
return super().validate_memory(mem)
def set_memory(self, mem):
number = mem.number
_mem = self._memobj.memory[number-1]
_name = self._memobj.memnames[number-1]
newname = [str(c) for c in mem.name]
_name.name = "".join(newname).ljust(6, '\x00')
if mem.empty:
_mem.set_raw(b'\xff' * 13 + b'\x06\x11\x00')
return
_mem.rx_freq = mem.freq // 10
if mem.duplex == "+":
_mem.tx_freq = (mem.freq + mem.offset) // 10
elif mem.duplex == "-":
_mem.tx_freq = (mem.freq - mem.offset) // 10
elif mem.duplex == "":
_mem.tx_freq = _mem.rx_freq
elif mem.duplex == "off":
_mem.tx_freq.fill_raw(b'\xff')
_mem.mode = self.idx_or_def(mem.mode, self.VALID_MODES)
_mem.power = self.idx_or_def(mem.power, self.POWER_LEVELS)
_mem.scan = False if mem.skip == "S" else True
((txmode, txval, txpol),
(rxmode, rxval, rxpol)) = chirp_common.split_tone_encode(mem)
self.set_tone(_mem.tx_tone, txmode, txval, txpol)
self.set_tone(_mem.rx_tone, rxmode, rxval, rxpol)
# extra settings
for setting in mem.extra:
setattr(_mem, setting.get_name(), setting.value)
def get_tone(self, _memval):
_memval[1].ignore_bits(self.DCS_N | self.DCS_R)
dcsn = _memval[1].get_bits(self.DCS_N)
dcsr = _memval[1].get_bits(self.DCS_R)
rb = _memval[1].get_raw()
if rb == b'\xff' or rb == b'\x00':
return "", 0, None
elif dcsn:
pol = "R" if dcsr else "N"
return "DTCS", int(_memval), pol
else:
return "Tone", int(_memval) / 10, None
def set_tone(self, _memval, mode, val, pol):
# sets tones in _mem from ui edit
_memval[1].ignore_bits(self.DCS_N | self.DCS_R)
if mode == "":
_memval.set_raw(b'\xff\xff')
if mode == "Tone":
_memval[1].clr_bits(self.DCS_N | self.DCS_R)
_memval.set_value(val * 10)
if mode == "DTCS":
_memval[1].set_bits(self.DCS_N)
if pol == 'R':
_memval[1].set_bits(self.DCS_R)
else:
_memval[1].clr_bits(self.DCS_R)
_memval.set_value(val)
def get_settings(self):
_settings = self._memobj.settings
_dtmf = self._memobj.dtmf
_dtmf_list = self._memobj.dtmf_list
gsettings = RadioSettingGroup("gsettings", "General Settings")
group = RadioSettings(gsettings)
_options = ["Logo", "Message", "Voltage"]
rs = RadioSettingValueList(_options,
current_index=_settings.bootscrmode)
rset = RadioSetting("settings.bootscrmode", "Boot Screen Mode", rs)
gsettings.append(rset)
_current = "".join(chr(i) for i in _settings.bootscreen1
if chr(i) in self.VALID_CHARSET)
rs = RadioSettingValueString(minlength=0, maxlength=10,
current=_current,
charset=self.VALID_CHARSET,
mem_pad_char=' ')
rset = RadioSetting("settings.bootscreen1",
"Boot Screen 1", rs)
gsettings.append(rset)
_current = "".join(chr(i) for i in _settings.bootscreen2
if chr(i) in self.VALID_CHARSET)
rs = RadioSettingValueString(minlength=0, maxlength=10,
current=_current,
charset=self.VALID_CHARSET,
mem_pad_char=' ')
rset = RadioSetting("settings.bootscreen2",
"Boot Screen 2", rs)
gsettings.append(rset)
_options = [str(i) for i in range(0, 601, 15)]
rs = RadioSettingValueList(_options,
current_index=_settings.timeout)
rset = RadioSetting("settings.timeout", "Timeout (s)", rs)
gsettings.append(rset)
rs = RadioSettingValueInteger(minval=0, maxval=9,
current=_settings.squelch, step=1)
rset = RadioSetting("settings.squelch", "Squelch Level", rs)
gsettings.append(rset)
rs = RadioSettingValueInteger(minval=0, maxval=9,
current=_settings.vox_level, step=1)
rset = RadioSetting("settings.vox_level", "Vox Level", rs)
gsettings.append(rset)
rs = RadioSettingValueBoolean(
current=_settings.voice_alert, mem_vals=(0, 1))
rset = RadioSetting("settings.voice_alert", "Voice Alert", rs)
gsettings.append(rset)
_options = ["Freq. Mode", "Ch. Mode"]
rs = RadioSettingValueList(_options,
current_index=_settings.work_mode)
rset = RadioSetting("settings.work_mode", "Display Mode", rs)
gsettings.append(rset)
_options = ["None", "1:1", "1:2", "1:3", "1:4"]
rs = RadioSettingValueList(_options,
current_index=_settings.batt_save)
rset = RadioSetting("settings.batt_save", "Battery Save Mode", rs)
gsettings.append(rset)
_options = ["Bright", "1", "2", "3", "4", "5",
"6", "7", "8", "9", "10"]
rs = RadioSettingValueList(_options,
current_index=_settings.backlight)
rset = RadioSetting("settings.backlight", "Backlight", rs)
gsettings.append(rset)
rs = RadioSettingValueBoolean(
current=_settings.auto_key_lock, mem_vals=(0, 1))
rset = RadioSetting("settings.auto_key_lock", "Auto Key Lock", rs)
gsettings.append(rset)
_options = ["Off", "DT-ST", "ANI-ST", "DT+ANI"]
rs = RadioSettingValueList(_options,
current_index=_settings.side_tone)
rset = RadioSetting("settings.side_tone", "DTMF Side Tone", rs)
gsettings.append(rset)
_options = ["Time", "Carrier", "Search"]
rs = RadioSettingValueList(_options,
current_index=_settings.scan_type)
rset = RadioSetting("settings.scan_type", "Scan Type", rs)
gsettings.append(rset)
rs = RadioSettingValueBoolean(
current=_settings.ctcss_revert, mem_vals=(0, 1))
rset = RadioSetting("settings.ctcss_revert", "CTCSS Tail Revert", rs)
gsettings.append(rset)
rs = RadioSettingValueBoolean(
current=_settings.beep_tone, mem_vals=(0, 1))
rset = RadioSetting("settings.beep_tone", "Beep Tone", rs)
gsettings.append(rset)
_options = ["On Site", "Send Sound", "Send Code"]
rs = RadioSettingValueList(_options,
current_index=_settings.alarm_mode)
rset = RadioSetting("settings.alarm_mode", "Alarm Mode", rs)
gsettings.append(rset)
rs = RadioSettingValueBoolean(
current=_settings.fm_radio, mem_vals=(0, 1))
rset = RadioSetting("settings.fm_radio", "FM Radio", rs)
gsettings.append(rset)
rs = RadioSettingValueBoolean(
current=_settings.roger, mem_vals=(0, 1))
rset = RadioSetting("settings.roger", "Roger Beep", rs)
gsettings.append(rset)
rs = RadioSettingValueBoolean(
current=_settings.standby, mem_vals=(0, 1))
rset = RadioSetting("settings.standby", "Dual Standby", rs)
gsettings.append(rset)
_options = [str(i) for i in range(0, 1001, 100)]
rs = RadioSettingValueList(_options,
current_index=_settings.tail_revert)
rset = RadioSetting("settings.tail_revert",
"Repeater Tail Revert (ms)", rs)
gsettings.append(rset)
# same options as tail_rvt
rs = RadioSettingValueList(_options,
current_index=_settings.tail_delay)
rset = RadioSetting("settings.tail_delay",
"Repeater Tail Delay (ms)", rs)
gsettings.append(rset)
_options = ["1000", "1450", "1750", "2100"]
rs = RadioSettingValueList(_options,
current_index=_settings.tbst)
rset = RadioSetting("settings.tbst", "Tone Burst", rs)
gsettings.append(rset)
_options = ["Name + Number", "Freq. + Number"]
rs = RadioSettingValueList(_options,
current_index=_settings.a_ch_disp)
rset = RadioSetting("settings.a_ch_disp", "A Channel Display Type", rs)
gsettings.append(rset)
# same options as a_chan_disp
rs = RadioSettingValueList(_options,
current_index=_settings.b_ch_disp)
rset = RadioSetting("settings.b_ch_disp", "B Channel Display Type", rs)
gsettings.append(rset)
# DTMF Menu
dtmf = RadioSettingGroup("dtmf", "DTMF")
group.append(dtmf)
def _dtmf_decode(setting, pad_len):
_map = list(range(10)) + ['A', 'B', 'C', 'D', '*', '#']
s = ""
for i in setting:
_i = int(i)
if _i < len(_map):
s += str(_map[_i])
return s.ljust(pad_len)
_current = _dtmf_decode(_dtmf.radio_id, 5)
rs = RadioSettingValueString(
minlength=5, maxlength=5, current=_current)
rset = RadioSetting("dtmf.radio_id", "Radio ID", rs)
dtmf.append(rset)
rs = RadioSettingValueBoolean(
current=_dtmf.press_send, mem_vals=(0, 1))
rset = RadioSetting("dtmf.press_send", "PTT Press Send", rs)
dtmf.append(rset)
rs = RadioSettingValueBoolean(
current=_dtmf.release_send, mem_vals=(0, 1))
rset = RadioSetting("dtmf.release_send", "PTT Release Send", rs)
dtmf.append(rset)
_options = [str(i) for i in range(100, 1010, 50)]
rs = RadioSettingValueList(_options,
current_index=_dtmf.delay_time)
rset = RadioSetting("dtmf.delay_time", "Delay Time (ms)", rs)
dtmf.append(rset)
_options = [str(i) for i in range(80, 2010, 10)]
rs = RadioSettingValueList(_options,
current_index=_dtmf.digit_dur)
rset = RadioSetting("dtmf.digit_dur", "Digit Duration (ms)", rs)
dtmf.append(rset)
# uses same options as digit_dur
rs = RadioSettingValueList(_options,
current_index=_dtmf.inter_dur)
rset = RadioSetting(
"dtmf.inter_dur", "Digit Interval Duration (ms)", rs)
dtmf.append(rset)
# DTMF Entries List
dtmflist = RadioSettingGroup("dtmflist", "DTMF List")
group.append(dtmflist)
for i in range(0, 15): # Entries # 1-15
rs = RadioSettingValueString(
minlength=0, maxlength=5,
current=_dtmf_decode(_dtmf_list[i].entry, 5),
autopad=False,
charset=self.VALID_DTMF + [' '])
rset = RadioSetting(f"dtmf_list[{i}].entry", f"Entry {i+1}", rs)
dtmflist.append(rset)
# Password Menu
pro = RadioSettingGroup("protect", "Protect")
group.append(pro)
def _ascii_num_filter(setting):
s = ""
for i in setting:
if chr(i) in self.ASCII_NUM:
s += str(chr(i))
return s
rs = RadioSettingValueBoolean(
current=_settings.passw_w_ena, mem_vals=(0, 1))
rs.set_mutable(False)
rset = RadioSetting("settings.passw_w_ena", "Write Protect", rs)
pro.append(rset)
_current = _ascii_num_filter(_settings.passw_w_val)
rs = RadioSettingValueString(
minlength=0, maxlength=8, current=_current,
charset=self.ASCII_NUM)
rs.set_mutable(False)
rset = RadioSetting("settings.passw_w_val", "Write Password", rs)
pro.append(rset)
rs = RadioSettingValueBoolean(
current=_settings.passw_r_ena, mem_vals=(0, 1))
rs.set_mutable(False)
rset = RadioSetting("settings.passw_r_ena", "Read Protect", rs)
pro.append(rset)
_current = _ascii_num_filter(_settings.passw_r_val)
rs = RadioSettingValueString(
minlength=0, maxlength=8, current=_current,
charset=self.ASCII_NUM)
rs.set_mutable(False)
rset = RadioSetting("settings.passw_r_val", "Read Password", rs)
pro.append(rset)
return group
def _ff_pad__mem(self, obj, setting, element, charset, allow_space=False):
""" set_settings helper for 0xff padded elements
optional remove space chars
"""
_charset = [c for c in list(charset)]
if not allow_space:
_charset = [c for c in list(charset) if c != ' ']
_val = [0xff] * len(obj[setting])
_i = 0 # offset for invalid chars
for i in range(len(obj[setting])):
if element.value[i] in _charset:
_val[i - _i] = ord(element.value[i])
else:
_i += 1
setattr(obj, setting, _val)
def _dtmf_set__mem(self, obj, setting, element):
_dtmf_map = [str(i) for i in range(10)]
_dtmf_map += ['A', 'B', 'C', 'D', '*', '#']
_val = [0xff] * len(obj[setting])
_os = 0 # offset _mem setting idx for pad chars
for i in range(len(element.value)):
if element.value[i] in _dtmf_map:
_val[i - _os] = _dtmf_map.index(element.value[i])
else:
_os += 1
setattr(obj, setting, _val)
def set_settings(self, settings):
for element in settings:
if not isinstance(element, RadioSetting):
self.set_settings(element)
continue
else:
try:
if "." in element.get_name():
toks = element.get_name().split(".")
obj = self._memobj
for tok in toks[:-1]:
if '[' in tok:
t, i = tok.split("[")
i = int(i[:-1])
obj = getattr(obj, t)[i]
else:
obj = getattr(obj, tok)
setting = toks[-1]
else:
obj = self._memobj.settings
setting = element.get_name()
if element.has_apply_callback():
LOG.debug("applying callback")
element.run_apply_callback()
if element.value.get_mutable():
if setting in ['passw_w_val', 'passw_r_val']:
self._ff_pad__mem(
obj, setting, element, self.ASCII_NUM)
elif setting in ['bootscreen1', 'bootscreen2']:
self._ff_pad__mem(
obj, setting, element, self.VALID_CHARSET,
allow_space=True)
elif setting == 'entry':
self._dtmf_set__mem(
obj, setting, element)
else:
setattr(obj, setting, element.value)
except Exception:
LOG.debug(element.get_name())
raise
@directory.register
class BaofengGM15Pro(RadioddityGM30):
"""Baofeng GM-15Pro"""
VENDOR = "Baofeng"
MODEL = "GM-15Pro"
_range = [(136000000, 174000000), (400000000, 512000000)]
@directory.register
class RadioddityMU5(RadioddityGM30):
"""Radioddity MU-5 (MURS)
Identical to Radioddity GM-30 except for the following:
MURS channels 1-20 are fixed to the 5 MURS frequencies in a repeating
pattern. Frequency in EEPROM is ignored for 1-20. Channels 21-250 are
user-programmable RX-only channels. Changing power level is not allowed
on 1-20. It is possible to set power level on 21-250, but it is not used
(radio forces RX-only).
"""
VENDOR = "Radioddity"
MODEL = "MU-5"
# MU-5 does not actually support variable power levels
# MURS channels are fixed to "Low" power which is presumably 2W
POWER_LEVELS = [chirp_common.PowerLevel("Low", watts=2.00),
chirp_common.PowerLevel("High", watts=2.00)]
MURS_FREQS = [151820000, 151880000, 151940000, 154570000, 154600000]
def _get_murs_freq(self, channel):
return self.MURS_FREQS[(channel - 1) % 5]
def _is_murs_narrowband(self, channel):
return ((channel - 1) % 5) < 3
def get_memory(self, number):
mem = chirp_common.Memory()
mem.number = number
_mem = self._memobj.memory[number-1]
_name = self._memobj.memnames[number-1]["name"]
str_name = self.get_str_name(_name)
mem.name = str_name.rstrip()
# 1-20 always exist and may have 0xFFFFFFFF for freq
# other channels with 0xFFFFFFFF for freq are empty
if _mem.rx_freq.get_raw() == b'\xff\xff\xff\xff':
if not (1 <= number <= 20):
mem.empty = True
return mem
mem.freq = int(_mem.rx_freq) * 10
if _mem.tx_freq.get_raw() == b'\xff\xff\xff\xff':
mem.duplex = 'off'
elif int(_mem.tx_freq) - int(_mem.rx_freq) > 0:
mem.duplex = '+'
mem.offset = (int(_mem.tx_freq) - int(_mem.rx_freq)) * 10
elif int(_mem.tx_freq) - int(_mem.rx_freq) < 0:
mem.duplex = '-'
mem.offset = (int(_mem.rx_freq) - int(_mem.tx_freq)) * 10
mem.mode = self.val_or_def(_mem.mode, self.VALID_MODES)
mem.power = self.val_or_def(_mem.power, self.POWER_LEVELS)
mem.skip = "" if _mem.scan else "S"
# MURS channels 1-20: fixed freq, simplex, fixed power
if 1 <= mem.number <= 20:
mem.freq = self._get_murs_freq(mem.number)
mem.duplex = ""
mem.offset = 0
mem.immutable = ['freq', 'duplex', 'offset', 'power', 'empty']
# Narrowband channels have fixed mode
if self._is_murs_narrowband(mem.number):
mem.mode = "NFM"
mem.immutable = mem.immutable + ['mode']
# Channels 21-250: RX-only user channels
elif mem.number > 20:
mem.duplex = "off"
mem.immutable = ['duplex', 'offset']
txtone = self.get_tone(_mem.tx_tone)
rxtone = self.get_tone(_mem.rx_tone)
chirp_common.split_tone_decode(mem, txtone, rxtone)
mem.extra = RadioSettingGroup("Extra", "extra")
rs = RadioSettingValueBoolean(_mem.busy_lock)
rset = RadioSetting("busy_lock", "Busy Lock", rs)
mem.extra.append(rset)
rs = RadioSettingValueBoolean(_mem.freq_hop)
rset = RadioSetting("freq_hop", "Freq. Hop", rs)
mem.extra.append(rset)
_current = _mem.signal if _mem.signal else 1
rs = RadioSettingValueInteger(1, 15, current=_current)
rset = RadioSetting("signal", "DTMF ID", rs)
mem.extra.append(rset)
options = ['Off', 'BOT', 'EOT', 'BOTH']
rs = RadioSettingValueList(options, current_index=_mem.ptt_id)
rset = RadioSetting("ptt_id", "PTT ID", rs)
mem.extra.append(rset)
return mem
def validate_memory(self, mem):
if 1 <= mem.number <= 20:
expected_freq = self._get_murs_freq(mem.number)
if mem.freq != expected_freq:
return [chirp_common.ValidationError(
f'MURS Channel {mem.number} must be '
f'{expected_freq / 1000000:.3f} MHz')]
if self._is_murs_narrowband(mem.number) and mem.mode != "NFM":
return [chirp_common.ValidationError(
f'MURS Channel {mem.number} must be narrowband (NFM)')]
return chirp_common.CloneModeRadio.validate_memory(self, mem)
def set_memory(self, mem):
# For MURS channels 1-20, write 0xFFFFFFFF for rx/tx freq
# The radio firmware hardcodes the actual MURS frequency
# This is what the official CPS does
if 1 <= mem.number <= 20:
number = mem.number
_mem = self._memobj.memory[number-1]
_name = self._memobj.memnames[number-1]
newname = [str(c) for c in mem.name]
_name.name = "".join(newname).ljust(6, '\x00')
if mem.empty:
_mem.set_raw(b'\xff' * 13 + b'\x06\x11\x00')
return
_mem.rx_freq.fill_raw(b'\xff')
_mem.tx_freq.fill_raw(b'\xff')
_mem.mode = self.idx_or_def(mem.mode, self.VALID_MODES)
_mem.power = self.idx_or_def(mem.power, self.POWER_LEVELS)
_mem.scan = False if mem.skip == "S" else True
((txmode, txval, txpol),
(rxmode, rxval, rxpol)) = chirp_common.split_tone_encode(mem)
self.set_tone(_mem.tx_tone, txmode, txval, txpol)
self.set_tone(_mem.rx_tone, rxmode, rxval, rxpol)
for setting in mem.extra:
setattr(_mem, setting.get_name(), setting.value)
else:
super().set_memory(mem)
chirp-20261030/chirp/drivers/radioddity_r2.py 0000644 0001750 0001750 00000052464 15257672455 017763 0 ustar hibby hibby # Copyright August 2018 Klaus Ruebsam
#
# 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 .
import time
import struct
import logging
from chirp import chirp_common, directory, memmap
from chirp import bitwise, errors, util
from chirp.settings import RadioSetting, RadioSettingGroup, \
RadioSettingValueInteger, RadioSettingValueList, \
RadioSettingValueBoolean, RadioSettings
LOG = logging.getLogger(__name__)
# memory map
# 0000 copy of channel 16: 0100 - 010F
# 0010 Channel 1
# 0020 Channel 2
# 0030 Channel 3
# 0040 Channel 4
# 0050 Channel 5
# 0060 Channel 6
# 0070 Channel 7
# 0080 Channel 8
# 0090 Channel 9
# 00A0 Channel 10
# 00B0 Channel 11
# 00C0 Channel 12
# 00D0 Channel 13
# 00E0 Channel 14
# 00F0 Channel 15
# 0100 Channel 16
# 03C0 various settings
# the last three bytes of every channel are identical
# to the first three bytes of the next channel in row.
# However it will automatically be filled by the radio itself
MEM_FORMAT = """
#seekto 0x0010;
struct {
lbcd rx_freq[4];
lbcd tx_freq[4];
lbcd rx_tone[2];
lbcd tx_tone[2];
u8 unknown1:1,
compand:1,
scramb:1,
scanadd:1,
power:1,
mode:1,
unknown2:1,
bclo:1;
u8 reserved[3];
} memory[%(memnum)i];
#seekto 0x03C0;
struct {
u8 unknown3c08:1,
scanmode:1,
unknown3c06:1,
unknown3c05:1,
voice:2,
save:1,
beep:1;
u8 squelch;
u8 unknown3c2;
u8 timeout;
u8 voxgain;
u8 specialcode;
u8 unknown3c6;
u8 voxdelay;
} settings;
"""
CMD_ACK = b"\x06"
CMD_ALT_ACK = b"\x53"
CMD_STX = b"\x02"
CMD_ENQ = b"\x05"
POWER_LEVELS = [chirp_common.PowerLevel("Low", watts=0.50),
chirp_common.PowerLevel("High", watts=3.00)]
TIMEOUT_LIST = ["Off"] + ["%s seconds" % x for x in range(30, 330, 30)]
SCANMODE_LIST = ["Carrier", "Timer"]
VOICE_LIST = ["Off", "Chinese", "English"]
VOX_LIST = ["Off"] + ["%s" % x for x in range(1, 9)]
VOXDELAY_LIST = ["0.5", "1.0", "1.5", "2.0", "2.5", "3.0"]
MODE_LIST = ["FM", "NFM"]
TONES = chirp_common.TONES
DTCS_CODES = chirp_common.DTCS_CODES
FRS16_FREQS = [462562500, 462587500, 462612500, 462637500,
462662500, 462625000, 462725000, 462687500,
462712500, 462550000, 462575000, 462600000,
462650000, 462675000, 462700000, 462725000]
PMR_FREQS1 = [446006250, 446018750, 446031250, 446043750, 446056250,
446068750, 446081250, 446093750]
PMR_FREQS2 = [446106250, 446118750, 446131250, 446143750, 446156250,
446168750, 446181250, 446193750]
PMR_FREQS = PMR_FREQS1 + PMR_FREQS2
VALID_CHARS = chirp_common.CHARSET_ALPHANUMERIC + \
"`{|}!\"#$%&'()*+,-./:;<=>?@[]^_"
def _r2_enter_programming_mode(radio):
serial = radio.pipe
magic = b"TYOGRAM"
exito = False
serial.write(CMD_STX)
for i in range(0, 5):
for j in range(0, len(magic)):
serial.write(magic[j:j + 1])
ack = serial.read(1)
if ack == CMD_ACK:
exito = True
break
# check if we had EXITO
if exito is False:
if not ack:
raise errors.RadioNoResponse()
msg = "The radio did not accept program mode after five tries.\n"
msg += "Check your interface cable and power cycle your radio."
raise errors.RadioError(msg)
try:
serial.write(CMD_STX)
ident = serial.read(8)
except:
_r2_exit_programming_mode(radio)
raise errors.RadioError("Error communicating with radio")
# No idea yet what the next 7 bytes stand for
# as long as they start with ACK (or ALT_ACK on some devices) we are fine
if not ident.startswith(CMD_ACK) and not ident.startswith(CMD_ALT_ACK):
_r2_exit_programming_mode(radio)
LOG.debug(util.hexprint(ident))
raise errors.RadioError("Radio returned unknown identification string")
try:
serial.write(CMD_ACK)
ack = serial.read(1)
except:
_r2_exit_programming_mode(radio)
raise errors.RadioError("Error communicating with radio")
if ack != CMD_ACK:
_r2_exit_programming_mode(radio)
raise errors.RadioError("Radio refused to enter programming mode")
# the next 6 bytes represent the 6 digit password
# they are somehow coded where '1' becomes x01 and 'a' becomes x25
try:
serial.write(CMD_ENQ)
ack = serial.read(6)
except:
_r2_exit_programming_mode(radio)
raise errors.RadioError("Error communicating with radio")
# we will only read if no password is set
if ack != b"\xFF\xFF\xFF\xFF\xFF\xFF":
_r2_exit_programming_mode(radio)
raise errors.RadioError("Radio is password protected")
try:
serial.write(CMD_ACK)
ack = serial.read(1)
except:
_r2_exit_programming_mode(radio)
raise errors.RadioError("Error communicating with radio 2")
# The latest Retevis RT24 (and likely the RT24V and H777S) models no
# longer acknowledge the CMD_ACK above so the 'ack' will be empty and
# fail the check causing cloning to fail.
#
# The factory CPS continues with or without an ack so CHIRP will
# behave the same way.
if ack and ack != CMD_ACK:
_r2_exit_programming_mode(radio)
raise errors.RadioError("Radio refused to enter programming mode 2")
def _r2_exit_programming_mode(radio):
serial = radio.pipe
try:
serial.write(CMD_ACK)
except:
raise errors.RadioError("Radio refused to exit programming mode")
def _r2_read_block(radio, block_addr, block_size):
serial = radio.pipe
cmd = struct.pack(">cHb", b'R', block_addr, block_size)
expectedresponse = b"W" + cmd[1:]
LOG.debug("Reading block %04x..." % (block_addr))
try:
for j in range(0, len(cmd)):
time.sleep(0.005)
serial.write(cmd[j:j + 1])
response = serial.read(4 + block_size)
if response[:4] != expectedresponse:
_r2_exit_programming_mode(radio)
raise Exception("Error reading block %04x." % (block_addr))
block_data = response[4:]
time.sleep(0.005)
serial.write(CMD_ACK)
ack = serial.read(1)
except:
_r2_exit_programming_mode(radio)
raise errors.RadioError("Failed to read block at %04x" % block_addr)
if ack != CMD_ACK:
_r2_exit_programming_mode(radio)
raise Exception("No ACK reading block %04x." % (block_addr))
return block_data
def _r2_write_block(radio, block_addr, block_size):
serial = radio.pipe
cmd = struct.pack(">cHb", b'W', block_addr, block_size)
data = radio.get_mmap()[block_addr:block_addr + block_size]
LOG.debug("Writing block %04x..." % (block_addr))
LOG.debug(util.hexprint(cmd + data))
try:
for j in range(0, len(cmd)):
serial.write(cmd[j:j + 1])
for j in range(0, len(data)):
serial.write(data[j:j + 1])
if serial.read(1) != CMD_ACK:
raise Exception("No ACK")
except:
_r2_exit_programming_mode(radio)
raise errors.RadioError("Failed to send block "
"%04x to radio" % block_addr)
def do_download(radio):
LOG.debug("download")
_r2_enter_programming_mode(radio)
data = b""
status = chirp_common.Status()
status.msg = "Cloning from radio"
status.cur = 0
status.max = radio._memsize
for addr in range(0, radio._memsize, radio._block_size):
status.cur = addr + radio._block_size
radio.status_fn(status)
block = _r2_read_block(radio, addr, radio._block_size)
data += block
LOG.debug("Address: %04x" % addr)
LOG.debug(util.hexprint(block))
_r2_exit_programming_mode(radio)
return memmap.MemoryMapBytes(data)
def do_upload(radio):
status = chirp_common.Status()
status.msg = "Uploading to radio"
_r2_enter_programming_mode(radio)
status.cur = 0
status.max = radio._memsize
for start_addr, end_addr, block_size in radio._ranges:
for addr in range(start_addr, end_addr, block_size):
status.cur = addr + block_size
radio.status_fn(status)
_r2_write_block(radio, addr, block_size)
_r2_exit_programming_mode(radio)
@directory.register
class RadioddityR2(chirp_common.CloneModeRadio):
"""Radioddity R2"""
VENDOR = "Radioddity"
MODEL = "R2"
BAUD_RATE = 9600
# definitions on how to read StartAddr EndAddr BlockZize
_ranges = [
(0x0000, 0x01F8, 0x08),
(0x01F8, 0x03F0, 0x08)
]
_memsize = 0x03F0
# never read more than 8 bytes at once
_block_size = 0x08
# frequency range is 400-470 MHz
_range = [400000000, 470000000]
# maximum 16 channels
_upper = 16
_mem_params = {
'memnum': _upper, # number of channels
}
_frs16 = _pmr = False
def get_features(self):
rf = chirp_common.RadioFeatures()
rf.has_settings = True
rf.has_bank = False
rf.has_tuning_step = False
rf.has_name = False
rf.has_offset = True
rf.has_mode = True
rf.has_dtcs = True
rf.has_rx_dtcs = True
rf.has_dtcs_polarity = True
rf.has_ctone = True
rf.has_cross = True
rf.can_odd_split = False
# FIXME: Is this right? The get_memory() has no checking for
# deleted memories, but set_memory() used to reference a missing
# variable likely copied from another driver
rf.can_delete = False
rf.valid_modes = MODE_LIST
rf.valid_duplexes = ["", "-", "+", "off"]
rf.valid_tmodes = ["", "Tone", "TSQL", "DTCS", "Cross"]
rf.valid_cross_modes = [
"Tone->DTCS",
"DTCS->Tone",
"->Tone",
"Tone->Tone",
"->DTCS",
"DTCS->",
"DTCS->DTCS"]
rf.valid_power_levels = POWER_LEVELS
rf.valid_skips = ["", "S"]
rf.valid_bands = [self._range]
rf.memory_bounds = (1, self._upper)
rf.valid_tuning_steps = [2.5, 5.0, 6.25, 10.0, 12.5, 25.0]
return rf
def process_mmap(self):
"""Process the mem map into the mem object"""
self._memobj = bitwise.parse(MEM_FORMAT % self._mem_params, self._mmap)
# to set the vars on the class to the correct ones
def sync_in(self):
"""Download from radio"""
try:
data = do_download(self)
except errors.RadioError:
# Pass through any real errors we raise
raise
except:
# If anything unexpected happens, make sure we raise
# a RadioError and log the problem
LOG.exception('Unexpected error during download')
raise errors.RadioError('Unexpected error communicating '
'with the radio')
self._mmap = data
self.process_mmap()
def sync_out(self):
"""Upload to radio"""
try:
do_upload(self)
except errors.RadioError:
raise
except Exception:
# If anything unexpected happens, make sure we raise
# a RadioError and log the problem
LOG.exception('Unexpected error during upload')
raise errors.RadioError('Unexpected error communicating '
'with the radio')
def get_raw_memory(self, number):
return repr(self._memobj.memory[number - 1])
def decode_tone(self, val):
"""Parse the tone data to decode from mem, it returns:
Mode (''|DTCS|Tone), Value (None|###), Polarity (None,N,R)"""
if val.get_raw(asbytes=False) == "\xFF\xFF":
return '', None, None
val = int(val)
if val >= 12000:
a = val - 12000
return 'DTCS', a, 'R'
elif val >= 8000:
a = val - 8000
return 'DTCS', a, 'N'
else:
a = val / 10.0
return 'Tone', a, None
def encode_tone(self, memval, mode, value, pol):
"""Parse the tone data to encode from UI to mem"""
if mode == '':
memval[0].set_raw(0xFF)
memval[1].set_raw(0xFF)
elif mode == 'Tone':
memval.set_value(int(value * 10))
elif mode == 'DTCS':
flag = 0x80 if pol == 'N' else 0xC0
memval.set_value(value)
memval[1].set_bits(flag)
else:
raise Exception("Internal error: invalid mode `%s'" % mode)
def get_memory(self, number):
bitpos = (1 << ((number - 1) % 8))
bytepos = ((number - 1) / 8)
LOG.debug("bitpos %s" % bitpos)
LOG.debug("bytepos %s" % bytepos)
_mem = self._memobj.memory[number - 1]
mem = chirp_common.Memory()
mem.number = number
mem.freq = int(_mem.rx_freq) * 10
# We'll consider any blank (i.e. 0 MHz frequency) to be empty
if mem.freq == 0:
mem.empty = True
return mem
if _mem.rx_freq.get_raw(asbytes=False) == "\xFF\xFF\xFF\xFF":
mem.freq = 0
mem.empty = True
return mem
txfreq = int(_mem.tx_freq) * 10
if txfreq == mem.freq:
mem.duplex = ""
elif txfreq == 0:
mem.duplex = "off"
mem.offset = 0
# 166666665*10 is the equivalent for FF FF FF FF
# stored in the TX field
elif txfreq == 1666666650:
mem.duplex = "off"
mem.offset = 0
elif txfreq < mem.freq:
mem.duplex = "-"
mem.offset = mem.freq - txfreq
elif txfreq > mem.freq:
mem.duplex = "+"
mem.offset = txfreq - mem.freq
# get bandwidth FM or NFM
mem.mode = MODE_LIST[_mem.mode]
# tone data
txtone = self.decode_tone(_mem.tx_tone)
rxtone = self.decode_tone(_mem.rx_tone)
chirp_common.split_tone_decode(mem, txtone, rxtone)
if not _mem.scanadd:
mem.skip = "S"
mem.power = POWER_LEVELS[_mem.power]
# add extra channel settings to the OTHER tab of the properties
# extra settings are unfortunately inverted
mem.extra = RadioSettingGroup("extra", "Extra")
bclo = RadioSetting("bclo", "Busy Lockout",
RadioSettingValueBoolean(not bool(_mem.bclo)))
bclo.set_doc("Busy Lockout")
mem.extra.append(bclo)
scramb = RadioSetting("scramb", "Scramble",
RadioSettingValueBoolean(not bool(_mem.scramb)))
scramb.set_doc("Scramble Audio Signal")
mem.extra.append(scramb)
compand = RadioSetting("compand", "Compander",
RadioSettingValueBoolean(
not bool(_mem.compand)))
compand.set_doc("Compress Audio for TX")
mem.extra.append(compand)
immutable = []
if self._frs16:
if mem.freq in FRS16_FREQS:
if mem.number >= 1 and mem.number <= 16:
FRS_FREQ = FRS16_FREQS[mem.number - 1]
mem.freq = FRS_FREQ
mem.duplex == ''
mem.offset = 0
mem.mode = "NFM"
immutable = ["empty", "freq", "duplex", "offset", "mode"]
elif self._pmr:
if mem.freq in PMR_FREQS:
if mem.number >= 1 and mem.number <= 16:
PMR_FREQ = PMR_FREQS[mem.number - 1]
mem.freq = PMR_FREQ
mem.duplex = ''
mem.offset = 0
mem.mode = "NFM"
mem.power = POWER_LEVELS[0]
immutable = ["empty", "freq", "duplex", "offset", "mode",
"power"]
mem.immutable = immutable
return mem
def set_memory(self, mem):
bitpos = (1 << ((mem.number - 1) % 8))
bytepos = ((mem.number - 1) / 8)
LOG.debug("bitpos %s" % bitpos)
LOG.debug("bytepos %s" % bytepos)
# Get a low-level memory object mapped to the image
_mem = self._memobj.memory[mem.number - 1]
_rsvd = _mem.reserved.get_raw(asbytes=False)
if mem.empty:
_mem.set_raw("\xFF" * 13 + _rsvd)
return
_mem.rx_freq = mem.freq / 10
if mem.duplex == "off":
for i in range(0, 4):
_mem.tx_freq[i].set_raw("\xFF")
elif mem.duplex == "+":
_mem.tx_freq = (mem.freq + mem.offset) / 10
elif mem.duplex == "-":
_mem.tx_freq = (mem.freq - mem.offset) / 10
else:
_mem.tx_freq = mem.freq / 10
# power, default power is low
if mem.power:
_mem.power = POWER_LEVELS.index(mem.power)
else:
_mem.power = 0 # low
# tone data
((txmode, txtone, txpol), (rxmode, rxtone, rxpol)) = \
chirp_common.split_tone_encode(mem)
self.encode_tone(_mem.tx_tone, txmode, txtone, txpol)
self.encode_tone(_mem.rx_tone, rxmode, rxtone, rxpol)
_mem.scanadd = mem.skip != "S"
_mem.mode = MODE_LIST.index(mem.mode)
# extra settings are unfortunately inverted
for setting in mem.extra:
LOG.debug("@set_mem: %s %s", setting.get_name(), setting.value)
setattr(_mem, setting.get_name(), not setting.value)
def get_settings(self):
_settings = self._memobj.settings
basic = RadioSettingGroup("basic", "Basic Settings")
top = RadioSettings(basic)
rs = RadioSetting("settings.squelch", "Squelch Level",
RadioSettingValueInteger(0, 9, _settings.squelch))
basic.append(rs)
rs = RadioSetting("settings.timeout", "Timeout Timer",
RadioSettingValueList(
TIMEOUT_LIST, current_index=_settings.timeout))
basic.append(rs)
rs = RadioSetting("settings.scanmode", "Scan Mode",
RadioSettingValueList(
SCANMODE_LIST,
current_index=_settings.scanmode))
basic.append(rs)
rs = RadioSetting("settings.voice", "Voice Prompts",
RadioSettingValueList(
VOICE_LIST, current_index=_settings.voice))
basic.append(rs)
rs = RadioSetting("settings.voxgain", "VOX Level",
RadioSettingValueList(
VOX_LIST, current_index=_settings.voxgain))
basic.append(rs)
rs = RadioSetting("settings.voxdelay", "VOX Delay Time",
RadioSettingValueList(
VOXDELAY_LIST,
current_index=_settings.voxdelay))
basic.append(rs)
rs = RadioSetting("settings.save", "Battery Save",
RadioSettingValueBoolean(_settings.save))
basic.append(rs)
rs = RadioSetting("settings.beep", "Beep Tone",
RadioSettingValueBoolean(_settings.beep))
basic.append(rs)
def _filter(name):
filtered = ""
for char in str(name):
if char in VALID_CHARS:
filtered += char
else:
filtered += " "
return filtered
return top
def set_settings(self, settings):
for element in settings:
if not isinstance(element, RadioSetting):
self.set_settings(element)
continue
else:
try:
if "." in element.get_name():
bits = element.get_name().split(".")
obj = self._memobj
for bit in bits[:-1]:
obj = getattr(obj, bit)
setting = bits[-1]
else:
obj = self._memobj.settings
setting = element.get_name()
LOG.debug("Setting %s = %s" % (setting, element.value))
setattr(obj, setting, element.value)
except Exception:
LOG.debug(element.get_name())
raise
@directory.register
class RetevisRT24(RadioddityR2):
"""Retevis RT24"""
VENDOR = "Retevis"
MODEL = "RT24"
_pmr = False # sold as PMR radio but supports full band TX/RX
@directory.register
class RetevisRT24V(RadioddityR2):
"""Retevis RT24V"""
VENDOR = "Retevis"
MODEL = "RT24V"
# sold as FreeNet radio but supports full band TX/RX
# frequency range is 136-174 MHz
_range = [136000000, 174000000]
# maximum 6 channels
_upper = 6
_mem_params = {
'memnum': _upper, # number of channels
}
@directory.register
class RetevisH777S(RadioddityR2):
"""Retevis H777S"""
VENDOR = "Retevis"
MODEL = "H777S"
_frs16 = False # sold as FRS radio but supports full band TX/RX
chirp-20261030/chirp/drivers/radtel_rt490.py 0000644 0001750 0001750 00000205547 15257672455 017443 0 ustar hibby hibby # Copyright 2023 angeof9 angelof9@protonmail.com
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see http://www.gnu.org/licenses/.
#
# This file is dual-licensed by the copyright holder, under the GPLv3
# for inclusion in CHIRP here.
# The code is also available under the BSD 2-Clause "Simplified" License
# here:
# https://chirpmyradio.com/attachments/7833
# Python3 byte clean by Darryl Pogue
# Pep8 style compliant by Jim Unroe
import logging
import struct
from chirp import (
bitwise,
chirp_common,
directory,
errors,
memmap,
util,
)
from chirp.settings import (
RadioSetting,
RadioSettingGroup,
RadioSettings,
RadioSettingValueBoolean,
RadioSettingValueInteger,
RadioSettingValueList,
RadioSettingValueString,
RadioSettingValueMap,
RadioSettingValueFloat,
InvalidValueError
)
LOG = logging.getLogger(__name__)
# 'True' or 'False'
# True unlocks:
# - FM preset
# - Channel memory # preset
# - Killswitch (with revive killed radios)
# - Bands settings
# - Disable radio identification string verification
# (good to recover from a bad state)
RT490_EXPERIMENTAL = True
MEM_FORMAT_RT490 = """
struct { // Memory settings
lbcd rxfreq[4];
lbcd txfreq[4];
ul16 rxtone;
ul16 txtone;
u8 signal; // int 0->14, Signal 1->15
u8 pttid; // ['OFF', 'BOT', 'EOT', 'Both']
u8 dcp:4, // What is DCP ? FHSS ? DC-FHSS ??? TODO
power:4; // POWER_LEVELS
u8 unknown3_0:1, // Used by the driver to store AM/NAM flag
// (thank you Radtel for free space)
narrow:1, // bool true=NFM false=FM (col[7] a)
unknown3_1:1,
unknown3_2:1,
bcl:1, // bool (col[9] a2)
scan:1, // bool (col[10] a3)
tx_enable:1, // bool (col[1] a4)
learn:1; // bool ??? TODO (col[14] a5)
} memory[%(memsize)d];
// #seekto 0x1000; // Memory names (@4096)
struct {
char name[12];
u8 ffpad[4];
} memname[%(memsize)d];
// #seekto 0x2000; // GOOD DCP keys ??? TODO ?? (@8192)
struct {
u8 code[4];
} memcode[%(memsize)d]; // up to @x2400
// Filled with xFF during download (=> no need to
// fill with xFF), ready to upload
#seekto 0x3400; // Custom ANI Names (@13312)
struct {
char name[10];
u8 ffpad[6];
} custom_ani_names[10];
// Filled with xFF during download (=> no need to
// fill with xFF), ready to upload
#seekto 0x3500; // ANI Codes (@13568)
struct {
u8 anicode[6];
u8 ffpad[10];
} anicodes[60];
// Filled with xFF during download (=> no need to
// fill with xFF), ready to upload
#seekto 0x3900; // Custom channel names (@14592)
struct {
char name[10];
u8 ffpad[6];
} custom_channel_names[10];
// Filled with xFF during download (=> no need to
// fill with xFF), ready to upload
#seekto 0x3A00; // Settings (@14848)
struct {
u8 squelch; // 0: int 0 -> 9
u8 savemode; // 1: ['OFF', 'Normal', 'Super', 'Deep']
u8 vox; // 2: off=0, 1 -> 9
u8 backlight; // 3: ['OFF', '5s', '15s', '20s', '30s', '1m',
// '2m', '3m']
u8 tdr; // 4: bool
u8 timeout; // 5: n*30seconds, 0-240s
u8 beep; // 6: bool
u8 voice; // 7: bool
u8 byte_not_used_10; // 8: Always 1
u8 dtmfst; // 9: ['OFF', 'KB Side Tone', 'ANI Side Tone',
// 'KB ST + ANI ST']
u8 scanmode; // 10: ['TO', 'CO', 'SE']
u8 pttid; // 11: ['OFF', 'BOT', 'EOT', 'Both']
u8 pttiddelay; // 12: ['0', '100ms', '200ms', '400ms', '600ms',
// '800ms', '1000ms']
u8 cha_disp; // 13: ['Name', 'Freq', 'Channel ID']
u8 chb_disp; // 14: ['Name', 'Freq', 'Channel ID']
u8 bcl; // 15: bool
u8 autolock; // 0: ['OFF', '5s', '10s', 15s']
u8 alarm_mode; // 1: ['Site', 'Tone', 'Code']
u8 alarmsound; // 2: bool
u8 txundertdr; // 3: ['OFF', 'A', 'B']
u8 tailnoiseclear; // 4: [off, on]
u8 rptnoiseclear; // 5: n*100ms, 0-1000
u8 rptnoisedelay; // 6: n*100ms, 0-1000
u8 roger; // 7: bool
u8 active_channel; // 8: 0 or 1
u8 fmradio; // 9: boolean, inverted
u8 workmodeb:4, // 10:up ['VFO', 'CH Mode']
workmodea:4; // 10:down ['VFO', 'CH Mode']
u8 kblock; // 11: bool // TODO TEST WITH autolock
u8 powermsg; // 12: 0=Image / 1=Voltage
u8 byte_not_used_21; // 13: Always 0
u8 rpttone; // 14: ['1000Hz', '1450Hz', '1750Hz', '2100Hz']
u8 byte_not_used_22; // 15: pad with xFF
u8 vox_delay; // 0: [str(float(a)/10)+'s' for a in range(5,21)]
// '0.5s' to '2.0s'
u8 timer_menu_quit; // 1: ['5s', '10s', '15s', '20s', '25s', '30s',
// '35s', '40s', '45s', '50s', '60s']
u8 byte_not_used_30; // 2: pad with xFF
u8 byte_not_used_31; // 3: pad with xFF
u8 enable_killsw; // 4: bool
u8 display_ani; // 5: bool
u8 byte_not_used_32; // 6: pad with xFF
u8 enable_gps; // 7: bool
u8 scan_dcs; // 8: ['All', 'Receive', 'Transmit']
u8 ani_id; // 9: int 0-59 (ANI 1-60)
u8 rx_time; // 10: bool
u8 ffpad0[5]; // 11: Pad xFF
u8 cha_memidx; // 0: Memory index when channel A use memories
u8 byte_not_used_40;
u8 chb_memidx; // 2: Memory index when channel B use memories
u8 byte_not_used_41;
u8 ffpad1[10];
ul16 fmpreset;
} settings;
// Filled with xFF during download (=> no need to
// fill with xFF), ready to upload
struct settings_vfo_chan {
u8 rxfreq[8]; // 0
ul16 rxtone; // 8
ul16 txtone; // 10
ul16 byte_not_used0; // 12 Pad xFF
u8 sftd:4, // 14 Shift dir ['OFF', '+', '-']
signal:4; // 14 int 0->14, Signal 1->15
u8 byte_not_used1; // 15 Pad xFF
u8 power; // 16:0 POWER_LEVELS
u8 fhss:4, // 17 ['OFF', 'FHSS 1', 'FHSS 2', 'FHSS 3',
// 'FHSS 4']
narrow:4; // 17 bool true=NFM false=FM
u8 byte_not_used2; // 18 Pad xFF but received 0x00 ???
u8 freqstep; // 19:3 ['2.5 KHz', '5.0 KHz', '6.25 KHz',
// '10.0 KHz', '12.5 KHz', '20.0 KHz',
// '25.0 KHz', '50.0 KHz']
u8 byte_not_used3; // 20:4 Pad xFF but received 0x00 ??? TODO
u8 offset[6]; // 21:5 Freq NN.NNNN (without the dot) TEST TEST
u8 byte_not_used4; // 27:11 Pad xFF
u8 byte_not_used5; // 28 Pad xFF
u8 byte_not_used6; // 29 Pad xFF
u8 byte_not_used7; // 30 Pad xFF
u8 byte_not_used8; // 31:15 Pad xFF
};
// #seekto 0x3A40; // VFO A/B (@14912)
struct {
struct settings_vfo_chan vfo_a;
struct settings_vfo_chan vfo_b;
} settings_vfo;
// #seekto 0x3A80; // Side keys settings (@14976)
struct { // Values from Radio
u8 pf2_short; // { '7': 'FM', '10': 'Tx Power', '28': 'Scan',
// '29': 'Search, '1': 'PPT B' }
u8 pf2_long; // { '7': 'FM', '10': 'Tx Power', '28': 'Scan',
// '29': 'Search' }
u8 pf3_short; // {'7': 'FM', '10': 'Tx Power', '28': 'Scan',
// '29': 'Search'}
u8 ffpad; // Pad xFF
} settings_sidekeys;
struct dtmfcode {
u8 code[5]; // 5 digits DTMF
u8 ffpad[11]; // Pad xFF
};
// Filled with xFF during download (=> no need to
// fill with xFF), ready to upload
#seekto 0x3B00; // DTMF (@15104)
struct dtmfcode settings_dtmfgroup[15];
struct { // @15296+3x16
u8 byte_not_used1; // 0: Pad xFF something here
u8 byte_not_used2; // 1: Pad xFF something here
u8 byte_not_used3; // 2: Pad xFF something here
u8 byte_not_used4; // 3: Pad xFF
u8 byte_not_used5; // 4: Pad xFF
u8 unknown_dtmf; // 5: 0 TODO ???? wtf is alarmcode/alarmcall TODO
u8 pttid; // 6: [off, BOT, EOT, Both]
u8 dtmf_speed_on; // 7: ['50ms', '100ms', '200ms', '300ms', '500ms']
u8 dtmf_speed_off; // 8:0 ['50ms', '100ms', '200ms', '300ms', '500ms']
} settings_dtmf;
// Filled with xFF during download (=> no need to
// fill with xFF), ready to upload
#seekto 0x3C00; // DTMF Kill/ReLive Codes (@15360)
struct {
u8 kill_dtmf[6]; // 0: Kill DTMF
u8 ffpad1[2]; // Pad xFF
u8 revive_dtmf[6]; // 8: Revive DTMF
u8 ffpad2[2]; // Pad xFF
} settings_killswitch;
// Some unknown data between 0x3E00 and 0x3F00
#seekto 0x3F80; // Hmm hmm
struct {
u8 unknown_data_0[16];
u8 language;
u8 active; // Bool radio killed (killed=0, active=1)
u8 unknown_data_2[46];
} management_settings;
struct band {
u8 enable; // 0 bool / enable-disable Tx on band
bbcd freq_low[2]; // 1 lowest band frequency
bbcd freq_high[2]; // 3 highest band frequency
};
// #seekto 0x3FC0; // Bands settings (@16320)
struct {
struct band band136; // 0 Settings for 136MHz band
struct band band400; // 5 Settings for 400MHz band
struct band band200; // 10 Settings for 200MHz band
u8 byte_not_used1; // 15
struct band band350; // 0 Settings for 350MHz band
u8 byte_not_used2[43]; // 5
} settings_bands;
"""
CMD_ACK = b"\x06"
DTCS_CODES = tuple(sorted(chirp_common.DTCS_CODES + (645,)))
DTMFCHARS = '0123456789ABCD*#'
def _enter_programming_mode(radio):
serial = radio.pipe
exito = False
for i in range(0, 5):
serial.write(radio._magic)
ack = serial.read(1)
try:
if ack == CMD_ACK:
exito = True
break
except Exception:
LOG.debug("Attempt #%s, failed, trying again" % i)
pass
# check if we had EXITO
if exito is False:
if not ack:
raise errors.RadioNoResponse()
msg = "The radio did not accept program mode after five tries.\n"
msg += "Check you interface cable and power cycle your radio."
raise errors.RadioError(msg)
try:
serial.write(b"F")
ident = serial.read(8)
except Exception:
raise errors.RadioError("Error communicating with radio")
if not ident.startswith(radio._fingerprint) and not RT490_EXPERIMENTAL:
LOG.debug(util.hexprint(ident))
raise errors.RadioError("Radio returned unknown identification string")
def _exit_programming_mode(radio):
serial = radio.pipe
try:
serial.write(b"E")
except Exception:
raise errors.RadioError("Radio refused to exit programming mode")
def _read_block(radio, block_addr, block_size):
serial = radio.pipe
cmd = struct.pack(">cHb", b'R', block_addr, block_size)
expectedresponse = b"R" + cmd[1:]
LOG.debug("Reading block %04x..." % (block_addr))
try:
serial.write(cmd)
response = serial.read(4 + block_size)
if response[:4] != expectedresponse:
raise Exception("Error reading block %04x." % (block_addr))
block_data = response[4:]
except Exception:
raise errors.RadioError("Failed to read block at %04x" % block_addr)
return block_data
def _write_block(radio, block_addr, block_size):
serial = radio.pipe
cmd = struct.pack(">cHb", b'W', block_addr, block_size)
data = radio.get_mmap()[block_addr:block_addr + block_size]
LOG.debug("Writing Data:")
LOG.debug(util.hexprint(cmd + data))
try:
serial.write(cmd + data)
if serial.read(1) != CMD_ACK:
raise Exception("No ACK")
except Exception:
raise errors.RadioError("Failed to send block "
"to radio at %04x" % block_addr)
def do_download(radio):
LOG.debug("download")
_enter_programming_mode(radio)
data = b""
status = chirp_common.Status()
status.msg = "Cloning from radio"
status.cur = 0
status.max = radio._memsize
for addr in range(0, radio._memsize, radio.BLOCK_SIZE):
status.cur = addr + radio.BLOCK_SIZE
radio.status_fn(status)
block = _read_block(radio, addr, radio.BLOCK_SIZE)
data += block
LOG.debug("Address: %04x" % addr)
LOG.debug(util.hexprint(block))
_exit_programming_mode(radio)
return memmap.MemoryMapBytes(data)
def do_upload(radio):
status = chirp_common.Status()
status.msg = "Uploading to radio"
_enter_programming_mode(radio)
status.cur = 0
status.max = radio._memsize
for start_addr, end_addr in radio._ranges:
for addr in range(start_addr, end_addr, radio.BLOCK_SIZE):
status.cur = addr + radio.BLOCK_SIZE
radio.status_fn(status)
_write_block(radio, addr, radio.BLOCK_SIZE)
_exit_programming_mode(radio)
@directory.register
class RT490Radio(chirp_common.CloneModeRadio):
"""RADTEL RT-490"""
VENDOR = "Radtel"
MODEL = "RT-490"
BLOCK_SIZE = 0x40 # 64 bytes
BAUD_RATE = 9600
POWER_LEVELS = [chirp_common.PowerLevel("H", watts=5.00),
chirp_common.PowerLevel("L", watts=3.00)]
# magic = progmode + modelType + garbage (works with any last char)
_magic = b"PROGROMJJCCU"
# fingerprint is default band ranges of the radio
# the driver can change band ranges and fingerprint will
# change accordingly, so it is not used to verify radio id.
_fingerprint = b"\x01\x36\x01\x80\x04\x00\x05\x20"
# Ranges of memory used when uploading data to radio
# same as official software
_ranges = [
(0x0000, 0x2400),
(0x3400, 0x3C40),
(0x3FC0, 0x4000)
]
def filter_name(self, name):
return name.encode('gb2312')[:12].decode('gb2312', errors='replace')
if RT490_EXPERIMENTAL:
# Experimental driver (already heavily tested)
_ranges = [(0x0000, 0x2400), (0x3400, 0x3C40), (0x3F80, 0x4000)]
# Danger zone
# _ranges = [(0x0000, 0x2500), (0x3400, 0x3C40), (0x3E00, 0x4000)]
# 16KB of memory, download read everything
# same as official software (remark: loops if overread :))
_memsize = 16384
POWER_LEVELS_LIST = [str(i) for i in POWER_LEVELS]
FHSS_LIST = ['OFF', 'ENCRYPT 1', 'ENCRYPT 2', 'ENCRYPT 3', 'ENCRYPT 4']
DCP_LIST = ['OFF', 'DCP1', 'DCP2', 'DCP3', 'DCP4'] # Same as FHSS ?
# # Seems yes
TUNING_STEPS = [2.5, 5.0, 6.25, 10.0, 12.5, 20.0, 25.0, 50.0]
TUNING_STEPS_LIST = [str(i)+' KHz' for i in TUNING_STEPS]
SIGNAL = [str(i) for i in range(1, 16)]
PTTID = ['OFF', 'BOT', 'EOT', 'Both']
PTTIDDELAYS = ['0', '100ms', '200ms', '400ms', '600ms', '800ms', '1000ms']
DTMF_SPEEDS = ['50ms', '100ms', '200ms', '300ms', '500ms']
SIDEKEY_VALUEMAP = [('FM', 7), ('Tx Power', 10), ('Scan', 28),
('Search', 29), ('PTT B', 1)]
KEY_CHARS = '0123456789ABCDEF'
FULL_CHARSET_ASCII = ("".join([chr(x) for x in range(ord(" "),
ord("~") + 1)] + [chr(x) for x in range(128, 255)] +
[chr(0)]))
VFO_SFTD = ['OFF', '+', '-']
WORKMODES = ['VFO', 'Memory Mode']
SAVEMODES = ['OFF', 'Normal', 'Super', 'Deep']
DISPLAYMODES = ['Name', 'Freq', 'Memory ID']
SCANMODES = ['TO', 'CO', 'SE']
ALARMMODES = ['On Site', 'Send Sound', 'Send Code']
TDRTXMODES = ['OFF', 'A', 'B']
SCANDCSMODES = ['All', 'Receive', 'Transmit']
POWERMESSAGES = ['Image', 'Voltage']
FMRADIO = ['ON', 'OFF']
ENABLERADIO = ['Killed', 'Active']
CHANNELS = ['A', 'B']
TOT_LIST = ['OFF'] + [str(i*30) + "s" for i in range(1, 9)]
VOX_LIST = ['OFF'] + [str(i) for i in range(1, 9)]
BACKLIGHT_TO = ['OFF', '5s', '10s', '15s', '20s', '30s', '1m', '2m', '3m']
AUTOLOCK_TO = ['OFF', '5s', '10s', '15s']
MENUEXIT_TO = ['5s', '10s', '15s', '20s', '25s', '30s', '35s', '40s',
'45s', '50s', '60s']
SQUELCHLVLS = [str(i) for i in range(10)]
ANI_IDS = [str(i+1) for i in range(60)]
VOXDELAYLIST = [str(float(a)/10)+'s' for a in range(5, 21)]
DTMFSTLIST = ['OFF', 'DT Side Tone', 'ANI Side Tone', 'DT ST + ANI ST']
RPTTONES = ['1000Hz', '1450Hz', '1750Hz', '2100Hz']
RPTNOISE = [str(a)+'s' for a in range(11)]
_memory_size = _upper = 256 # Number of memory slots
_mem_params = (_upper-1)
_frs = _murs = _pmr = _gmrs = True
def set_settings(self, settings):
for element in settings:
if not isinstance(element, RadioSetting):
self.set_settings(element)
continue
else:
self._set_setting(element)
def _set_setting(self, setting): # WIP
_mem = self._memobj
key = setting.get_name()
val = setting.value
if key.startswith('dummy'):
return
elif key.startswith('settings_dtmfgroup.'):
if str(val) == "":
setattr(_mem.settings_dtmfgroup[int(key.split('@')[1])-1],
'code', [0xFF]*5)
else:
tmp = [DTMFCHARS.index(c) for c in str(val)]
tmp += [0xFF] * (5 - len(tmp))
setattr(_mem.settings_dtmfgroup[int(key.split('@')[1])-1],
'code', tmp)
elif key.startswith('settings.'):
if key.endswith('_memidx'):
val = int(val) - 1
if key.endswith('fmpreset'):
tmp = val.get_value() * 10
setattr(_mem.settings, key.split('.')[1], tmp)
else:
setattr(_mem.settings, key.split('.')[1], int(val))
elif key.startswith('settings_dtmf.'):
attr = key.split('.')[1]
setattr(_mem.settings_dtmf, attr, int(val))
if attr.startswith('pttid'):
setattr(_mem.settings, attr, int(val))
elif key.startswith('settings_sidekeys.'):
setattr(_mem.settings_sidekeys, key.split('.')[1], int(val))
elif key.startswith('settings_vfo.'): # TODO rx/tx tones
tmp = key.split('.')
attr = tmp[2]
vfo = tmp[1]
# LOG.debug(">>> PRE key '%s'" % key)
# LOG.debug(">>> PRE val '%s'" % val)
if attr.startswith('rxfreq'):
value = chirp_common.parse_freq(str(val)) / 10
for i in range(7, -1, -1):
_mem.settings_vfo[vfo].rxfreq[i] = value % 10
value /= 10
elif attr.startswith('offset'):
value = int(float(str(val)) * 10000)
for i in range(5, -1, -1):
_mem.settings_vfo[vfo].offset[i] = value % 10
value /= 10
else:
setattr(_mem.settings_vfo[vfo], attr, int(val))
elif key.startswith('settings_bands.'):
tmp = key.split('.')
attr = tmp[2]
band = tmp[1]
setattr(_mem.settings_bands[band], attr, int(val))
elif key.startswith('settings_killswitch.'):
attr = key.split('.')[1]
if attr.endswith('dtmf'):
if str(val) == "":
setattr(_mem.settings_killswitch, attr,
[0x01, 0x02, 0x01, 0x03, 0x01, 0x04])
else:
setattr(_mem.settings_killswitch, attr,
[DTMFCHARS.index(c) for c in str(val)])
elif attr.startswith('enable'):
setattr(_mem.settings_killswitch, attr, int(val))
else:
LOG.debug(">>> TODO key '%s'" % key)
LOG.debug(">>> TODO val '%s'" % val)
elif key.startswith('custom_') or key.startswith('anicode') \
or key.startswith('memcode'):
tmp = key.split('@')
if key.startswith('anicode'):
val = [DTMFCHARS.index(c) for c in str(val)]
val += [0xFF] * (6 - len(val))
for i in range(10):
_mem[str(tmp[0])][int(tmp[2])]["ffpad"][i] = 0xFF
elif key.startswith('memcode'):
if len(str(val)) > 0:
tmpp = str(val).zfill(6)
val = self._encode_key(tmpp)
else:
val = (0xFF, 0xFF, 0xFF, 0xFF)
if key.startswith('custom_'):
for i in range(6):
_mem[str(tmp[0])][int(tmp[2])]["ffpad"][i] = 0xFF
setattr(_mem[str(tmp[0])][int(tmp[2])], str(tmp[1]), val)
elif key.startswith('management_settings'):
setattr(_mem.management_settings, key.split('.')[1], int(val))
else:
LOG.debug(">>> TODO _set_setting key '%s'" % key)
LOG.debug(">>> TODO _set_setting val '%s'" % val)
def _get_settings_bands(self):
_mem = self._memobj
ret = RadioSettingGroup('bands', 'Bands')
bands = [('136', _mem.settings_bands.band136),
('200', _mem.settings_bands.band200),
('350', _mem.settings_bands.band350),
('400', _mem.settings_bands.band400)]
for label, band in bands:
rs = RadioSetting('settings_bands.band%s.enable' % label,
'Enable Band %s' % label,
RadioSettingValueBoolean(band.enable))
ret.append(rs)
rsi = RadioSettingValueInteger(1, 1000, band.freq_low)
# if label == '136' or label == '400':
# rsi.set_mutable(False)
rs = RadioSetting("settings_bands.band%s.freq_low" % label,
"Band %s Lower Limit (MHz) (EXPERIMENTAL)"
% label, rsi)
ret.append(rs)
rsi = RadioSettingValueInteger(1, 1000, band.freq_high)
# if label == '350':
# rsi.set_mutable(False)
rs = RadioSetting("settings_bands.band%s.freq_high" % label,
"Band %s Upper Limit (MHz) (EXPERIMENTAL)"
% label, rsi)
ret.append(rs)
return ret
def _get_settings_ks(self):
_mem = self._memobj
ret = RadioSettingGroup('killswitch', 'Killswitch')
# Kill Enable/Disable enable_killsw
rsvb = RadioSettingValueBoolean(_mem.settings.enable_killsw)
ret.append(RadioSetting('settings.enable_killsw',
'Enable Killswitch', rsvb))
# Kill DTMF
cur = ''.join(
DTMFCHARS[i]
for i in _mem.settings_killswitch.kill_dtmf if int(i) < 0xF)
ret.append(
RadioSetting('settings_killswitch.kill_dtmf', 'DTMF Kill',
RadioSettingValueString(6, 6, cur,
autopad=False,
charset=DTMFCHARS)))
# Revive DTMF
cur = ''.join(
DTMFCHARS[i]
for i in _mem.settings_killswitch.revive_dtmf if int(i) < 0xF)
ret.append(
RadioSetting('settings_killswitch.revive_dtmf', 'DTMF Revive',
RadioSettingValueString(6, 6, cur,
autopad=False,
charset=DTMFCHARS)))
# Enable/Disable entire radio
rs = RadioSettingValueString(0, 255, "Can be used to revive radio")
rs.set_mutable(False)
ret.append(RadioSetting('dummy', 'Factory reserved', rs))
tmp = 1 if int(_mem.management_settings.active) > 0 else 0
ret.append(RadioSetting('management_settings.active', 'Radio Status',
RadioSettingValueList(self.ENABLERADIO,
current_index=tmp)))
return ret
def _get_settings_dtmf(self):
_mem = self._memobj
dtmf = RadioSettingGroup('dtmf', 'DTMF')
# DTMF Group
msgs = ["Allowed chars (%s)" % DTMFCHARS,
"Input from 0 to 5 characters."
]
for msg in msgs:
rsvs = RadioSettingValueString(0, 255, msg, autopad=False)
rsvs.set_mutable(False)
rs = RadioSetting('dummy_dtmf_msg_%i' % msgs.index(msg),
'Input rule %i' % int(msgs.index(msg)+1), rsvs)
dtmf.append(rs)
for i in range(1, 16):
cur = ''.join(
DTMFCHARS[i]
for i in _mem.settings_dtmfgroup[i - 1].code if int(i) < 0xF)
dtmf.append(
RadioSetting(
'settings_dtmfgroup.code@%i' % i, 'PTT ID Code %i' % i,
RadioSettingValueString(0, 5, cur,
autopad=False,
charset=DTMFCHARS)))
# DTMF Speed (on time in ms)
dtmf_speed_on = int(_mem.settings_dtmf.dtmf_speed_on)
if dtmf_speed_on > len(self.DTMF_SPEEDS)-1:
_mem.settings_dtmf.dtmf_speed_on = 0
LOG.debug('DTMF Speed On overflow')
dtmf.append(
RadioSetting(
'settings_dtmf.dtmf_speed_on', 'DTMF Speed (on time in ms)',
RadioSettingValueList(self.DTMF_SPEEDS,
current_index=dtmf_speed_on)))
# DTMF Speed (on time in ms)
dtmf_speed_off = int(_mem.settings_dtmf.dtmf_speed_off)
if dtmf_speed_off > len(self.DTMF_SPEEDS)-1:
_mem.settings_dtmf.dtmf_speed_off = 0
LOG.debug('DTMF Speed Off overflow')
dtmf.append(
RadioSetting(
'settings_dtmf.dtmf_speed_off', 'DTMF Speed (off time in ms)',
RadioSettingValueList(self.DTMF_SPEEDS,
current_index=dtmf_speed_off)))
# PTT ID
pttid = int(_mem.settings_dtmf.pttid)
if pttid > len(self.PTTID)-1:
_mem.settings_dtmf.pttid = 0
LOG.debug('PTT ID overflow')
dtmf.append(
RadioSetting(
'settings_dtmf.pttid', 'Send DTMF Code (PTT ID)',
RadioSettingValueList(self.PTTID, current_index=pttid)))
# PTT ID Delay
pttiddelay = int(_mem.settings.pttiddelay)
if pttiddelay > len(self.PTTIDDELAYS)-1:
_mem.settings.pttiddelay = 0
LOG.debug('PTT ID Delay overflow')
rsvl = RadioSettingValueList(self.PTTIDDELAYS,
current_index=pttiddelay)
dtmf.append(RadioSetting('settings.pttiddelay',
'PTT ID Delay', rsvl))
rsvl = RadioSettingValueList(self.DTMFSTLIST,
current_index=_mem.settings.dtmfst)
dtmf.append(RadioSetting('settings.dtmfst',
'DTMF Side Tone (Required for GPS ID)', rsvl))
return dtmf
def _get_settings_sidekeys(self):
_mem = self._memobj
ret = RadioSettingGroup('sidekeys', 'Side Keys')
rsvm = RadioSettingValueMap(self.SIDEKEY_VALUEMAP,
_mem.settings_sidekeys.pf2_short)
ret.append(RadioSetting('settings_sidekeys.pf2_short',
'Side key 1 (PTT2) Short press', rsvm))
rsvm = RadioSettingValueMap(self.SIDEKEY_VALUEMAP[:-1],
_mem.settings_sidekeys.pf2_long)
ret.append(RadioSetting('settings_sidekeys.pf2_long',
'Side key 1 (PTT2) Long press', rsvm))
rsvm = RadioSettingValueMap(self.SIDEKEY_VALUEMAP[:-1],
_mem.settings_sidekeys.pf3_short)
ret.append(RadioSetting('settings_sidekeys.pf3_short',
'Side key 2 (PTT3) Short press', rsvm))
rs = RadioSettingValueString(0, 255, "MONI")
rs.set_mutable(False)
ret.append(RadioSetting('dummy', 'Side key 2 (PTT3) Long press', rs))
return ret
def _get_settings_vfo(self, vfo, chan): # WIP TODO Rx/Tx tones
_mem = self._memobj
ret = RadioSettingGroup('settings_vfo@%s' % chan.lower(),
'VFO %s Settings' % chan)
rsvl = RadioSettingValueList(self.WORKMODES,
current_index=_mem.settings[
'workmode'+chan.lower()])
ret.append(RadioSetting('settings.workmode%s' % chan.lower(),
'VFO %s Workmode' % chan, rsvl))
tmp = ''.join(DTMFCHARS[i] for i in _mem.settings_vfo[
'vfo_'+chan.lower()].rxfreq if i < 0xFF)
rsvf = RadioSettingValueFloat(66, 550, chirp_common.format_freq(
int(tmp) * 10), resolution=0.00001,
precision=5)
ret.append(RadioSetting('settings_vfo.vfo_%s.rxfreq' % chan.lower(),
'Rx Frequency', rsvf))
# TODO Rx/Tx tones
rsvl = RadioSettingValueList(self.VFO_SFTD,
current_index=_mem.settings_vfo[
'vfo_'+chan.lower()].sftd)
ret.append(RadioSetting('settings_vfo.vfo_%s.sftd' % chan.lower(),
'Freq offset direction', rsvl))
tmp = ''.join(DTMFCHARS[i] for i in _mem.settings_vfo[
'vfo_'+chan.lower()].offset if i < 0xFF)
rsvf = RadioSettingValueFloat(0, 99.9999, float(tmp) / 10000)
ret.append(RadioSetting('settings_vfo.vfo_%s.offset' % chan.lower(),
'Tx Offset', rsvf))
rsvl = RadioSettingValueList(self.SIGNAL,
current_index=_mem.settings_vfo[
'vfo_'+chan.lower()].signal)
ret.append(RadioSetting('settings_vfo.vfo_%s.signal' % chan.lower(),
'PTT ID Code (S-Code)', rsvl))
rsvl = RadioSettingValueList(self.POWER_LEVELS_LIST,
current_index=_mem.settings_vfo[
'vfo_'+chan.lower()].power)
ret.append(RadioSetting('settings_vfo.vfo_%s.power' % chan.lower(),
'Tx Power', rsvl))
rsvl = RadioSettingValueList(self.FHSS_LIST,
current_index=_mem.settings_vfo[
'vfo_'+chan.lower()].fhss)
ret.append(RadioSetting('settings_vfo.vfo_%s.fhss' % chan.lower(),
'FHSS (Encryption)', rsvl))
chanwidth = ['Wide', 'Narrow']
rsvl = RadioSettingValueList(
chanwidth,
current_index=int(_mem.settings_vfo['vfo_'+chan.lower()].narrow))
ret.append(RadioSetting('settings_vfo.vfo_%s.narrow' % chan.lower(),
'Wide / Narrow', rsvl))
rsvl = RadioSettingValueList(self.TUNING_STEPS_LIST,
current_index=_mem.settings_vfo[
'vfo_'+chan.lower()].freqstep)
ret.append(RadioSetting('settings_vfo.vfo_%s.freqstep' % chan.lower(),
'Tuning Step', rsvl))
return ret
def _get_custom_channel_names(self):
_mem = self._memobj
ret = RadioSettingGroup('ccn', 'Custom Channel Names')
msgs = ["Add custom chan names to radio",
"-> Menu 09 CH-NAME",
"Allowed chars (ASCII only)",
"Input from 0 to 10 characters."
]
for msg in msgs:
rsvs = RadioSettingValueString(0, 255, msg, autopad=False)
rsvs.set_mutable(False)
rs = RadioSetting('dummy_cnames_msg_%i' % msgs.index(msg),
'Input rule %i' % int(msgs.index(msg)+1), rsvs)
ret.append(rs)
for i in range(0, len(_mem.custom_channel_names)):
tmp = ''.join([str(j) for j in _mem.custom_channel_names[i].name
if ord(str(j)) < 0xFF and ord(str(j)) > 0x00])
rsvs = RadioSettingValueString(0, 10, tmp, autopad=True,
charset=self.FULL_CHARSET_ASCII)
ret.append(RadioSetting('custom_channel_names@name@%i' % i,
'Custom Channel Name (%i)' % i, rsvs))
return ret
def _get_custom_ani_names(self):
_mem = self._memobj
ret = RadioSettingGroup('can', 'Custom ANI Names')
msgs = ["Can be used as radio id in GPS.",
"Allowed chars (ASCII only)",
"Input from 0 to 10 characters."
]
for msg in msgs:
rsvs = RadioSettingValueString(0, 255, msg, autopad=False)
rsvs.set_mutable(False)
rs = RadioSetting('dummy_caninames_msg_%i' % msgs.index(msg),
'Input rule %i' % int(msgs.index(msg)+1), rsvs)
ret.append(rs)
for i in range(0, len(_mem.custom_ani_names)):
tmp = ''.join([str(j) for j in
_mem.custom_ani_names[i].name
if ord(str(j)) < 0xFF and ord(str(j)) > 0x00])
rsvs = RadioSettingValueString(0, 10, tmp, autopad=True,
charset=self.FULL_CHARSET_ASCII)
ret.append(RadioSetting('custom_ani_names@name@%i' % i,
'Custom ANI Name (%i)' % (i+51), rsvs))
return ret
def _get_anicodes(self):
_mem = self._memobj
ret = RadioSettingGroup('ani', 'ANI Codes')
split = len(_mem.anicodes) - len(_mem.custom_ani_names)
msgs = ["Allowed chars (%s)" % DTMFCHARS,
"Input from 0 to 6 characters."
]
for msg in msgs:
rsvs = RadioSettingValueString(0, 255, msg, autopad=False)
rsvs.set_mutable(False)
rs = RadioSetting('dummy_canic_msg_%i' % msgs.index(msg),
'Input rule %i' % int(msgs.index(msg)+1), rsvs)
ret.append(rs)
for i in range(0, split):
tmp = ''.join([DTMFCHARS[int(j)] for j in
_mem.anicodes[i].anicode if int(j) < 0xFF])
# LOG.debug("ANI Code (%i) '%s'" % (i, tmp))
rsvs = RadioSettingValueString(0, 6, tmp, autopad=False,
charset=DTMFCHARS)
ret.append(RadioSetting('anicodes@anicode@%i' % i,
'ANI-ID (%i) Code' % (i+1), rsvs))
for i in range(split, len(_mem.anicodes)):
tmp = ''.join([DTMFCHARS[int(j)] for j in
_mem.anicodes[i].anicode if int(j) < 0xFF])
tmp2 = ''.join([str(j) for j in
_mem.custom_ani_names[i-split].name
if ord(str(j)) < 0xFF and ord(str(j)) > 0x00])
# LOG.debug("ANI Code (%s) (%i) '%s'" % (tmp2, i, tmp))
rsvs = RadioSettingValueString(0, 6, tmp, autopad=False,
charset=DTMFCHARS)
ret.append(RadioSetting('anicodes@anicode@%i' % i,
'ANI-ID (%s) (%i) Code' % (tmp2, i+1), rsvs))
return ret
def _get_settings_adv(self):
_mem = self._memobj
ret = RadioSettingGroup('advanced', 'Advanced')
if RT490_EXPERIMENTAL:
rsvi = RadioSettingValueInteger(
1, self._memory_size, int(_mem.settings.cha_memidx)+1)
ret.append(RadioSetting("settings.cha_memidx",
"Channel A Memory index", rsvi))
rsvi = RadioSettingValueInteger(1, self._memory_size,
int(_mem.settings.chb_memidx)+1)
ret.append(RadioSetting("settings.chb_memidx",
"Channel B Memory index", rsvi))
ret.append(RadioSetting('settings.vox', 'VOX Sensitivity',
RadioSettingValueList(self.VOX_LIST,
current_index=_mem.settings.vox)))
ret.append(
RadioSetting(
'settings.vox_delay', 'VOX Delay',
RadioSettingValueList(
self.VOXDELAYLIST,
current_index=_mem.settings.vox_delay)))
ret.append(RadioSetting('settings.tdr', 'Dual Receive (TDR)',
RadioSettingValueBoolean(_mem.settings.tdr)))
ret.append(
RadioSetting(
'settings.txundertdr', 'Tx under TDR',
RadioSettingValueList(
self.TDRTXMODES,
current_index=_mem.settings.txundertdr)))
ret.append(RadioSetting('settings.voice', 'Menu Voice Prompts',
RadioSettingValueBoolean(_mem.settings.voice)))
ret.append(
RadioSetting(
'settings.scanmode', 'Scan Mode',
RadioSettingValueList(
self.SCANMODES, current_index=_mem.settings.scanmode)))
ret.append(RadioSetting('settings.bcl', 'Busy Channel Lockout',
RadioSettingValueBoolean(_mem.settings.bcl)))
ret.append(RadioSetting('settings.display_ani', 'Display ANI ID',
RadioSettingValueBoolean(_mem.settings.display_ani)))
ret.append(RadioSetting('settings.ani_id', 'ANI ID',
RadioSettingValueList(self.ANI_IDS,
current_index=_mem.settings.ani_id)))
ret.append(
RadioSetting(
'settings.alarm_mode', 'Alarm Mode',
RadioSettingValueList(
self.ALARMMODES,
current_index=_mem.settings.alarm_mode)))
ret.append(RadioSetting('settings.alarmsound', 'Alarm Sound',
RadioSettingValueBoolean(_mem.settings.alarmsound)))
ret.append(RadioSetting('settings.fmradio', 'Enable FM Radio',
RadioSettingValueList(self.FMRADIO,
current_index=_mem.settings.fmradio)))
if RT490_EXPERIMENTAL:
tmp = _mem.settings.fmpreset / 10.0
if tmp < 65.0 or tmp > 108.0:
tmp = 80.0
ret.append(RadioSetting("settings.fmpreset", "FM Radio Freq",
RadioSettingValueFloat(65, 108, tmp, resolution=0.1,
precision=1)))
ret.append(RadioSetting('settings.kblock', 'Enable Keyboard Lock',
RadioSettingValueBoolean(_mem.settings.kblock)))
ret.append(
RadioSetting(
'settings.autolock', 'Autolock Keyboard',
RadioSettingValueList(
self.AUTOLOCK_TO,
current_index=_mem.settings.autolock)))
ret.append(
RadioSetting(
'settings.timer_menu_quit', 'Menu Exit Time',
RadioSettingValueList(
self.MENUEXIT_TO,
current_index=_mem.settings.timer_menu_quit)))
ret.append(RadioSetting('settings.enable_gps', 'Enable GPS',
RadioSettingValueBoolean(_mem.settings.enable_gps)))
ret.append(
RadioSetting(
'settings.scan_dcs', 'CDCSS Save Modes',
RadioSettingValueList(
self.SCANDCSMODES,
current_index=_mem.settings.scan_dcs)))
ret.append(RadioSetting('settings.tailnoiseclear', 'Tail Noise Clear',
RadioSettingValueBoolean(_mem.settings.tailnoiseclear)))
ret.append(
RadioSetting(
'settings.rptnoiseclear', 'Rpt Noise Clear',
RadioSettingValueList(
self.RPTNOISE,
current_index=_mem.settings.rptnoiseclear)))
ret.append(
RadioSetting(
'settings.rptnoisedelay', 'Rpt Noise Delay',
RadioSettingValueList(
self.RPTNOISE,
current_index=_mem.settings.rptnoisedelay)))
ret.append(RadioSetting('settings.rpttone', 'Rpt Tone',
RadioSettingValueList(self.RPTTONES,
current_index=_mem.settings.rpttone)))
return ret
def _get_settings_basic(self):
_mem = self._memobj
ret = RadioSettingGroup('basic', 'Basic')
ret.append(RadioSetting('settings.squelch', 'Carrier Squelch Level',
RadioSettingValueList(self.SQUELCHLVLS,
current_index=_mem.settings.squelch)))
ret.append(
RadioSetting(
'settings.savemode', 'Battery Savemode',
RadioSettingValueList(
self.SAVEMODES, current_index=_mem.settings.savemode)))
ret.append(
RadioSetting(
'settings.backlight', 'Backlight Timeout',
RadioSettingValueList(
self.BACKLIGHT_TO,
current_index=_mem.settings.backlight)))
ret.append(RadioSetting('settings.timeout', 'Timeout Timer (TOT)',
RadioSettingValueList(self.TOT_LIST,
current_index=_mem.settings.timeout)))
ret.append(RadioSetting('settings.beep', 'Beep',
RadioSettingValueBoolean(_mem.settings.beep)))
ret.append(
RadioSetting(
'settings.active_channel', 'Active Channel',
RadioSettingValueList(
self.CHANNELS,
current_index=_mem.settings.active_channel)))
ret.append(
RadioSetting(
'settings.cha_disp', 'Channel A Display Mode',
RadioSettingValueList(
self.DISPLAYMODES,
current_index=_mem.settings.cha_disp)))
ret.append(
RadioSetting(
'settings.chb_disp', 'Channel B Display Mode',
RadioSettingValueList(
self.DISPLAYMODES,
current_index=_mem.settings.chb_disp)))
ret.append(RadioSetting('settings.roger', 'Roger Beep',
RadioSettingValueBoolean(_mem.settings.roger)))
ret.append(
RadioSetting(
'settings.powermsg', 'Power Message',
RadioSettingValueList(
self.POWERMESSAGES,
current_index=_mem.settings.powermsg)))
ret.append(RadioSetting('settings.rx_time', 'Show RX Time',
RadioSettingValueBoolean(_mem.settings.rx_time)))
ret.append(RadioSetting(
'management_settings.language',
_('Language'),
RadioSettingValueList(
['English', 'Chinese'],
current_index=_mem.management_settings.language)))
return ret
def _get_memcodes(self):
ret = RadioSettingGroup('mc', 'Memory Channel Privacy Codes')
msgs = ["Only hexadecimal chars accepted.",
"Allowed chars (%s)" % self.KEY_CHARS,
"Input from 0 to 6 characters. If code",
"length is less than 6 chars it will be",
"padded with leading zeros.",
"Ex: 1D32EB or 0F12 or AB521, etc...",
"Enable Code for the Location on the",
"'Other' tab in 'Memory Properties'."
]
for msg in msgs:
rsvs = RadioSettingValueString(0, 255, msg, autopad=False)
rsvs.set_mutable(False)
rs = RadioSetting('dummy_memcodes_msg_%i' % msgs.index(msg),
'Input rule %i' % int(msgs.index(msg)+1), rsvs)
ret.append(rs)
for i in range(self._memory_size):
code = ""
if self._memobj.memcode[i].code[3] < 0xFF:
code = self._decode_key(self._memobj.memcode[i].code)
code = code.zfill(6)
rsvs = RadioSettingValueString(0, 6, code, autopad=False,
charset=self.KEY_CHARS)
rs = RadioSetting('memcode@code@%i' % i,
'Memory Location (%i) Privacy Code' %
int(i+1), rsvs)
ret.append(rs)
return ret
def get_settings(self):
radio_settings = []
basic = self._get_settings_basic()
radio_settings.append(basic)
adv = self._get_settings_adv()
radio_settings.append(adv)
vfoa = self._get_settings_vfo(self._memobj.settings_vfo.vfo_a, 'A')
radio_settings.append(vfoa)
vfob = self._get_settings_vfo(self._memobj.settings_vfo.vfo_b, 'B')
radio_settings.append(vfob)
sk = self._get_settings_sidekeys()
radio_settings.append(sk)
dtmf = self._get_settings_dtmf()
radio_settings.append(dtmf)
ccn = self._get_custom_channel_names()
radio_settings.append(ccn)
can = self._get_custom_ani_names()
radio_settings.append(can)
ani = self._get_anicodes()
radio_settings.append(ani)
mcodes = self._get_memcodes()
radio_settings.append(mcodes)
if RT490_EXPERIMENTAL:
ks = self._get_settings_ks()
radio_settings.append(ks)
bands = self._get_settings_bands()
radio_settings.append(bands)
top = RadioSettings(*radio_settings)
return top
def get_raw_memory(self, number):
return '\n'.join([repr(self._memobj.memory[number]),
repr(self._memobj.memname[number])])
# TODO Add Code when RadioSettingValueString is fixed
def _get_extra(self, _mem, num):
group = RadioSettingGroup('extra', 'Extra')
# LOG.debug("Get extra %i" % num)
s = RadioSetting('bcl', 'Busy Channel Lockout',
RadioSettingValueBoolean(_mem.bcl))
group.append(s)
dcp = int(_mem.dcp)
if dcp > len(self.FHSS_LIST)-1:
_mem.dcp = cur = 0
LOG.debug('DCP ID / FHSS overflow for channel %d' % num)
cur = self.FHSS_LIST[dcp]
s = RadioSetting('dcp', 'FHSS (Encryption)',
RadioSettingValueList(self.FHSS_LIST, cur))
group.append(s)
# Does not work, no error, why ??? TODO
"""
code = ""
if self._memobj.memcode[num-1].code[3] < 0xFF:
code = self._decode_key(self._memobj.memcode[num-1].code)
code = code.zfill(6)
LOG.debug('CODE "%s"' % code)
s = RadioSetting('dcp_code', 'DCP code',
RadioSettingValueString(0, 6, code,
autopad=False,
charset=self.KEY_CHARS))
group.append(s) """
pttid = int(_mem.pttid)
if pttid > len(self.PTTID)-1:
_mem.pttid = cur = 0
LOG.debug('PTTID overflow for channel %d' % num)
cur = self.PTTID[pttid]
s = RadioSetting('pttid', 'Send DTMF Code (PTT ID)',
RadioSettingValueList(self.PTTID, cur))
group.append(s)
cur = self.SIGNAL[int(_mem.signal)]
s = RadioSetting('signal', 'PTT ID Code (S-Code)',
RadioSettingValueList(self.SIGNAL, cur))
group.append(s)
s = RadioSetting('learn',
'Use Memory Privacy Code as Tx/Rx DCS (Learn)',
RadioSettingValueBoolean(_mem.learn))
group.append(s)
return group
# TODO Add Code when RadioSettingValueString is fixed
def _set_extra(self, _mem, mem):
# memidx = mem.number - 1 # commented because not used
_mem.bcl = int(mem.extra['bcl'].value)
_mem.dcp = int(mem.extra['dcp'].value)
_mem.pttid = int(mem.extra['pttid'].value)
_mem.signal = int(mem.extra['signal'].value)
# self._memobj.memcode[mem.number].code = \
# self._encode_key(mem.extra['dcp_code'].value)
if (int(mem.extra['learn'].value) > 0) and \
(self._memobj.memcode[mem.number-1].code[3] == 0xA0):
_mem.learn = 1
elif (int(mem.extra['learn'].value) > 0) and \
(self._memobj.memcode[mem.number-1].code[3] != 0xA0):
_mem.learn = 0
raise InvalidValueError(
"Use Memory Privacy Code as Tx/Rx DCS (Learn) requires "
"that a memory code has been previously set for this memory."
"Go in 'Settings' -> 'Memory Channel Privacy Codes' and set "
"a code for the current memory before enabling 'Learn'.")
else:
_mem.learn = 0
def _is_txinh(self, _mem):
raw_tx = b""
for i in range(0, 4):
raw_tx += _mem.txfreq[i].get_raw()
return raw_tx == b"\xFF\xFF\xFF\xFF"
def get_memory(self, num):
memidx = num - 1
_mem = self._memobj.memory[memidx]
_nam = self._memobj.memname[memidx]
mem = chirp_common.Memory()
mem.number = num
if int(_mem.rxfreq) == 166666665:
mem.empty = True
return mem
try:
mem.name = bytes(
_nam.name.get_raw()).rstrip(b'\xFF').decode('gb2312')
except UnicodeDecodeError:
if _nam.name[0].get_raw() != b'\xFF':
LOG.error('Unable to decode name buffer %r',
_nam.name.get_raw())
mem.name = ''
mem.freq = int(_mem.rxfreq) * 10
offset = (int(_mem.txfreq) - int(_mem.rxfreq)) * 10
if self._is_txinh(_mem) or _mem.tx_enable == 0:
mem.duplex = 'off'
# _mem.txfreq = _mem.rxfreq # TODO REMOVE (force fix broken saves)
elif offset == 0:
mem.duplex = ''
mem.offset = 0
elif abs(offset) < 100000000:
mem.duplex = offset < 0 and '-' or '+'
mem.offset = abs(offset)
else:
mem.duplex = 'split'
mem.offset = int(_mem.txfreq) * 10
mem.power = self.POWER_LEVELS[_mem.power]
if _mem.unknown3_0 and _mem.narrow:
mem.mode = 'NAM'
elif _mem.unknown3_0 and not _mem.narrow:
mem.mode = 'AM'
elif not _mem.unknown3_0 and _mem.narrow:
mem.mode = 'NFM'
elif not _mem.unknown3_0 and not _mem.narrow:
mem.mode = 'FM'
else:
LOG.exception('Failed to get mode for %i' % num)
mem.skip = '' if _mem.scan else 'S'
# LOG.warning('got txtone: %s' % repr(self._decode_tone(_mem.txtone)))
# LOG.warning('got rxtone: %s' % repr(self._decode_tone(_mem.rxtone)))
txtone = self._decode_tone(_mem.txtone)
rxtone = self._decode_tone(_mem.rxtone)
chirp_common.split_tone_decode(mem, txtone, rxtone)
try:
mem.extra = self._get_extra(_mem, num)
except Exception:
LOG.exception('Failed to get extra for %i' % num)
return mem
def set_memory(self, mem):
memidx = mem.number - 1
_mem = self._memobj.memory[memidx]
_nam = self._memobj.memname[memidx]
if mem.empty:
_mem.set_raw(b'\xff' * 16)
_nam.set_raw(b'\xff' * 16)
return
if int(_mem.rxfreq) == 166666665:
LOG.debug('Initializing new memory %i' % memidx)
_mem.set_raw(b'\x00' * 16)
_nam.name.set_raw(mem.name.encode('gb2312').ljust(12, b'\xFF')[:12])
_mem.rxfreq = mem.freq / 10
_mem.tx_enable = 1
if mem.duplex == '':
_mem.txfreq = mem.freq / 10
elif mem.duplex == 'split':
_mem.txfreq = mem.offset / 10
elif mem.duplex == 'off':
_mem.tx_enable = 0
_mem.txfreq = mem.freq / 10 # Optional but keeps compat with
# vendor software
elif mem.duplex == '-':
_mem.txfreq = (mem.freq - mem.offset) / 10
elif mem.duplex == '+':
_mem.txfreq = (mem.freq + mem.offset) / 10
else:
raise errors.RadioError('Unsupported duplex mode %r' % mem.duplex)
txtone, rxtone = chirp_common.split_tone_encode(mem)
# LOG.warning('tx tone is %s' % repr(txtone))
# LOG.warning('rx tone is %s' % repr(rxtone))
_mem.txtone = self._encode_tone(*txtone)
_mem.rxtone = self._encode_tone(*rxtone)
try:
_mem.power = self.POWER_LEVELS.index(mem.power)
except ValueError:
_mem.power = 0
if int(_mem.rxfreq) < 30000000:
_mem.unknown3_0 = mem.mode in ['AM', 'NAM']
else:
_mem.unknown3_0 = 0
_mem.narrow = mem.mode[0] == 'N'
_mem.scan = mem.skip != 'S'
if mem.extra:
self._set_extra(_mem, mem)
def sync_out(self):
try:
do_upload(self)
except errors.RadioError:
# Pass through any real errors we raise
raise
except Exception:
# If anything unexpected happens, make sure we raise
# a RadioError and log the problem
LOG.exception('Unexpected error during upload')
raise errors.RadioError('Unexpected error communicating '
'with the radio')
def sync_in(self):
"""Download from radio"""
try:
data = do_download(self)
except errors.RadioError:
# Pass through any real errors we raise
raise
except Exception:
# If anything unexpected happens, make sure we raise
# a RadioError and log the problem
LOG.exception('Unexpected error during download')
raise errors.RadioError('Unexpected error communicating '
'with the radio')
self._mmap = data
self.process_mmap()
def process_mmap(self):
self._memobj = bitwise.parse(MEM_FORMAT_RT490 %
{"memsize": self._memory_size},
self._mmap)
def get_features(self): # GOOD ?
rf = chirp_common.RadioFeatures()
rf.has_settings = True
rf.has_bank = False
rf.has_cross = True
rf.has_rx_dtcs = True
rf.has_dtcs_polarity = True
rf.has_tuning_step = False
rf.can_odd_split = True
rf.has_name = True
rf.valid_name_length = 12
# We implement filter_name() ourselves
rf.valid_characters = ''
rf.valid_skips = ["", "S"]
rf.valid_tmodes = ["", "Tone", "TSQL", "DTCS", "Cross"]
rf.valid_cross_modes = ["Tone->Tone", "DTCS->", "->DTCS", "Tone->DTCS",
"DTCS->Tone", "->Tone", "DTCS->DTCS"]
rf.valid_power_levels = self.POWER_LEVELS
rf.valid_duplexes = ["", "+", "-", "split", "off"]
rf.valid_modes = ["FM", "NFM", "AM", "NAM"]
rf.memory_bounds = (1, 256)
rf.valid_tuning_steps = self.TUNING_STEPS
rf.valid_bands = [(108000000, 136000000),
(136000000, 180000000),
(200000000, 260000000),
(350000000, 400000000),
(400000000, 520000000)]
return rf
@classmethod
def get_prompts(cls):
rp = chirp_common.RadioPrompts()
rp.pre_upload = _("This driver is in development and should be "
"considered as experimental.")
rp.experimental = _("This driver is in development and should be "
"considered as experimental.")
rp.info = _("This driver is in development and should be considered "
"as experimental.")
return rp
def _encode_key(self, key):
arr = bytearray(4)
arr[3] = 160
arr[2] = self.KEY_CHARS.index(key[0]) # << 4
arr[2] = arr[2] << 4
arr[2] |= self.KEY_CHARS.index(key[1])
arr[1] = self.KEY_CHARS.index(key[2]) # << 4
arr[1] = arr[1] << 4
arr[1] |= self.KEY_CHARS.index(key[3])
arr[0] = self.KEY_CHARS.index(key[4]) # << 4
arr[0] = arr[0] << 4
arr[0] |= self.KEY_CHARS.index(key[5])
return arr
def _decode_key(self, key):
ret = ""
if key[3] == 0xA0:
ret += self.KEY_CHARS[key[2] >> 4]
ret += self.KEY_CHARS[key[2] & 0xF]
ret += self.KEY_CHARS[key[1] >> 4]
ret += self.KEY_CHARS[key[1] & 0xF]
ret += self.KEY_CHARS[key[0] >> 4]
ret += self.KEY_CHARS[key[0] & 0xF]
LOG.debug('DCP Code: "%s"' % ret)
return ret
def _decode_tone(self, toneval):
if toneval in (0, 0xFFFF):
# LOG.debug('no tone value: %s' % toneval)
return None, None, None
elif toneval < 670:
toneval = toneval - 1
index = toneval % len(DTCS_CODES)
if index != int(toneval):
pol = 'R'
# index -= 1
else:
pol = 'N'
return 'DTCS', DTCS_CODES[index], pol
else:
return 'Tone', toneval / 10.0, 'N'
def _encode_tone(self, mode, val, pol):
if not mode:
return 0x0000
elif mode == 'Tone':
return int(val * 10)
elif mode == 'DTCS':
index = DTCS_CODES.index(val)
if pol == 'R':
index += len(DTCS_CODES)
index += 1
# LOG.debug('Encoded dtcs %s/%s to %04x' % (val, pol, index))
return index
else:
raise errors.RadioError('Unsupported tone mode %r' % mode)
@directory.register
class MML8629Radio(RT490Radio):
"""MMLradio JC-8629"""
VENDOR = "MMLradio"
MODEL = "JC-8629"
@directory.register
class JJCC8629Radio(RT490Radio):
"""JJCC JC-8629"""
VENDOR = "JJCC"
MODEL = "JC-8629"
@directory.register
class SocotranJC8629Radio(RT490Radio):
"""Socotran JC-8629"""
VENDOR = "Socotran"
MODEL = "JC-8629"
@directory.register
class SocotranFB8629Radio(RT490Radio):
"""Socotran FB-8629"""
VENDOR = "Socotran"
MODEL = "FB-8629"
@directory.register
class Jianpai8629Radio(RT490Radio):
"""Jianpai 8800 Plus"""
VENDOR = "Jianpai"
MODEL = "8800_Plus"
@directory.register
class Boristone8RSRadio(RT490Radio):
"""Boristone 8RS"""
VENDOR = "Boristone"
MODEL = "8RS"
@directory.register
class AbbreeAR869Radio(RT490Radio):
"""Abbree AR-869"""
VENDOR = "Abbree"
MODEL = "AR-869"
@directory.register
class HamGeekHG590Radio(RT490Radio):
"""HamGeek HG-590"""
VENDOR = "HamGeek"
MODEL = "HG-590"
IMHEX_DESC = """
// Perfect tool for binary reverse engineering
// https://github.com/WerWolv/ImHex
// Below is the pattern
"""
IMHEX_PATTERN = """
struct memory { // Memory settings
u8 rxfreq[4];
u8 txfreq[4];
u16 rxtone;
u16 txtone;
u8 signal; // int 0->14, Signal 1->15
u8 pttid; // ['OFF', 'BOT', 'EOT', 'Both']
u8 dcp_power; // POWER_LEVELS
u8 unknown3_0_narrow_unknown3_1_bcl_scan_tx_enable_learn; // bool ??? TODO
};
struct memname {
char name[12];
padding[4];
};
struct memcode {
u8 code[4];
};
struct custom_ani_names {
char name[12];
padding[4];
};
struct anicodes {
u8 anicode[6];
padding[10];
};
struct custom_channel_names {
char name[12];
padding[4];
};
bitfield workmode {
b : 4;
a : 4;
};
struct settings {
u8 squelch; // 0: int 0 -> 9
u8 savemode; // 1: ['OFF', 'Normal', 'Super', 'Deep']
u8 vox; // 2: off=0, 1 -> 9
u8 backlight; // 3: ['OFF', '5s', '15s', '20s', '30s', '1m', '2m',
// '3m']
u8 tdr; // 4: bool
u8 timeout; // 5: n*30seconds, 0-240s
u8 beep; // 6: bool
u8 voice; // 7: bool
u8 byte_not_used_10; // 8: Always 1
u8 dtmfst; // 9: ['OFF', 'KB Side Tone', 'ANI Side Tone',
// 'KB ST + ANI ST']
u8 scanmode; // 10: ['TO', 'CO', 'SE']
u8 pttid; // 11: ['OFF', 'BOT', 'EOT', 'Both']
u8 pttiddelay; // 12: ['0', '100ms', '200ms', '400ms', '600ms',
// '800ms', '1000ms']
u8 cha_disp; // 13: ['Name', 'Freq', 'Channel ID']
u8 chb_disp; // 14: ['Name', 'Freq', 'Channel ID']
u8 bcl; // 15: bool
u8 autolock; // 0: ['OFF', '5s', '10s', 15s']
u8 alarm_mode; // 1: ['Site', 'Tone', 'Code']
u8 alarmsound; // 2: bool
u8 txundertdr; // 3: ['OFF', 'A', 'B']
u8 tailnoiseclear; // 4: [off, on]
u8 rptnoiseclear; // 5: n*100ms, 0-1000
u8 rptnoisedelay; // 6: n*100ms, 0-1000
u8 roger; // 7: bool
u8 active_channel; // 8: 0 or 1
u8 fmradio; // 9: boolean, inverted
workmode _workmode; // 10: up ['VFO', 'CH Mode']
u8 kblock; // 11: bool // TODO TEST WITH autolock
u8 powermsg; // 12: 0=Image / 1=Voltage
u8 byte_not_used_21; // 13: Always 0
u8 rpttone; // 14: ['1000Hz', '1450Hz', '1750Hz', '2100Hz']
u8 byte_not_used_22; // 15: pad with xFF
u8 vox_delay; // 0: [str(float(a)/10)+'s' for a in range(5,21)]
// '0.5s' to '2.0s'
u8 timer_menu_quit; // 1: ['5s', '10s', '15s', '20s', '25s', '30s',
// '35s', '40s', '45s', '50s', '60s']
u8 byte_not_used_30; // 2: pad with xFF
u8 byte_not_used_31; // 3: pad with xFF
u8 enable_killsw; // 4: bool
u8 display_ani; // 5: bool
u8 byte_not_used_32; // 6: pad with xFF
u8 enable_gps; // 7: bool
u8 scan_dcs; // 8: ['All', 'Receive', 'Transmit']
u8 ani_id; // 9: int 0-59 (ANI 1-60)
u8 rx_time; // 10: bool
padding[5]; // 11: Pad xFF
u8 cha_memidx; // 0: Memory index when channel A use memories
u8 byte_not_used_40;
u8 chb_memidx; // 2: Memory index when channel B use memories
u8 byte_not_used_41;
padding[10];
u16 fmpreset;
};
struct settings_vfo_chan {
u8 rxfreq[8]; // 0
u16 rxtone; // 8
u16 txtone; // 10
u16 byte_not_used0; // 12 Pad xFF
u8 sftd_signal; // 14 int 0->14, Signal 1->15
u8 byte_not_used1; // 15 Pad xFF
u8 power; // 16:0 POWER_LEVELS
u8 fhss_narrow; // 17 bool true=NFM false=FM
u8 byte_not_used2; // 18 Pad xFF but received 0x00 ???
u8 freqstep; // 19:3 ['2.5 KHz', '5.0 KHz', '6.25 KHz',
// '10.0 KHz', '12.5 KHz', '20.0 KHz',
// '25.0 KHz', '50.0 KHz']
u8 byte_not_used3; // 20:4 Pad xFF but received 0x00 ??? TODO
u8 offset[6]; // 21:5 Freq NN.NNNN (without the dot) TEST TEST
u8 byte_not_used4; // 27:11 Pad xFF
u8 byte_not_used5; // 28 Pad xFF
u8 byte_not_used6; // 29 Pad xFF
u8 byte_not_used7; // 30 Pad xFF
u8 byte_not_used8; // 31:15 Pad xFF
};
struct settings_vfo {
settings_vfo_chan vfo_a;
settings_vfo_chan vfo_b;
};
struct settings_sidekeys { // Values from Radio
u8 pf2_short; // { '7': 'FM', '10': 'Tx Power', '28': 'Scan',
// '29': 'Search, '1': 'PPT B' }
u8 pf2_long; // { '7': 'FM', '10': 'Tx Power', '28': 'Scan',
// '29': 'Search' }
u8 pf3_short; // { '7': 'FM', '10': 'Tx Power', '28': 'Scan',
// '29': 'Search' }
u8 ffpad; // Pad xFF
};
struct dtmfcode {
u8 code[5]; // 5 digits DTMF
padding[11]; // Pad xFF
};
struct settings_dtmf { // @15296+3x16
u8 byte_not_used1; // 0: Pad xFF
u8 byte_not_used2; // 1: Pad xFF
u8 byte_not_used3; // 2: Pad xFF
u8 byte_not_used4; // 3: Pad xFF
u8 byte_not_used5; // 4: Pad xFF
u8 unknown_dtmf; // 5: 0 TODO ???? wtf is alarmcode/alarmcall TODO
u8 pttid; // 6: [off, BOT, EOT, Both]
u8 dtmf_speed_on; // 7: ['50ms', '100ms', '200ms', '300ms', '500ms']
u8 dtmf_speed_off; // 8:0 ['50ms', '100ms', '200ms', '300ms', '500ms']
};
struct settings_dtmf_global {
dtmfcode settings_dtmfgroup[15];
settings_dtmf _settings_dtmf;
};
struct settings_killswitch {
u8 kill_dtmf[6]; // 0: Kill DTMF
padding[2]; // Pad xFF
u8 revive_dtmf[6]; // 8: Revive DTMF
padding[2]; // Pad xFF
};
struct management_settings {
u8 unknown_data_0[16];
u8 unknown_data_1;
u8 active; // Bool radio killed (killed=0, active=1)
padding[46];
};
struct band {
u8 enable; // 0 bool / enable-disable Tx on band
u8 freq_low[2]; // 1 lowest band frequency
u8 freq_high[2]; // 3 highest band frequency
};
struct settings_bands {
band band136; // 0 Settings for 136MHz band
band band400; // 5 Settings for 400MHz band
band band200; // 10 Settings for 200MHz band
padding[1]; // 15
band band350; // 0 Settings for 350MHz band
padding[43];// 5
};
memory mem[256] @ 0x0000;
memname mname[256] @ 0x1000;
memcode mcode[256] @ 0x2000;
custom_ani_names caninames[10] @ 0x3400;
anicodes anic[60] @ 0x3500;
custom_channel_names ccnames[10] @ 0x3900;
settings _settings @ 0x3A00;
settings_vfo _settings_vfo @ 0x3A40;
settings_sidekeys _settings_sidekeys @ 0x3A80;
settings_dtmf_global _settings_dtmf_global @ 0x3B00;
settings_killswitch _settings_killswitch @ 0x3C00;
management_settings _management_settings @ 0x3F80;
settings_bands _settings_bands @ 0x3FC0;
"""
chirp-20261030/chirp/drivers/radtel_rt620.py 0000644 0001750 0001750 00000035336 14724616660 017425 0 ustar hibby hibby # Copyright 2024:
# * Pavel Moravec, OK2MOP
#
# 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 .
import logging
from chirp import chirp_common, directory
from chirp.settings import RadioSettingGroup, RadioSetting, \
RadioSettingValueBoolean, RadioSettingValueList
from chirp.drivers import baofeng_uv17Pro, baofeng_uv17
LOG = logging.getLogger(__name__)
@directory.register
class RT620(baofeng_uv17.UV17):
"""Radtel RT-620"""
VENDOR = "Radtel"
MODEL = "RT-620"
_radio_memsize = 0x2efff
_has_workmode_support = True
_has_gps = True
_vfoscan = True
# Top key is referenced in CPS but does not exist in GPS model
_has_top_key = False
_has_pilot_tone = True
STEPS = [2.5, 5.0, 6.25, 10.0, 12.5, 20.0, 25.0, 50.0]
# Blocks 16-19 - channels, 24-26 channel names, 4 settings,
# 6 - secondary settings, identity codes,
# 2 - calibration, 1 - empty radio param
BLOCK_O_READ = baofeng_uv17.UV17.BLOCK_O_READ + [2] # back up calibration
_airband = (108000000, 135999999)
_vhf_range = (136000000, 299999999)
_uhf_range = (300000000, 559999999)
VALID_BANDS = [_airband, _vhf_range, _uhf_range]
_fingerprint = b"\x06" + b"PROGR6F"
_magics2 = [(b"\x06", 1),
(b"\xFF\xFF\xFF\xFF\x0CPROGR6F", 1),
(b"\02", 8),
(b"\x06", 1)]
CHANNELS = 999
MODES = ["NFM", "FM", "WFM"]
POWER_LEVELS = [chirp_common.PowerLevel("Low", watts=1.00),
chirp_common.PowerLevel("Mid", watts=5.00),
chirp_common.PowerLevel("High", watts=10.00)]
LIST_MODE = ["VFO", "Channel Number",
"Frequency + Channel Number", "Name + Channel Number"]
LIST_BEEP = ["Off", "On"]
LIST_VOX_LEVEL = baofeng_uv17Pro.LIST_VOX_LEVEL
LIST_ALARMMODE = baofeng_uv17Pro.LIST_ALARMMODE
LIST_PILOT_TONE = baofeng_uv17Pro.LIST_PILOT_TONE
LIST_VOX_DELAY_TIME = ["Off"] + ["%s ms" %
x for x in range(500, 2100, 100)]
LIST_NOAA = ["NOAA OFF", "NOAA Forecast",
"NOAA Alarm", "NOAA Both"] # NOAA here
LIST_100ms = ["%s ms" % x for x in range(100, 1100, 100)]
LIST_AB = ["A", "B"]
LIST_BRIGHTNESS = ["%i" % x for x in range(1, 6)]
LIST_MENU_QUIT_TIME = ["Off"] + ["%s sec" % x for x in range(1, 31)]
LIST_KEYS = ["None", "Monitor", "Sweep", "Scan", "Voltage",
"Emergency", "Scrambler", "FM Radio", "Compander"]
MEM_DEFS = """
struct channel {
lbcd rxfreq[4];
lbcd txfreq[4];
ul16 rxtone;
ul16 txtone;
u8 unknown12_7:1,
bcl:1,
pttid:2,
wide:2,
lowpower:2;
u8 unknown13_5:3,
compander:1,
unknown_13_2:2,
scrambler:2;
u8 scode:4,
unknown14_1:3,
scan:1;
u8 step;
};
struct channelname {
char name[11];
};
struct settings {
u8 powerondistype;
u8 unknown0[15];
char boottext1[10];
u8 unknown1[6];
char boottext2[10];
u8 unknown2[22];
u8 tot;
u8 voxdlytime;
u8 vox;
u8 powersave: 4,
unknown3:1,
chedit:1,
voice: 1,
voicesw: 1;
u8 backlight;
u8 beep:1,
autolock:1,
unknown_5b5:1,
noaa:2,
tail:1,
dtmfst:2;
u8 fminterrupt:1,
dualstandby:1,
roger:1,
alarmmode:2,
alarmtone:1,
fmenable:1,
absel:1;
u8 tailrevert;
u8 taildelay;
u8 tone;
u8 backlightbr;
u8 squelch;
u8 chbsquelch;
u8 chbdistype;
u8 chadistype;
u8 menuquittime;
u8 sk1short;
u8 sk1long;
u8 sk2short;
u8 sk2long;
u8 topshort;
u8 toplong;
u8 scanmode; // Fake Scanmode to satisfy parent method
u8 unknown8[5];
};
struct ani {
u8 unknown[5];
u8 code[5];
};
struct pttid {
u8 code[5];
};
struct proglocks {
u8 writelock; //0x00 - off, 0xA5 - on
u8 readlock;
char writepass[8];
char readpass[8];
u8 empty[13];
char unknowncode[6]; //Kill code maybe
};
struct gps {
u8 state;
u8 tzadd12;
};
struct dtmfen {
u8 delay;
u8 digit;
u8 interval;
};
"""
MEM_LAYOUT = """
struct {
ul16 channels;
ul16 ch_a;
ul16 ch_b;
} chsel;
#seekto 0x0030;
struct {
struct channel mem[252];
} mem1;
#seek 0x10;
struct {
struct channel mem[255];
} mem2;
#seek 0x10;
struct {
struct channel mem[255];
} mem3;
#seek 0x10;
struct {
struct channel mem[237];
} mem4;
#seekto 0x7000;
struct settings settings;
#seekto 0x0010;
struct {
struct channel a;
struct channel b;
} vfo;
#seekto 0x4000;
struct channelname names1[372];
#seek 0x4;
struct channelname names2[372];
#seek 0x4;
struct channelname names3[255];
#seekto 0x7100;
struct gps gps;
#seekto 0x7069;
struct proglocks locks;
#seekto 0x8000;
struct pttid pttid[15];
struct ani ani;
#seekto 0x8061;
struct dtmfen dtmf;
"""
MEM_FORMAT = MEM_DEFS + MEM_LAYOUT
REMOVE_SETTINGS = ['settings.scanmode']
def remove_extras(self, settings):
for element in settings:
if not isinstance(element, RadioSetting):
settings.remove(element)
settings.append(self.remove_extra_items(element))
settings.sort()
def remove_extra_items(self, group):
tmp = RadioSettingGroup(group.get_name(), group.get_shortname())
for element in group:
name = element.get_name()
if not isinstance(element, RadioSetting):
tmp.append(self.remove_extra_items(element))
elif name not in self.REMOVE_SETTINGS:
tmp.append(element)
return tmp
def get_settings(self):
top = super().get_settings()
_mem = self._memobj
self.remove_extras(top)
extra = RadioSettingGroup("extra", "Extra Settings")
self.get_settings_extra(extra, _mem)
top.append(extra)
if self._has_gps:
gps = RadioSettingGroup("gps", "GPS Settings")
rs = RadioSetting("gps.state", "GPS Enable",
RadioSettingValueBoolean(_mem.gps.state))
gps.append(rs)
rs = RadioSetting("gps.tzadd12", "GPS Timezone",
RadioSettingValueList(
baofeng_uv17Pro.LIST_GPS_TIMEZONE,
current_index=_mem.gps.tzadd12))
gps.append(rs)
top.append(gps)
return top
def get_settings_extra(self, extra, _mem):
rs = RadioSetting("settings.absel", "Selected Channel",
RadioSettingValueList(
self.LIST_AB, current_index=_mem.settings.absel))
extra.append(rs)
if _mem.settings.chbsquelch >= len(self.SQUELCH_LIST):
val = 0x00
else:
val = _mem.settings.chbsquelch
rs = RadioSetting("settings.chbsquelch", "Channel B Squelch",
RadioSettingValueList(
self.SQUELCH_LIST, current_index=val))
extra.append(rs)
# Settings menu 4
rs = RadioSetting("settings.vox", "Vox Level",
RadioSettingValueList(
self.LIST_VOX_LEVEL,
current_index=_mem.settings.vox))
extra.append(rs)
# Settings menu 5
rs = RadioSetting("settings.voxdlytime", "Vox Delay Time",
RadioSettingValueList(
self.LIST_VOX_DELAY_TIME,
current_index=_mem.settings.voxdlytime))
extra.append(rs)
# Settings menu 12
rs = RadioSetting("settings.backlightbr", "Backlight Brightnes",
RadioSettingValueList(
self.LIST_BRIGHTNESS,
current_index=_mem.settings.backlightbr))
extra.append(rs)
rs = RadioSetting("settings.chedit", "Allow Channel Editing",
RadioSettingValueBoolean(_mem.settings.chedit))
extra.append(rs)
# Settings menu 22
rs = RadioSetting("settings.tail", "CTCSS Tail Revert",
RadioSettingValueBoolean(_mem.settings.tail))
extra.append(rs)
rs = RadioSetting("settings.tailrevert", "Repeater Tail Revert Time",
RadioSettingValueList(
self.LIST_100ms,
current_index=_mem.settings.tailrevert))
extra.append(rs)
rs = RadioSetting("settings.taildelay", "Repeater Tail Delay Time",
RadioSettingValueList(
self.LIST_100ms,
current_index=_mem.settings.taildelay))
extra.append(rs)
rs = RadioSetting("settings.fminterrupt", "FM radio Interruption",
RadioSettingValueBoolean(_mem.settings.fminterrupt))
extra.append(rs)
# Settings menu 21
rs = RadioSetting("settings.alarmmode", "Alarm Mode",
RadioSettingValueList(
self.LIST_ALARMMODE,
current_index=_mem.settings.alarmmode))
extra.append(rs)
rs = RadioSetting("settings.alarmtone", "Sound Alarm",
RadioSettingValueBoolean(_mem.settings.alarmtone))
extra.append(rs)
rs = RadioSetting("settings.menuquittime", "Menu Quit Timer",
RadioSettingValueList(
self.LIST_MENU_QUIT_TIME,
current_index=_mem.settings.menuquittime))
extra.append(rs)
if self._has_pilot_tone:
rs = RadioSetting("settings.tone", "Pilot Tone",
RadioSettingValueList(
self.LIST_PILOT_TONE,
current_index=_mem.settings.tone))
extra.append(rs)
# Settings menu 27
if _mem.settings.sk1short >= len(self.LIST_KEYS):
val = 0x00
else:
val = _mem.settings.sk1short
rs = RadioSetting("settings.sk1short", "Side Key 1 Short Press",
RadioSettingValueList(
self.LIST_KEYS, current_index=val))
extra.append(rs)
# Settings menu 28
if _mem.settings.sk1long >= len(self.LIST_KEYS):
val = 0x00
else:
val = _mem.settings.sk1long
rs = RadioSetting("settings.sk1long", "Side Key 1 Long Press",
RadioSettingValueList(
self.LIST_KEYS, current_index=val))
extra.append(rs)
# Settings menu 29
if _mem.settings.sk2short >= len(self.LIST_KEYS):
val = 0x00
else:
val = _mem.settings.sk2short
rs = RadioSetting("settings.sk2short", "Side Key 2 Short Press",
RadioSettingValueList(
self.LIST_KEYS, current_index=val))
extra.append(rs)
# Settings menu 30
if _mem.settings.sk2long >= len(self.LIST_KEYS):
val = 0x00
else:
val = _mem.settings.sk2long
rs = RadioSetting("settings.sk2long", "Side Key 2 Long Press",
RadioSettingValueList(
self.LIST_KEYS, current_index=val))
extra.append(rs)
# Official CPS sets this, but radio does not have top key
# Probably the non-GPS version or some other clone uses it so
# I have included it here when/if it will be needed
if self._has_top_key:
if _mem.settings.topshort >= len(self.LIST_KEYS):
val = 0x00
else:
val = _mem.settings.topshort
rs = RadioSetting("settings.topshort", "Top Key Short Press",
RadioSettingValueList(
self.LIST_KEYS, current_index=val))
extra.append(rs)
# Settings menu 30
if _mem.settings.toplong >= len(self.LIST_KEYS):
val = 0x00
else:
val = _mem.settings.toplong
rs = RadioSetting("settings.toplong", "Top Key Long Press",
RadioSettingValueList(
self.LIST_KEYS, current_index=val))
extra.append(rs)
# NOAA menu
rs = RadioSetting("settings.noaa", "NOAA Weather mode",
RadioSettingValueList(
self.LIST_NOAA,
current_index=_mem.settings.noaa))
extra.append(rs)
def _get_raw_memory(self, number):
if number == -1:
return self._memobj.vfo.b
elif number == -2:
return self._memobj.vfo.a
else:
return super()._get_raw_memory(number)
def get_memory(self, number):
mem = super().get_memory(number)
_mem = self._get_raw_memory(number)
if hasattr(mem.extra, "keys") and \
"compander" not in mem.extra.keys():
rs = RadioSetting("compander", "Compander",
RadioSettingValueBoolean(_mem.compander))
mem.extra.append(rs)
mem.tuning_step = self.STEPS[_mem.step] or self.STEPS[0]
if _mem.wide < len(self.MODES):
mem.mode = self.MODES[_mem.wide]
return mem
def set_memory(self, mem):
_mem = self._get_raw_memory(mem.number)
super().set_memory(mem)
_mem.step = self.STEPS.index(mem.tuning_step)
_mem.wide = self.MODES.index(mem.mode)
if _mem.rxtone == 0:
_mem.rxtone = 0xFFFF
if _mem.txtone == 0:
_mem.txtone = 0xFFFF
def get_features(self):
rf = super().get_features()
rf.has_tuning_step = True
rf.has_bank = False
return rf
chirp-20261030/chirp/drivers/radtel_rt880g.py 0000644 0001750 0001750 00000255215 15257672455 017612 0 ustar hibby hibby # Copyright 2023 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 .
import struct
import logging
from enum import Enum
from chirp import chirp_common, directory, memmap, checksum
from chirp import bitwise, errors, util
from chirp.settings import RadioSetting, RadioSettingGroup, \
RadioSettingValueInteger, RadioSettingValueList, \
RadioSettingValueBoolean, RadioSettingValueString, \
RadioSettingValueFloat, RadioSettings
from chirp.drivers.iradio_common import enter_programming_mode, \
exit_programming_mode
LOG = logging.getLogger(__name__)
MEM_FORMAT = """
#seekto 0x0000;
struct {
u8 unknown_1[16]; // 0000-000f
u8 startup_image_enable; // 0010
u8 unknown_2[2]; // 0011-0012
u8 startup_ringtone_enable; // 0013
u8 welcome_message_enable; // 0014
ul16 welcome_text_color; // 0015-0016
ul16 welcome_start_line; // 0017-0018
ul16 welcome_start_column; // 0019-001a
u8 startup_password_enable; // 001b
char startup_password[16]; // 001c-002b
char welcome_message[32]; // 002c-004b
char name_callsign[16]; // 004c-005b
u8 voice_prompt; // 005c
u8 key_beep; // 005d
u8 unknown_3; // 005e
u8 lock_timer; // 005f
u8 key_led; // 0060
u8 brightness; // 0061
u8 lcd_timer; // 0062
u8 save_mode; // 0063
u8 save_start_timer; // 0064
u8 menu_exit; // 0065
u8 tx_priority; // 0066
u8 talkaround; // 0067
u8 alarm_type; // 0068
u8 auto_power_off; // 0069
ul32 auto_power_off_time; // 006a-006d
u8 auto_wake_up; // 006e
ul32 auto_wake_up_time; // 006f-0072
u8 multi_ptt; // 0073
u8 work_range; // 0074
u8 current_area; // 0075
u8 multi_standby; // 0076
u8 uv_repeater; // 0077
u8 freq_step; // 0078
u8 area_a_mode; // 0079
u8 area_a_show; // 007a
u8 area_a_zone; // 007b
ul16 area_a_channel; // 007c-007d
u8 area_b_mode; // 007e
u8 area_b_show; // 007f
u8 area_b_zone; // 0080
ul16 area_b_channel; // 0081-0082
u8 area_c_mode; // 0083
u8 area_c_show; // 0084
u8 area_c_zone; // 0085
ul16 area_c_channel; // 0086-0087
u8 lock_range_1_type; // 0088
ul16 lock_range_1_start; // 0089-008a
ul16 lock_range_1_end; // 008b-008c
u8 lock_range_2_type; // 008d
ul16 lock_range_2_start; // 008e-008f
ul16 lock_range_2_end; // 0090-0091
u8 lock_range_3_type; // 0092
ul16 lock_range_3_start; // 0093-0094
ul16 lock_range_3_end; // 0095-0096
u8 lock_range_4_type; // 0097
ul16 lock_range_4_start; // 0098-0099
ul16 lock_range_4_end; // 009a-009b
u8 scan_direction; // 009c
u8 scan_mode; // 009d
u8 scan_return; // 009e
u8 scan_dwell_timer; // 009f
u8 tx_voice_refresh; // 00a0
u8 rx_rssi_refresh; // 00a1
ul16 single_tone; // 00a2-00a3
u8 squelch_level; // 00a4
u8 tx_start_tone; // 00a5
u8 tx_end_tone; // 00a6
u8 vox; // 00a7
u8 vox_threshold; // 00a8
u8 vox_delay; // 00a9
u8 detect_range; // 00aa
u8 repeater_delay; // 00ab
u8 noaa_monitor; // 00ac
u8 mic_gain; // 00ad
u8 fm_spk_gain; // 00ae
u8 fm_dac_gain; // 00af
u8 am_spk_gain; // 00b0
u8 am_dac_gain; // 00b1
u8 glitch_threshold; // 00b2
u8 tt_rx_test; // 00b3
u8 tt_rssi_threshold; // 00b4
u8 tt_noise_threshold; // 00b5
u8 tt_power_test; // 00b6
u8 tt_power_136_174; // 00b7
u8 tt_power_400_480; // 00b8
u8 tt_power_18_64; // 00b9
u8 tt_power_64_136; // 00ba
u8 tt_power_173_240; // 00bb
u8 tt_power_240_320; // 00bc
u8 tt_power_320_400; // 00bd
u8 tt_power_480_560; // 00be
u8 tt_power_560_620; // 00bf
u8 tt_power_840_920; // 00c0
u8 tt_power_920_1000; // 00c1
u8 tt_power_1000_1080; // 00c2
u8 tt_power_1080_1160; // 00c3
u8 tt_power_1160_1240; // 00c4
u8 tt_power_1240_1300; // 00c5
u8 side_key_1_s; // 00c6
u8 side_key_1_l; // 00c7
u8 side_key_2_s; // 00c8
u8 side_key_2_l; // 00c9
u8 alarm_key_2_s; // 00ca
u8 alarm_key_2_l; // 00cb
u8 key_0_long; // 00cc
u8 key_1_long; // 00cd
u8 key_2_long; // 00ce
u8 key_3_long; // 00cf
u8 key_4_long; // 00d0
u8 key_5_long; // 00d1
u8 key_6_long; // 00d2
u8 key_7_long; // 00d3
u8 key_8_long; // 00d4
u8 key_9_long; // 00d5
u8 gps_enable; // 00d6
u8 gps_baud_rate; // 00d7
u8 gps_utc_zone; // 00d8
u8 unknown_4; // 00d9
u8 gps_auto_record; // 00da
u8 gps_auto_record_interval; // 00db
u8 unknown_5[40]; // 00dc-0103
u8 freq_input; // 0104
u8 fm_rx_standby; // 0105 * bool
u8 fm_channel; // 0106 * 1-128
u8 repeater_monitor; // 0107
u8 denoise; // 0108
} settings;
struct tone {
ul16 unknown:2,
is_dtcs:1,
// when is_dtcs is false, tone_or_inverted specifies if mode is tone
// when is_dtcs is true, tone_or_inverted specifies if dtcs is inverted
tone_or_inverted:1,
tone_val:12;
};
struct memory {
u8 rx_mode; // 00
u8 limit_rx_tx; // 01
u8 enabled; // 02
u8 isnarrow; // 03
struct tone rx_tone; // 04-05
ul32 rxfreq; // 06-0a
ul32 txfreq; // 0b-0e
struct tone tx_tone; // 0f-10
u8 power; // 11
u8 bcl; // 12
u8 dcs_encrypt:3, // 13
tot:5; //
u8 scan_remove:1, // 14
tail_tone:3, //
scrambler:4; //
ul32 mutecode; // 15-18
u8 unknown[8]; // 19-20
char name[16]; // 21-2f
};
struct dtmf_code {
char code[14]; // 00-0d
u8 unknown; // 0e
u8 code_length; // 0f
};
#seekto 0x200;
struct {
u8 dtmf_delay; // 0200
u8 dtmf_interval; // 0201
u8 dtmf_duration; // 0202
u8 dtmf_mode; // 0203
u8 dtmf_select; // 0204
u8 dtmf_display; // 0205
u8 dtmf_tx_gain; // 0206
u8 dtmf_rx_threshold; // 0207
u8 dtmf_control; // 0208
u8 time_calibrate; // 0209
struct dtmf_code dtmf_codes[16]; // 020a
struct dtmf_code remote_stun; // 030a
struct dtmf_code remote_kill; // 031a
struct dtmf_code remote_wake; // 032a
struct dtmf_code remote_monitor; // 033a
} dtmf;
#seekto 0x34a;
struct {
u8 reverse_channel; // 034a
u8 ctdcs_code_show; // 034b
u8 ch_alias_color; // 034c
u8 unknown[3]; // 034d-034f
u8 tx_unlimit; // 0350
} settings2;
#seekto 0x400;
struct {
struct memory vfo_uv_a; // 0400
struct memory vfo_uv_b; // 0430
struct memory vfo_uv_c; // 0460
struct memory vfo_cb_a; // 0490
struct memory vfo_cb_b; // 04c0
struct memory vfo_cb_c; // 04f0
struct memory vfo_850_a; // 0520
struct memory vfo_850_b; // 0550
struct memory vfo_850_c; // 0580
} vfo;
#seekto 0xc00;
struct {
u8 gps_coord_type; // 0c00
u8 gps_speed_unit; // 0c01
u8 gps_distance_unit; // 0c02
u8 gps_altitude_unit; // 0c03
char gps_fixed_lat[7]; // 0c04-0c0a
u8 unknown_1[3]; // 0c0b-0c0d
char gps_fixed_lat_dir; // 0c0e
char gps_fixed_long[8]; // 0c0f-0c16
u8 unknown_2[3]; // 0c17-0c19
char gps_fixed_long_dir; // 0c1a
char gps_fixed_altitude[7]; // 0c1b-0c21
u8 unknown_3[4]; // 0c22-0c25
u8 gps_mileage_type; // 0c26
} gps;
#seekto 0xc20;
struct {
u8 unknown_1[7]; // 0c20-0c26
u8 aprs_station_mode; // 0c27
u8 aprs_rx_ch; // 0c28
u8 aprs_tx_ch; // 0c29
u8 aprs_ptt_priority; // 0c2a
u8 aprs_ptt_delay; // 0c2b
u8 aprs_beacon_popup; // 0c2c
u8 unknown_2[3]; // 0c2d-0c2f
char aprs_callsign[6]; // 0c30-0c35
u8 aprs_ssid; // 0c36
u8 aprs_symbol_table; // 0c37
u8 aprs_symbol; // 0c38
char aprs_comment[128]; // 0c39-0cb8
u8 aprs_mice_enable; // 0cb9
u8 aprs_mice_mode; // 0cba
char aprs_path_1[6]; // 0cbb-0cc0
u8 aprs_path_1_count; // 0cc1
char aprs_path_2[6]; // 0cc2-0cc7
u8 aprs_path_2_count; // 0cc8
u8 aprs_tx_voltage; // 0cc9
u8 unknown_3[2]; // 0cca-0ccb
u8 aprs_tx_star; // 0ccc
u8 aprs_tx_mileage; // 0ccd
u8 aprs_beacon_ptt_after; // 0cce
u8 aprs_smart_beacon; // 0ccf
u8 aprs_beacon_time_mode; // 0cd0
ul32 aprs_beacon_time; // 0cd1-0cd4
u8 aprs_queue_beacon; // 0cd5
u8 aprs_queue_interval; // 0cd6
u8 aprs_digi_rep_ch; // 0cd7
u8 aprs_digi_1_enable; // 0cd8
char aprs_digi_1_name[6]; // 0cd9-0cde
u8 aprs_digi_2_enable; // 0cdf
char aprs_digi_2_name[6]; // 0ce0-0ce5
u8 aprs_digi_wait_before_rep; // 0ce6
char aprs_digi_remote_password[6]; // 0ce7-0cec
u8 aprs_enable; // 0ced
u8 unknown_4[2]; // 0cee-0cef
u8 aprs_demod_tone; // 0cf0
u8 aprs_tx_level; // 0cf1
u8 aprs_rx_level; // 0cf2
u8 aprs_beacon_save; // 0cf3
u8 aprs_beacon_ptt_after_ch; // 0cf4
u8 aprs_rx_ch_mute; // 0cf5
u8 unknown_5[10]; // 0cf6-0cff
} aprs;
#seekto 0x2000;
struct memory channels[1024];
struct zone {
u8 channel_count1; // 0000
u8 channel_count2; // 0001
u8 unknown1[2]; // 0002-0003
char name[16]; // 0004-0013
ul16 channels[200]; // 0014-01a3
u8 unknown2[92]; // 01a4-01ff
};
#seekto 0x1a000;
struct zone zones[256];
struct fm_memory {
// 64-108 MHz, 2.0-30.0 MHz, 520-1710 KHz, 153-279 KHz
u8 range; // 00 *
// FM, AM, LSB, USB, CW
u8 demodulation; // 01 *
// 1 K, 5 K, 10 K, 50 K, 100 K, 500 K, 1 M, 9 K
u8 step; // 02 *
// 0.5 K, 1.0 K, 1.2 K, 2.2 K, 3.0 K, 4.0 K
u8 bandwidth; // 03 *
// AGC, 0, -1, -2, ..., -37
u8 attenuation; // 04 *
// x-32768?
ul16 bfo; // 05-06 *
ul16 frequency; // 07-08 *
// bool
u8 enabled; // 09 *
u8 unknown[6]; // 0a-0f
char name[16]; // 10-1f *
};
#seekto 0xd4000;
struct fm_memory fm_channels[128]; // *
"""
CMD_ACK = b"\x06"
DTCS_CODES = tuple(sorted(chirp_common.DTCS_CODES + (645,)))
LIST_ALARMTYPE = ["Local Alarm", "Remote Alarm", "Local + Remote"]
LIST_APRSBEACONAFTER = ["Off", "Area Match", "All Area"]
LIST_APRSBEACONSAVE = ["Not Stored", "FCS Check OK", "Stored All"]
LIST_APRSCHANNEL = ["Off", "CH-A", "CH-B", "CH-C"]
LIST_APRSCHANNELCOMBOS = ["CH-A", "CH-B", "CH-C", "CH-A + CH-B", "CH-A + CH-C",
"CH-B + CH-C", "CH-A + CH-B + CH-C"]
LIST_APRSLEVEL = ["-10.5dB", "-9.0dB", "-7.5dB", "-6.0dB",
"-4.5dB", "-3.0dB", "-1.5dB", "0dB"]
LIST_APRESMICEMODE = ["M0: Off Duty", "M1: En Route", "M2: In Service",
"M3: Returning", "M4: Committed", "M5: Special",
"M6: Priority", "Emergency"]
LIST_APRSPTTDELAY = LIST_DELAY = ["%s ms" % x for x in range(200, 650, 50)]
LIST_APRSPTTPRIORITY = ["Call", "APRS"]
LIST_APRSSMARTBEACON = ["Off", "Type 1", "Type 2", "Type 3"]
LIST_APRSSTATIONMODE = ["Fixed", "GPS"]
LIST_APRSSYMBOLS = chirp_common.APRS_SYMBOLS + \
("| TNC Stream Sw", "}", "~ TNC Stream Sw")
LIST_APRSSYMBOLTABLE = ["/", "\\"]
LIST_AREA = ["A", "B", "C"]
LIST_AREAMODE = ["Frequency", "Channel", "Zone"]
LIST_AREASHOW = ["Channel Number", "Frequency"]
LIST_BCL = ["Off", "Carrier Match", "CTC/DCS Match"]
LIST_BRIGHTNESS = ["%s" % x for x in range(0, 5, 1)]
LIST_COLOR = ["Blue", "Green", "Red", "Yellow", "White",
"Fuchsia", "Pink", "Orange", "Tomato", "Cyan", "Golden"]
LIST_DCSENCRYPT = ["Standard", "Encrypt 1",
"Encrypt 2", "Encrypt 3", "Mute Code"]
LIST_DELAY = ["%s ms" % x for x in range(0, 2100, 100)]
LIST_DENOISE = ["Off", "On", "840-1000MHz"]
LIST_DETECTRANGE = ["18-64 MHz", "64-136 MHz", "136-174 MHz", "174-240 MHz",
"240-320 MHz", "320-400 MHz", "400-480 MHz", "480-560 MHz",
"560-620 MHz", "840-920 MHz", "920-1000 MHz"]
LIST_DIRECTION = ["Up", "Down"]
LIST_DTMFMODE = ["Off", "TX Start", "TX End"]
LIST_DTMFSELECT = ["DTMF-%s" % x for x in range(1, 17)]
LIST_FREQINPUT = ["6 digit", "8 digit"]
LIST_FREQSTEP = ["0.25K", "1.25K", "2.5K", "5K", "6.25K", "10K", "12.5K",
"20K", "25K", "50K", "100K", "500K", "1M", "5M"]
LIST_GPSALT = ["m", "foot"]
LIST_GPSBAUD = ["4800", "9600", "14400", "19200", "38400",
"56000", "57600", "115200", "128000", "256000"]
LIST_GPSCOORD = ["degree", "degree.min", "degree.min.sec"]
LIST_GPSDIST = ["km", "n mile", "mile"]
LIST_GPSLATDIR = ["N", "S"]
LIST_GPSLONGDIR = ["E", "W"]
LIST_GPSMILEAGE = ["Startup Zero", "Keep"]
LIST_GPSSPEED = ["km/h", "knot", "mph"]
LIST_GPSUTC = ["UTC 0", "UTC+1", "UTC+2", "UTC+3",
"UTC+3.5", "UTC+4", "UTC+5", "UTC+5.5"] + \
["UTC+%s" % x for x in range(6, 13, 1)] + \
["UTC-%s" % x for x in range(1, 13, 1)]
LIST_INTERVAL = ["%s ms" % x for x in range(30, 210, 10)]
LIST_KEYDEFINE = ["None", "Monitor", "Power Switch", "Scanning", "VOX",
"Squelch", "Frequency Step", "Multi Standby", "Tx Priority",
"Roger Beep", "FM Radio", "Talkaround", "Alarm",
"Send Single Tone", "Frequency Detect", "CTC/DCS Scan",
"Spectrum", "Radio Sleep", "Query Status", "Save Channel",
"Rx Demodulation", "NOAA Channels", "LCD On-Off",
"LCD Brightness", "Key LED On-Off", "U/V Repeater", "GPS",
"GPS Manual REC", "GPS Track Query", "APRS Beacon REC",
"Zone Selection", "Work Range"]
LIST_LIMITRXTX = ["Rx+Tx", "Only Rx", "Only Tx"]
LIST_LOCK = ["Unlock", "Rx Only", "Lock"]
LIST_SAVEMODE = ["Off", "1:1", "1:2", "1:3"]
LIST_SCANMODE = ["CO", "TO", "SE"]
LIST_SCANRETURN = ["Original Channel", "Current Channel"]
LIST_SCANTIMER = ["Off"] + ["%s seconds" % x for x in range(1, 31, 1)]
LIST_SCRAMBLER = ["Off"] + ["%s" % x for x in range(1, 9, 1)]
LIST_TAILTONE = ["Off", "55Hz No Shift",
"120° Shift", "180° Shift", "240° Shift", ]
LIST_TALKAROUND = ["Off", "Talkaround", "Invert Frequency"]
LIST_TIMER = ["Off", "5 seconds", "10 seconds"] + [
"%s seconds" % x for x in range(15, 615, 15)]
LIST_TXENDTONE = ["Off", "Roger Beep 1",
"Roger Beep 2", "Send Radio Name", "Send GPS"]
LIST_TXPRI = ["Edit", "Busy"]
LIST_WORKRANGE = ["64-620MHz", "18-64MHz", "840-1000MHz"]
VALID_CHARS = chirp_common.CHARSET_ALPHANUMERIC + \
"`{|}!\"#$%&'()*+,-./:;<=>?@[]^_"
DTMF_CHARS = list("0123456789ABCD*#")
HEX_CHARS = "0123456789ABCDEF"
class MemoryRegions(Enum):
"""
Defines the logical memory regions for this radio model.
"""
settingData = 0
channelData = 1
zoneData = 2
fmData = 3
MEMORY_REGIONS_RANGES = {
# (Start addr, length in blocks, region id)
MemoryRegions.settingData: (0x0000, 4, 0x90),
MemoryRegions.channelData: (0x2000, 48, 0x91),
MemoryRegions.zoneData: (0x1a000, 128, 0x92),
MemoryRegions.fmData: (0xd4000, 4, 0x99),
}
def _read_block(radio, block_addr, block_size):
serial = radio.pipe
cmd = struct.pack(">BH", ord(b'R'), block_addr)
ccs = bytes([checksum.checksum_8bit(cmd)])
expectedresponse = b"R" + cmd[1:]
cmd = cmd + ccs
LOG.debug("Reading block %04x..." % block_addr)
try:
serial.write(cmd)
response = serial.read(3 + block_size + 1)
cs = checksum.checksum_8bit(response[:-1])
if response[:3] != expectedresponse:
raise Exception("Error reading block %04x." % block_addr)
chunk = response[3:]
if chunk[-1] != cs:
raise Exception("Block failed checksum!")
block_data = chunk[:-1]
except Exception:
raise errors.RadioError("Failed to read block at %04x" % block_addr)
return block_data
def _write_block(radio, block_number, block_size, region, data):
serial = radio.pipe
# map the upload address to the mmap start and end addresses
start_addr = block_number * block_size
end_addr = start_addr + block_size
block_data = data[start_addr:end_addr]
cmd = struct.pack(">BH", region, block_number)
cs = bytes([checksum.checksum_8bit(cmd + block_data)])
block_data += cs
LOG.debug("Writing Data:")
LOG.debug(util.hexprint(cmd + block_data))
try:
serial.write(cmd + block_data)
if serial.read(1) != CMD_ACK:
raise Exception("No ACK")
except Exception:
raise errors.RadioError("Failed to send block "
"to radio at %04x" % block_number)
def do_download(radio):
LOG.debug("download")
data = b""
status = chirp_common.Status()
status.msg = "Cloning from radio"
status.cur = 0
status.max = radio.END_ADDR
for addr in range(radio.START_ADDR, radio.END_ADDR, 1):
status.cur = addr
radio.status_fn(status)
block = _read_block(radio, addr + radio.READ_OFFSET, radio.BLOCK_SIZE)
data += block
LOG.debug("Address: %04x" % addr)
LOG.debug(util.hexprint(block))
return memmap.MemoryMapBytes(data)
def do_upload(radio):
status = chirp_common.Status()
status.msg = "Uploading to radio"
status.cur = 0
status.max = get_total_upload_blocks()
# The OEM software reads the 1st block from the radio before commencing
# with the upload. That behavior will be mirrored here.
_read_block(radio, radio.START_ADDR, radio.BLOCK_SIZE)
data_bytes = radio.get_mmap()
for item in MemoryRegions:
_, length, regionId = MEMORY_REGIONS_RANGES[item]
item_bytes = get_bytes_for_region(data_bytes, item, radio.BLOCK_SIZE)
for block in range(0, length):
status.cur += 1
radio.status_fn(status)
_write_block(radio, block, radio.BLOCK_SIZE, regionId, item_bytes)
def get_bytes_for_region(all_bytes: bytearray, item, blockSize):
start, length, _ = MEMORY_REGIONS_RANGES[item]
return all_bytes[start:start+(length*blockSize)]
def get_total_upload_blocks():
return sum(length for _, length, _ in MEMORY_REGIONS_RANGES.values())
class RT880GBank(chirp_common.NamedBank):
def get_name(self):
_bank = self._model._radio._memobj.zones[self.index]
return str(_bank.name).rstrip().replace("\xFF", "")
def set_name(self, name):
_bank = self._model._radio._memobj.zones[self.index]
_bank.name = str(name)[:16].rstrip().ljust(16, '\xFF')
class RT880GBankModel(chirp_common.BankModel):
def get_num_mappings(self):
return len(self.get_mappings())
def get_mappings(self):
banks = self._radio._memobj.zones
bank_mappings = []
for index, _bank in enumerate(banks):
bank = RT880GBank(self, "%i" % index, "b%i" % (index + 1))
bank.index = index
bank_mappings.append(bank)
return bank_mappings
def _get_channel_numbers_in_bank(self, bank):
_bank_used = self._radio._memobj.zones[bank.index].channel_count1 > 0
if not _bank_used:
return set()
_members = self._radio._memobj.zones[bank.index].channels
return set([int(ch) + 1 for ch in _members if ch != 0xFFFF])
def _update_bank_with_channel_numbers(self, bank, channels_in_bank):
_zone = self._radio._memobj.zones[bank.index]
if len(channels_in_bank) > len(_zone.channels):
raise Exception("Too many entries in bank %d" % bank.index)
_channel_count = len(channels_in_bank)
_zone.channel_count1 = _channel_count
_zone.channel_count2 = _channel_count
for index, item in enumerate(channels_in_bank):
_zone.channels[index] = item - 1
for index in range(_channel_count, len(_zone.channels)):
_zone.channels[index] = 0xFFFF
def add_memory_to_mapping(self, memory, bank):
channels_in_bank = self._get_channel_numbers_in_bank(bank)
channels_in_bank.add(memory.number)
self._update_bank_with_channel_numbers(bank, channels_in_bank)
def remove_memory_from_mapping(self, memory, bank):
channels_in_bank = self._get_channel_numbers_in_bank(bank)
try:
channels_in_bank.remove(memory.number)
except KeyError:
raise Exception("Memory %i is not in bank %s. Cannot remove" %
(memory.number, bank))
self._update_bank_with_channel_numbers(bank, channels_in_bank)
def get_mapping_memories(self, bank):
memories = []
for channel in self._get_channel_numbers_in_bank(bank):
memories.append(self._radio.get_memory(channel))
return memories
def get_memory_mappings(self, memory):
banks = []
for bank in self.get_mappings():
if memory.number in self._get_channel_numbers_in_bank(bank):
banks.append(bank)
return banks
@directory.register
class RT880G(chirp_common.CloneModeRadio):
"""Radtel RT-880G"""
VENDOR = "Radtel"
MODEL = "RT-880G"
NAME_LENGTH = 16
BAUD_RATE = 115200
_channelCount = 1024
_airband = (108000000, 135999999)
_work_range_1 = (64000000, 620000000)
_work_range_2 = (18000000, 64000000)
_work_range_3 = (840000000, 1000000000)
SPECIALS = {
"VFO UV-A": (_channelCount + 1, _work_range_1),
"VFO UV-B": (_channelCount + 2, _work_range_1),
"VFO UV-C": (_channelCount + 3, _work_range_1),
"VFO CB-A": (_channelCount + 4, _work_range_2),
"VFO CB-B": (_channelCount + 5, _work_range_2),
"VFO CB-C": (_channelCount + 6, _work_range_2),
"VFO 850-A": (_channelCount + 7, _work_range_3),
"VFO 850-B": (_channelCount + 8, _work_range_3),
"VFO 850-C": (_channelCount + 9, _work_range_3),
}
BLOCK_SIZE = 0x400
magic_enter = b"4R" + b"\x05\x10\x9b"
magic_exit = b"4R" + b"\x05\xEE\x79"
POWER_LEVELS = [chirp_common.PowerLevel("Low", watts=1),
chirp_common.PowerLevel("Medium", watts=5),
chirp_common.PowerLevel("High", watts=10)]
# Radio's write address starts at 0x0000
# Radio's write address ends at 0x0140
START_ADDR = 0
END_ADDR = 0x035b
# Radio's read address starts at 0x7820
# Radio's read address ends at 0x795F
READ_OFFSET = 0x0008
def get_features(self):
rf = chirp_common.RadioFeatures()
rf.has_settings = True
rf.has_bank = True
rf.has_bank_names = True
rf.has_ctone = True
rf.has_cross = True
rf.has_rx_dtcs = True
rf.has_tuning_step = False
rf.can_odd_split = True
rf.has_name = True
rf.valid_name_length = self.NAME_LENGTH
rf.valid_characters = chirp_common.CHARSET_ASCII
rf.valid_skips = ["", "S"]
rf.valid_tmodes = ["", "Tone", "TSQL", "DTCS", "Cross"]
rf.valid_cross_modes = ["Tone->Tone", "Tone->DTCS", "DTCS->Tone",
"->Tone", "->DTCS", "DTCS->", "DTCS->DTCS"]
rf.valid_power_levels = self.POWER_LEVELS
rf.valid_duplexes = ["", "-", "+", "split"]
# SSB is included as USB here since SSB isn't in the master list
# of chirp modes
rf.valid_modes = ["FM", "NFM", "AM", "USB"]
rf.valid_dtcs_codes = DTCS_CODES
rf.memory_bounds = (1, self._channelCount)
rf.has_nostep_tuning = True
rf.valid_bands = [self._work_range_2, self._work_range_1,
self._work_range_3]
rf.valid_special_chans = list(self.SPECIALS.keys())
return rf
def process_mmap(self):
self._memobj = bitwise.parse(MEM_FORMAT, self._mmap)
def sync_in(self):
"""Download from radio"""
try:
enter_programming_mode(self.pipe, self.magic_enter)
data = do_download(self)
except errors.RadioError:
# Pass through any real errors we raise
raise
except Exception:
# If anything unexpected happens, make sure we raise
# a RadioError and log the problem
LOG.exception('Unexpected error during download')
raise errors.RadioError('Unexpected error communicating '
'with the radio')
finally:
exit_programming_mode(self.pipe, self.magic_exit)
self._mmap = data
self.process_mmap()
def sync_out(self):
"""Upload to radio"""
try:
enter_programming_mode(self.pipe, self.magic_enter)
self.pipe.timeout = 1
do_upload(self)
except errors.RadioError:
raise
except Exception:
# If anything unexpected happens, make sure we raise
# a RadioError and log the problem
LOG.exception('Unexpected error during upload')
raise errors.RadioError('Unexpected error communicating '
'with the radio')
finally:
self.pipe.timeout = 0.25
exit_programming_mode(self.pipe, self.magic_exit)
def get_raw_memory(self, number):
return repr(self._memobj.memory[number - 1])
def get_bank_model(self):
return RT880GBankModel(self)
@staticmethod
def _decode_tone(tone: bitwise.structDataElement):
if not tone.is_dtcs and not tone.tone_or_inverted:
return '', None, None
elif tone.is_dtcs and tone.tone_or_inverted:
code = int('%o' % tone.tone_val)
return 'DTCS', code, 'R'
elif tone.is_dtcs and not tone.tone_or_inverted:
code = int('%o' % tone.tone_val)
return 'DTCS', code, 'N'
elif not tone.is_dtcs and tone.tone_or_inverted:
return 'Tone', (tone.tone_val / 10.0), None
else:
raise errors.RadioError('Unsupported tone value')
@staticmethod
def _encode_tone(mode, val, pol):
if not mode:
return 0, 0, 0
elif mode == 'Tone':
code = int(val * 10)
return 0, 1, code
elif mode == 'DTCS':
code = int('%i' % val, 8)
inverted = 0
if pol == 'R':
inverted = 1
return 1, inverted, code
else:
raise errors.RadioError('Unsupported tone mode %r' % mode)
def _get_memobjs(self, number):
if isinstance(number, str):
return getattr(
self._memobj.vfo,
number.lower().replace(" ", "_").replace("-", "_"))
elif number > self._channelCount:
for k, v in list(self.SPECIALS.items()):
ch_num, _ = v
if number == ch_num:
return getattr(
self._memobj.vfo,
k.lower().replace(" ", "_").replace("-", "_"))
else:
return self._memobj.channels[number - 1]
def get_memory(self, number):
_mem = self._get_memobjs(number)
mem = chirp_common.Memory()
if isinstance(number, str):
ch_num, _ = self.SPECIALS[number]
mem.number = ch_num
mem.extd_number = number
else:
mem.number = number
mem.freq = int(_mem.rxfreq) * 10
# We'll consider any blank (i.e. 0 MHz frequency) to be empty
if mem.freq == 0:
mem.empty = True
return mem
if _mem.rxfreq.get_raw() == b"\xFF\xFF\xFF\xFF":
mem.freq = 0
mem.empty = True
return mem
# Freq and offset
mem.freq = int(_mem.rxfreq) * 10
# TX freq set
offset = (int(_mem.txfreq) * 10) - mem.freq
if offset != 0:
if chirp_common.is_split(self.get_features().valid_bands,
mem.freq, int(_mem.txfreq) * 10):
mem.duplex = "split"
mem.offset = int(_mem.txfreq) * 10
elif offset < 0:
mem.offset = abs(offset)
mem.duplex = "-"
elif offset > 0:
mem.offset = offset
mem.duplex = "+"
else:
mem.offset = 0
mem.name = str(_mem.name).rstrip(" ").replace("\xFF", "")
mem.mode = _mem.isnarrow and "NFM" or "FM"
chirp_common.split_tone_decode(mem,
self._decode_tone(_mem.tx_tone),
self._decode_tone(_mem.rx_tone))
# mode
if _mem.rx_mode == 0x02:
mem.mode = "USB"
elif _mem.rx_mode == 0x01:
mem.mode = "AM"
elif _mem.rx_mode == 0x00 and _mem.isnarrow == 0x01:
mem.mode = "NFM"
else:
mem.mode = "FM"
# power
if _mem.power == 0x02:
mem.power = self.POWER_LEVELS[2]
elif _mem.power == 0x01:
mem.power = self.POWER_LEVELS[1]
else:
mem.power = self.POWER_LEVELS[0]
if _mem.scan_remove:
mem.skip = "S"
mem.extra = RadioSettingGroup("Extra", "extra")
rs = RadioSettingValueList(
LIST_DCSENCRYPT, current_index=_mem.dcs_encrypt)
rset = RadioSetting("dcs_encrypt", "DCS Encrypt", rs)
mem.extra.append(rset)
rs = RadioSettingValueString(
0, 8, f"{_mem.mutecode.get_value():x}".upper(), False, HEX_CHARS)
rset = RadioSetting("mutecode", "Mute Code", rs)
mem.extra.append(rset)
rs = RadioSettingValueList(
LIST_TAILTONE, current_index=_mem.tail_tone)
rset = RadioSetting("tail_tone", "Tail Tone", rs)
mem.extra.append(rset)
rs = RadioSettingValueList(
LIST_SCRAMBLER, current_index=_mem.scrambler)
rset = RadioSetting("scrambler", "Scrambler", rs)
mem.extra.append(rset)
rs = RadioSettingValueList(
LIST_BCL, current_index=_mem.bcl if _mem.bcl <= 2 else 0)
rset = RadioSetting("bcl", "Busy Channel Lockout", rs)
mem.extra.append(rset)
rs = RadioSettingValueList(
LIST_LIMITRXTX, current_index=_mem.limit_rx_tx)
rset = RadioSetting("limit_rx_tx", "Limit Rx/Tx", rs)
mem.extra.append(rset)
rs = RadioSettingValueList(
LIST_TIMER, current_index=_mem.tot)
rset = RadioSetting("tot", "Time Out Timer", rs)
mem.extra.append(rset)
return mem
def set_memory(self, mem):
LOG.debug("Setting %i(%s)" % (mem.number, mem.extd_number))
_mem = self._get_memobjs(mem.number)
# if empty memory
if mem.empty:
_mem.set_raw("\xFF" * 48)
return
_mem.set_raw("\x00" * 32 + "\x20" * 16)
_mem.enabled = 0x01
_mem.rxfreq = mem.freq / 10
if mem.duplex == "split":
_mem.txfreq = mem.offset / 10
elif mem.duplex == "+":
_mem.txfreq = (mem.freq + mem.offset) / 10
elif mem.duplex == "-":
_mem.txfreq = (mem.freq - mem.offset) / 10
else:
_mem.txfreq = mem.freq / 10
_mem.name = mem.name.rstrip(' ').ljust(self.NAME_LENGTH, '\xFF')
_mem.scan_remove = mem.skip == "S"
# mode
if str(mem.mode) == "NFM":
_mem.rx_mode = 0x00
_mem.isnarrow = 0x01
elif str(mem.mode) == "AM":
_mem.rx_mode = 0x01
_mem.isnarrow = 0x00
elif str(mem.mode) == "USB":
_mem.rx_mode = 0x02
_mem.isnarrow = 0x00
else: # FM
_mem.rx_mode = 0x00
_mem.isnarrow = 0x00
# power
if str(mem.power) == str(self.POWER_LEVELS[2]):
_mem.power = 0x02
elif str(mem.power) == str(self.POWER_LEVELS[1]):
_mem.power = 0x01
else:
_mem.power = 0x00
# tones
txtone, rxtone = chirp_common.split_tone_encode(mem)
is_dtcs, tone_or_inverted, tone_val = self._encode_tone(*txtone)
_mem.tx_tone.is_dtcs = is_dtcs
_mem.tx_tone.tone_or_inverted = tone_or_inverted
_mem.tx_tone.tone_val = tone_val
is_dtcs, tone_or_inverted, tone_val = self._encode_tone(*rxtone)
_mem.rx_tone.is_dtcs = is_dtcs
_mem.rx_tone.tone_or_inverted = tone_or_inverted
_mem.rx_tone.tone_val = tone_val
# extras
for setting in mem.extra:
if setting.get_name() == "mutecode":
setattr(_mem, setting.get_name(), int(str(setting.value), 16))
else:
setattr(_mem, setting.get_name(), setting.value)
def validate_memory(self, mem):
msgs = []
# airband should always be AM
if (chirp_common.in_range(mem.freq, [self._airband])
and mem.mode != 'AM'):
msgs.append(chirp_common.ValidationWarning(
'Frequency in this range requires AM mode'))
# adjust VFO channels to their appropriate work range
if mem.number > self._channelCount:
_, work_range = self.SPECIALS[mem.extd_number]
if not chirp_common.in_range(mem.freq, [work_range]):
msgs.append(chirp_common.ValidationError(
'Frequency out of valid range'))
return msgs + super().validate_memory(mem)
def apply_string(self, setting, obj):
"""
Callback for set_apply_callback to pad with 0xFF instead of spaces
"""
length = len(str(setting.value))
setattr(obj, setting.get_name().rsplit('.', 1)[-1],
str(setting.value).rstrip(' ').ljust(length, '\xFF'))
def get_settings(self):
basic = RadioSettingGroup("basic", "Basic Settings")
startup = RadioSettingGroup("startup", "Startup Settings")
keydefine = RadioSettingGroup("keydefine", "Key Define Settings")
analog = RadioSettingGroup("analog", "Analog Settings")
dtmf = RadioSettingGroup("dtmf", "DTMF Settings")
gps = RadioSettingGroup("gps", "GPS Settings")
aprs = RadioSettingGroup("aprs", "APRS Settings")
time = RadioSettingGroup("time", "Time Settings")
top = RadioSettings(basic, startup, keydefine, analog,
dtmf, gps, aprs, time)
try:
self.get_basic_settings(basic)
self.get_startup_settings(startup)
self.get_keydefine_settings(keydefine)
self.get_analog_settings(analog)
self.get_dtmf_settings(dtmf)
self.get_gps_settings(gps)
self.get_aprs_settings(aprs)
self.get_time_settings(time)
except Exception as e:
LOG.exception("Error getting settings: %s", e)
return top
def get_basic_settings(self, group):
_settings = self._memobj.settings
_settings2 = self._memobj.settings2
# Name/Callsign
_codeobj = _settings.name_callsign
_code = str(_codeobj).rstrip('\xFF')
rs = RadioSettingValueString(0, 16, _code, True)
rset = RadioSetting("name_callsign", "Name/Callsign", rs)
rset.set_apply_callback(self.apply_string, _settings)
group.append(rset)
# Voice Prompt
rs = RadioSettingValueBoolean(_settings.voice_prompt)
rset = RadioSetting("voice_prompt", "Voice Prompt", rs)
group.append(rset)
# Key Beep
rs = RadioSettingValueBoolean(_settings.key_beep)
rset = RadioSetting("key_beep", "Key Beep", rs)
group.append(rset)
# Key Lock Timer
rs = RadioSettingValueList(
LIST_TIMER, current_index=_settings.lock_timer)
rset = RadioSetting("lock_timer", "Key Lock Timer", rs)
group.append(rset)
# Key LED
rs = RadioSettingValueBoolean(_settings.key_led)
rset = RadioSetting("key_led", "Key LED", rs)
group.append(rset)
# Display Timer
rs = RadioSettingValueList(
LIST_TIMER, current_index=_settings.lcd_timer)
rset = RadioSetting("lcd_timer", "Display Timer", rs)
group.append(rset)
# LCD Brightness
rs = RadioSettingValueList(
LIST_BRIGHTNESS, current_index=_settings.brightness)
rset = RadioSetting("brightness", "LCD Brightness", rs)
group.append(rset)
# Menu Exit Timer
rs = RadioSettingValueList(
LIST_TIMER, current_index=_settings.menu_exit)
rset = RadioSetting("menu_exit", "Menu Exit Timer", rs)
group.append(rset)
# Tx Priority
rs = RadioSettingValueList(
LIST_TXPRI, current_index=_settings.tx_priority)
rset = RadioSetting("tx_priority", "Tx Priority", rs)
group.append(rset)
# Frequency Step
rs = RadioSettingValueList(
LIST_FREQSTEP, current_index=_settings.freq_step)
rset = RadioSetting("freq_step", "Frequency Step", rs)
group.append(rset)
# Talkaround
rs = RadioSettingValueList(
LIST_TALKAROUND, current_index=_settings.talkaround)
rset = RadioSetting("talkaround", "Talkaround", rs)
group.append(rset)
# Save Mode
rs = RadioSettingValueList(
LIST_SAVEMODE, current_index=_settings.save_mode)
rset = RadioSetting("save_mode", "Save Mode", rs)
group.append(rset)
# Save Start Timer
rs = RadioSettingValueList(
LIST_TIMER, current_index=_settings.save_start_timer)
rset = RadioSetting("save_start_timer", "Save Start Timer", rs)
group.append(rset)
# Scan Mode
rs = RadioSettingValueList(
LIST_SCANMODE, current_index=_settings.scan_mode)
rset = RadioSetting("scan_mode", "Scan Mode", rs)
group.append(rset)
# Scan Direction
rs = RadioSettingValueList(
LIST_DIRECTION, current_index=_settings.scan_direction)
rset = RadioSetting("scan_direction", "Scan Direction", rs)
group.append(rset)
# Scan Return
rs = RadioSettingValueList(
LIST_SCANRETURN, current_index=_settings.scan_return)
rset = RadioSetting("scan_return", "Scan Return", rs)
group.append(rset)
# Scan Dwell Timer
rs = RadioSettingValueList(
LIST_SCANTIMER, current_index=_settings.scan_dwell_timer)
rset = RadioSetting("scan_dwell_timer", "Scan Dwell Timer", rs)
group.append(rset)
# Alarm Type
rs = RadioSettingValueList(
LIST_ALARMTYPE, current_index=_settings.alarm_type)
rset = RadioSetting("alarm_type", "Alarm Type", rs)
group.append(rset)
# Work Range
rs = RadioSettingValueList(
LIST_WORKRANGE, current_index=_settings.work_range)
rset = RadioSetting("work_range", "Work Range", rs)
group.append(rset)
# Denoise
rs = RadioSettingValueList(
LIST_DENOISE, current_index=_settings.denoise)
rset = RadioSetting("denoise", "Denoise", rs)
group.append(rset)
# Multi Standby
rs = RadioSettingValueBoolean(_settings.multi_standby)
rset = RadioSetting("multi_standby", "Multi Standby", rs)
group.append(rset)
# U/V Repeater
rs = RadioSettingValueBoolean(_settings.uv_repeater)
rset = RadioSetting("uv_repeater", "U/V Repeater", rs)
group.append(rset)
# Repeater Monitor
rs = RadioSettingValueBoolean(_settings.repeater_monitor)
rset = RadioSetting("repeater_monitor", "Repeater Monitor", rs)
group.append(rset)
# NOAA Monitor
rs = RadioSettingValueBoolean(_settings.noaa_monitor)
rset = RadioSetting("noaa_monitor", "NOAA Monitor", rs)
group.append(rset)
# Frequency Input
rs = RadioSettingValueList(
LIST_FREQINPUT, current_index=_settings.freq_input)
rset = RadioSetting("freq_input", "Frequency Input", rs)
group.append(rset)
# Reverse Channel
rs = RadioSettingValueBoolean(_settings2.reverse_channel)
rset = RadioSetting("settings2.reverse_channel", "Reverse Channel", rs)
group.append(rset)
# CT/DCS Code Show
rs = RadioSettingValueBoolean(_settings2.ctdcs_code_show)
rset = RadioSetting("settings2.ctdcs_code_show",
"CT/DCS Code Show", rs)
group.append(rset)
# Channel Alias Color
rs = RadioSettingValueList(
LIST_COLOR, current_index=_settings2.ch_alias_color)
rset = RadioSetting("settings2.ch_alias_color",
"Channel Alias Color", rs)
group.append(rset)
# Tx Limit
rs = RadioSettingValueBoolean(_settings2.tx_unlimit)
rset = RadioSetting("settings2.tx_unlimit", "Tx Limit", rs)
group.append(rset)
area_group = RadioSettingGroup("basic_area", "Areas")
group.append(area_group)
def apply_with_offset(setting, obj, offset):
setattr(obj, setting.get_name(), int(setting.value) + offset)
# Current Area
rs = RadioSettingValueList(
LIST_AREA, current_index=_settings.current_area)
rset = RadioSetting("current_area", "Current Area", rs)
area_group.append(rset)
# Area A Mode
rs = RadioSettingValueList(
LIST_AREAMODE, current_index=_settings.area_a_mode)
rset = RadioSetting("area_a_mode", "Area A Mode", rs)
area_group.append(rset)
# Area A Show
rs = RadioSettingValueList(
LIST_AREASHOW, current_index=_settings.area_a_show)
rset = RadioSetting("area_a_show", "Area A Show", rs)
area_group.append(rset)
# Area A Zone
rs = RadioSettingValueInteger(1, 256, _settings.area_a_zone + 1)
rset = RadioSetting("area_a_zone", "Area A Zone", rs)
rset.set_doc('Value between 1-256')
rset.set_apply_callback(apply_with_offset, _settings, -1)
area_group.append(rset)
# Area A Channel
rs = RadioSettingValueInteger(1, self._channelCount,
_settings.area_a_channel + 1)
rset = RadioSetting("area_a_channel", "Area A Channel", rs)
rset.set_doc(f'Value between 1-{self._channelCount}')
rset.set_apply_callback(apply_with_offset, _settings, -1)
area_group.append(rset)
# Area B Mode
rs = RadioSettingValueList(
LIST_AREAMODE, current_index=_settings.area_b_mode)
rset = RadioSetting("area_b_mode", "Area B Mode", rs)
area_group.append(rset)
# Area B Show
rs = RadioSettingValueList(
LIST_AREASHOW, current_index=_settings.area_b_show)
rset = RadioSetting("area_b_show", "Area B Show", rs)
area_group.append(rset)
# Area B Zone
rs = RadioSettingValueInteger(1, 256, _settings.area_b_zone + 1)
rset = RadioSetting("area_b_zone", "Area B Zone", rs)
rset.set_doc('Value between 1-256')
rset.set_apply_callback(apply_with_offset, _settings, -1)
area_group.append(rset)
# Area B Channel
rs = RadioSettingValueInteger(1, self._channelCount,
_settings.area_b_channel + 1)
rset = RadioSetting("area_b_channel", "Area B Channel", rs)
rset.set_doc(f'Value between 1-{self._channelCount}')
rset.set_apply_callback(apply_with_offset, _settings, -1)
area_group.append(rset)
# Area C Mode
rs = RadioSettingValueList(
LIST_AREAMODE, current_index=_settings.area_c_mode)
rset = RadioSetting("area_c_mode", "Area C Mode", rs)
area_group.append(rset)
# Area C Show
rs = RadioSettingValueList(
LIST_AREASHOW, current_index=_settings.area_c_show)
rset = RadioSetting("area_c_show", "Area C Show", rs)
area_group.append(rset)
# Area C Zone
rs = RadioSettingValueInteger(1, 256, _settings.area_c_zone + 1)
rset = RadioSetting("area_c_zone", "Area C Zone", rs)
rset.set_doc('Value between 1-256')
rset.set_apply_callback(apply_with_offset, _settings, -1)
area_group.append(rset)
# Area C Channel
rs = RadioSettingValueInteger(1, self._channelCount,
_settings.area_c_channel + 1)
rset = RadioSetting("area_c_channel", "Area C Channel", rs)
rset.set_doc(f'Value between 1-{self._channelCount}')
rset.set_apply_callback(apply_with_offset, _settings, -1)
area_group.append(rset)
lock_group = RadioSettingGroup("basic_lock", "Lock Ranges")
group.append(lock_group)
# Lock Range 1 Type
rs = RadioSettingValueList(
LIST_LOCK, current_index=_settings.lock_range_1_type)
rset = RadioSetting("lock_range_1_type", "Lock Range 1 Type", rs)
lock_group.append(rset)
# Lock Range 1 Start
rs = RadioSettingValueInteger(18, 1000, _settings.lock_range_1_start)
rset = RadioSetting("lock_range_1_start", "Lock Range 1 Start", rs)
rset.set_doc('Value between 18-1000 (MHz)')
lock_group.append(rset)
# Lock Range 1 End
rs = RadioSettingValueInteger(18, 1000, _settings.lock_range_1_end)
rset = RadioSetting("lock_range_1_end", "Lock Range 1 End", rs)
rset.set_doc('Value between 18-1000 (MHz)')
lock_group.append(rset)
# Lock Range 2 Type
rs = RadioSettingValueList(
LIST_LOCK, current_index=_settings.lock_range_2_type)
rset = RadioSetting("lock_range_2_type", "Lock Range 2 Type", rs)
lock_group.append(rset)
# Lock Range 2 Start
rs = RadioSettingValueInteger(18, 1000, _settings.lock_range_2_start)
rset = RadioSetting("lock_range_2_start", "Lock Range 2 Start", rs)
rset.set_doc('Value between 18-1000 (MHz)')
lock_group.append(rset)
# Lock Range 2 End
rs = RadioSettingValueInteger(18, 1000, _settings.lock_range_2_end)
rset = RadioSetting("lock_range_2_end", "Lock Range 2 End", rs)
rset.set_doc('Value between 18-1000 (MHz)')
lock_group.append(rset)
# Lock Range 3 Type
rs = RadioSettingValueList(
LIST_LOCK, current_index=_settings.lock_range_3_type)
rset = RadioSetting("lock_range_3_type", "Lock Range 3 Type", rs)
lock_group.append(rset)
# Lock Range 3 Start
rs = RadioSettingValueInteger(18, 1000, _settings.lock_range_3_start)
rset = RadioSetting("lock_range_3_start", "Lock Range 3 Start", rs)
rset.set_doc('Value between 18-1000 (MHz)')
lock_group.append(rset)
# Lock Range 3 End
rs = RadioSettingValueInteger(18, 1000, _settings.lock_range_3_end)
rset = RadioSetting("lock_range_3_end", "Lock Range 3 End", rs)
rset.set_doc('Value between 18-1000 (MHz)')
lock_group.append(rset)
# Lock Range 4 Type
rs = RadioSettingValueList(
LIST_LOCK, current_index=_settings.lock_range_4_type)
rset = RadioSetting("lock_range_4_type", "Lock Range 4 Type", rs)
lock_group.append(rset)
# Lock Range 4 Start
rs = RadioSettingValueInteger(18, 1000, _settings.lock_range_4_start)
rset = RadioSetting("lock_range_4_start", "Lock Range 4 Start", rs)
rset.set_doc('Value between 18-1000 (MHz)')
lock_group.append(rset)
# Lock Range 4 End
rs = RadioSettingValueInteger(18, 1000, _settings.lock_range_4_end)
rset = RadioSetting("lock_range_4_end", "Lock Range 4 End", rs)
rset.set_doc('Value between 18-1000 (MHz)')
lock_group.append(rset)
def get_startup_settings(self, group):
_settings = self._memobj.settings
# Welcome Message
_codeobj = _settings.welcome_message
_code = str(_codeobj).rstrip('\xFF')
rs = RadioSettingValueString(0, 32, _code, True)
rset = RadioSetting("welcome_message", "Welcome Message", rs)
rset.set_apply_callback(self.apply_string, _settings)
group.append(rset)
# Welcome Message Enable
rs = RadioSettingValueBoolean(_settings.welcome_message_enable)
rset = RadioSetting("welcome_message_enable",
"Welcome Message Enable", rs)
group.append(rset)
# Welcome Message Color
rs = RadioSettingValueInteger(0, 65536, _settings.welcome_text_color)
rset = RadioSetting("welcome_text_color", "Welcome Message Color", rs)
rset.set_doc('Value between 0-65536')
group.append(rset)
# Welcome Message Start Line
rs = RadioSettingValueInteger(0, 319, _settings.welcome_start_line)
rset = RadioSetting("welcome_start_line",
"Welcome Message Start Line", rs)
rset.set_doc('Value between 0-319')
group.append(rset)
# Welcome Message Start Column
rs = RadioSettingValueInteger(0, 239, _settings.welcome_start_column)
rset = RadioSetting("welcome_start_column",
"Welcome Message Start Column", rs)
rset.set_doc('Value between 0-239')
group.append(rset)
# Startup Image Enable
rs = RadioSettingValueBoolean(_settings.startup_image_enable)
rset = RadioSetting("startup_image_enable",
"Startup Image Enable", rs)
group.append(rset)
# Startup Ringtone Enable
rs = RadioSettingValueBoolean(_settings.startup_ringtone_enable)
rset = RadioSetting("startup_ringtone_enable",
"Startup Ringtone Enable", rs)
group.append(rset)
# Startup Password
_codeobj = _settings.startup_password
_code = str(_codeobj).rstrip('\xFF')
rs = RadioSettingValueString(0, 16, _code, True)
rset = RadioSetting("startup_password", "Startup Password", rs)
rset.set_apply_callback(self.apply_string, _settings)
group.append(rset)
# Startup Password Enable
rs = RadioSettingValueBoolean(_settings.startup_password_enable)
rset = RadioSetting("startup_password_enable",
"Startup Password Enable", rs)
group.append(rset)
def get_keydefine_settings(self, group):
_settings = self._memobj.settings
# Multi PTT
rs = RadioSettingValueBoolean(_settings.multi_ptt)
rset = RadioSetting("multi_ptt", "Multi PTT", rs)
group.append(rset)
# Side Key 1 Short
rs = RadioSettingValueList(
LIST_KEYDEFINE, current_index=_settings.side_key_1_s)
rset = RadioSetting("side_key_1_s", "Side Key 1 Short", rs)
group.append(rset)
# Side Key 1 Long
rs = RadioSettingValueList(
LIST_KEYDEFINE, current_index=_settings.side_key_1_l)
rset = RadioSetting("side_key_1_l", "Side Key 1 Long", rs)
group.append(rset)
# Side Key 2 Short
rs = RadioSettingValueList(
LIST_KEYDEFINE, current_index=_settings.side_key_2_s)
rset = RadioSetting("side_key_2_s", "Side Key 2 Short", rs)
group.append(rset)
# Side Key 2 Long
rs = RadioSettingValueList(
LIST_KEYDEFINE, current_index=_settings.side_key_2_l)
rset = RadioSetting("side_key_2_l", "Side Key 2 Long", rs)
group.append(rset)
# Alarm Key 2 Short
rs = RadioSettingValueList(
LIST_KEYDEFINE, current_index=_settings.alarm_key_2_s)
rset = RadioSetting("alarm_key_2_s", "Alarm Key 2 Short", rs)
group.append(rset)
# Alarm Key 2 Long
rs = RadioSettingValueList(
LIST_KEYDEFINE, current_index=_settings.alarm_key_2_l)
rset = RadioSetting("alarm_key_2_l", "Alarm Key 2 Long", rs)
group.append(rset)
# 0 Press Long
rs = RadioSettingValueList(
LIST_KEYDEFINE, current_index=_settings.key_0_long)
rset = RadioSetting("key_0_long", "0 Press Long", rs)
group.append(rset)
# 1 Press Long
rs = RadioSettingValueList(
LIST_KEYDEFINE, current_index=_settings.key_1_long)
rset = RadioSetting("key_1_long", "1 Press Long", rs)
group.append(rset)
# 2 Press Long
rs = RadioSettingValueList(
LIST_KEYDEFINE, current_index=_settings.key_2_long)
rset = RadioSetting("key_2_long", "2 Press Long", rs)
group.append(rset)
# 3 Press Long
rs = RadioSettingValueList(
LIST_KEYDEFINE, current_index=_settings.key_3_long)
rset = RadioSetting("key_3_long", "3 Press Long", rs)
group.append(rset)
# 4 Press Long
rs = RadioSettingValueList(
LIST_KEYDEFINE, current_index=_settings.key_4_long)
rset = RadioSetting("key_4_long", "4 Press Long", rs)
group.append(rset)
# 5 Press Long
rs = RadioSettingValueList(
LIST_KEYDEFINE, current_index=_settings.key_5_long)
rset = RadioSetting("key_5_long", "5 Press Long", rs)
group.append(rset)
# 6 Press Long
rs = RadioSettingValueList(
LIST_KEYDEFINE, current_index=_settings.key_6_long)
rset = RadioSetting("key_6_long", "6 Press Long", rs)
group.append(rset)
# 7 Press Long
rs = RadioSettingValueList(
LIST_KEYDEFINE, current_index=_settings.key_7_long)
rset = RadioSetting("key_7_long", "7 Press Long", rs)
group.append(rset)
# 8 Press Long
rs = RadioSettingValueList(
LIST_KEYDEFINE, current_index=_settings.key_8_long)
rset = RadioSetting("key_8_long", "8 Press Long", rs)
group.append(rset)
# 9 Press Long
rs = RadioSettingValueList(
LIST_KEYDEFINE, current_index=_settings.key_9_long)
rset = RadioSetting("key_9_long", "9 Press Long", rs)
group.append(rset)
def get_analog_settings(self, group):
_settings = self._memobj.settings
# Squelch Level
rs = RadioSettingValueInteger(0, 10, _settings.squelch_level)
rset = RadioSetting("squelch_level", "Squelch Level", rs)
rset.set_doc('Value between 0-10')
group.append(rset)
# Tx Start Tone
rs = RadioSettingValueBoolean(_settings.tx_start_tone)
rset = RadioSetting("tx_start_tone", "Tx Start Tone", rs)
group.append(rset)
# Tx End Tone
rs = RadioSettingValueList(
LIST_TXENDTONE, current_index=_settings.tx_end_tone)
rset = RadioSetting("tx_end_tone", "Tx End Tone", rs)
group.append(rset)
# Single Tone
rs = RadioSettingValueInteger(0, 9999, _settings.single_tone)
rset = RadioSetting("single_tone", "Single Tone (Hz)", rs)
rset.set_doc('Value between 0-9999')
group.append(rset)
# VOX
rs = RadioSettingValueBoolean(_settings.vox)
rset = RadioSetting("vox", "VOX", rs)
group.append(rset)
# VOX Threshold
rs = RadioSettingValueInteger(0, 254, _settings.vox_threshold)
rset = RadioSetting("vox_threshold", "VOX Threshold", rs)
rset.set_doc('Value between 0-254')
group.append(rset)
# VOX Delay
rs = RadioSettingValueInteger(0, 5, _settings.vox_delay)
rset = RadioSetting("vox_delay", "VOX Delay", rs)
rset.set_doc('Value between 0-5')
group.append(rset)
# Detect Range
rs = RadioSettingValueList(
LIST_DETECTRANGE, current_index=_settings.detect_range)
rset = RadioSetting("detect_range", "Detect Range", rs)
group.append(rset)
# Repeater Delay
rs = RadioSettingValueList(
LIST_DELAY, current_index=_settings.repeater_delay)
rset = RadioSetting("repeater_delay", "Repeater Delay", rs)
group.append(rset)
# Mic Gain
rs = RadioSettingValueInteger(0, 31, _settings.mic_gain)
rset = RadioSetting("mic_gain", "Mic Gain", rs)
rset.set_doc('Value between 0-31')
group.append(rset)
# FM Mode SPK Gain
rs = RadioSettingValueInteger(0, 63, _settings.fm_spk_gain)
rset = RadioSetting("fm_spk_gain", "FM Mode SPK Gain", rs)
rset.set_doc('Value between 0-63')
group.append(rset)
# FM Mode DAC Gain
rs = RadioSettingValueInteger(0, 15, _settings.fm_dac_gain)
rset = RadioSetting("fm_dac_gain", "FM Mode DAC Gain", rs)
rset.set_doc('Value between 0-15')
group.append(rset)
# AM Mode SPK Gain
rs = RadioSettingValueInteger(0, 63, _settings.am_spk_gain)
rset = RadioSetting("am_spk_gain", "AM Mode SPK Gain", rs)
rset.set_doc('Value between 0-63')
group.append(rset)
# AM Mode DAC Gain
rs = RadioSettingValueInteger(0, 15, _settings.am_dac_gain)
rset = RadioSetting("am_dac_gain", "AM Mode DAC Gain", rs)
rset.set_doc('Value between 0-15')
group.append(rset)
# Glitch Threshold
rs = RadioSettingValueInteger(0, 10, _settings.glitch_threshold)
rset = RadioSetting("glitch_threshold", "Glitch Threshold", rs)
rset.set_doc('Value between 0-10')
group.append(rset)
# Voice Level Refresh
rs = RadioSettingValueList(
LIST_DELAY, current_index=_settings.tx_voice_refresh)
rset = RadioSetting("tx_voice_refresh", "Voice Level Refresh", rs)
group.append(rset)
# RSSI Level Refresh
rs = RadioSettingValueList(
LIST_DELAY, current_index=_settings.rx_rssi_refresh)
rset = RadioSetting("rx_rssi_refresh", "RSSI Level Refresh", rs)
group.append(rset)
tt_group = RadioSettingGroup("analog_tt", "Temporary Tuning")
group.append(tt_group)
# Temporary Tuning Rx Test
rs = RadioSettingValueBoolean(_settings.tt_rx_test)
rset = RadioSetting("tt_rx_test", "Rx Test", rs)
tt_group.append(rset)
# Temporary Tuning RSSI Threshold
rs = RadioSettingValueInteger(0, 127, _settings.tt_rssi_threshold)
rset = RadioSetting("tt_rssi_threshold", "RSSI Threshold", rs)
rset.set_doc('Value between 0-127')
tt_group.append(rset)
# Temporary Tuning Noise Threshold
rs = RadioSettingValueInteger(0, 127, _settings.tt_noise_threshold)
rset = RadioSetting("tt_noise_threshold", "Noise Threshold", rs)
rset.set_doc('Value between 0-127')
tt_group.append(rset)
# Temporary Tuning Power Test
rs = RadioSettingValueBoolean(_settings.tt_power_test)
rset = RadioSetting("tt_power_test", "Power Test", rs)
tt_group.append(rset)
# Temporary Tuning 136-174MHz Power
rs = RadioSettingValueInteger(0, 255, _settings.tt_power_136_174)
rset = RadioSetting("tt_power_136_174", "136-174MHz Power", rs)
rset.set_doc('Value between 0-255')
tt_group.append(rset)
# Temporary Tuning 400-480MHz Power
rs = RadioSettingValueInteger(0, 255, _settings.tt_power_400_480)
rset = RadioSetting("tt_power_400_480", "400-480MHz Power", rs)
rset.set_doc('Value between 0-255')
tt_group.append(rset)
# Temporary Tuning 18-64MHz Power
rs = RadioSettingValueInteger(0, 255, _settings.tt_power_18_64)
rset = RadioSetting("tt_power_18_64", "18-64MHz Power", rs)
rset.set_doc('Value between 0-255')
tt_group.append(rset)
# Temporary Tuning 64-136MHz Power
rs = RadioSettingValueInteger(0, 255, _settings.tt_power_64_136)
rset = RadioSetting("tt_power_64_136", "64-136MHz Power", rs)
rset.set_doc('Value between 0-255')
tt_group.append(rset)
# Temporary Tuning 173-240MHz Power
rs = RadioSettingValueInteger(0, 255, _settings.tt_power_173_240)
rset = RadioSetting("tt_power_173_240", "173-240MHz Power", rs)
rset.set_doc('Value between 0-255')
tt_group.append(rset)
# Temporary Tuning 240-320MHz Power
rs = RadioSettingValueInteger(0, 255, _settings.tt_power_240_320)
rset = RadioSetting("tt_power_240_320", "240-320MHz Power", rs)
rset.set_doc('Value between 0-255')
tt_group.append(rset)
# Temporary Tuning 320-400MHz Power
rs = RadioSettingValueInteger(0, 255, _settings.tt_power_320_400)
rset = RadioSetting("tt_power_320_400", "320-400MHz Power", rs)
rset.set_doc('Value between 0-255')
tt_group.append(rset)
# Temporary Tuning 480-560MHz Power
rs = RadioSettingValueInteger(0, 255, _settings.tt_power_480_560)
rset = RadioSetting("tt_power_480_560", "480-560MHz Power", rs)
rset.set_doc('Value between 0-255')
tt_group.append(rset)
# Temporary Tuning 560-620MHz Power
rs = RadioSettingValueInteger(0, 255, _settings.tt_power_560_620)
rset = RadioSetting("tt_power_560_620", "560-620MHz Power", rs)
rset.set_doc('Value between 0-255')
tt_group.append(rset)
# Temporary Tuning 840-920MHz Power
rs = RadioSettingValueInteger(0, 255, _settings.tt_power_840_920)
rset = RadioSetting("tt_power_840_920", "840-920MHz Power", rs)
rset.set_doc('Value between 0-255')
tt_group.append(rset)
# Temporary Tuning 920-1000MHz Power
rs = RadioSettingValueInteger(0, 255, _settings.tt_power_920_1000)
rset = RadioSetting("tt_power_920_1000", "920-1000MHz Power", rs)
rset.set_doc('Value between 0-255')
tt_group.append(rset)
# Temporary Tuning 1000-1080MHz Power
rs = RadioSettingValueInteger(0, 255, _settings.tt_power_1000_1080)
rset = RadioSetting("tt_power_1000_1080", "1000-1080MHz Power", rs)
rset.set_doc('Value between 0-255')
tt_group.append(rset)
# Temporary Tuning 1080-1160MHz Power
rs = RadioSettingValueInteger(0, 255, _settings.tt_power_1080_1160)
rset = RadioSetting("tt_power_1080_1160", "1080-1160MHz Power", rs)
rset.set_doc('Value between 0-255')
tt_group.append(rset)
# Temporary Tuning 1160-1240MHz Power
rs = RadioSettingValueInteger(0, 255, _settings.tt_power_1160_1240)
rset = RadioSetting("tt_power_1160_1240", "1160-1240MHz Power", rs)
rset.set_doc('Value between 0-255')
tt_group.append(rset)
# Temporary Tuning 1240-1300MHz Power
rs = RadioSettingValueInteger(0, 255, _settings.tt_power_1240_1300)
rset = RadioSetting("tt_power_1240_1300", "1240-1300MHz Power", rs)
rset.set_doc('Value between 0-255')
tt_group.append(rset)
def get_dtmf_settings(self, group):
_dtmf = self._memobj.dtmf
# DTMF Delay
rs = RadioSettingValueList(
LIST_DELAY, current_index=_dtmf.dtmf_delay)
rset = RadioSetting("dtmf.dtmf_delay", "DTMF Delay", rs)
group.append(rset)
# DTMF Interval
rs = RadioSettingValueList(
LIST_INTERVAL, current_index=_dtmf.dtmf_interval)
rset = RadioSetting("dtmf.dtmf_interval", "DTMF Interval", rs)
group.append(rset)
# DTMF Duration
rs = RadioSettingValueList(
LIST_INTERVAL, current_index=_dtmf.dtmf_duration)
rset = RadioSetting("dtmf.dtmf_duration", "DTMF Duration", rs)
group.append(rset)
# DTMF Mode
rs = RadioSettingValueList(
LIST_DTMFMODE, current_index=_dtmf.dtmf_mode)
rset = RadioSetting("dtmf.dtmf_mode", "DTMF Mode", rs)
group.append(rset)
# DTMF Select
rs = RadioSettingValueList(
LIST_DTMFSELECT, current_index=_dtmf.dtmf_select)
rset = RadioSetting("dtmf.dtmf_select", "DTMF Select", rs)
group.append(rset)
# DTMF Display
rs = RadioSettingValueBoolean(_dtmf.dtmf_display)
rset = RadioSetting("dtmf.dtmf_display", "DTMF Display", rs)
group.append(rset)
# DTMF Tx Gain
rs = RadioSettingValueInteger(0, 127, _dtmf.dtmf_tx_gain)
rset = RadioSetting("dtmf.dtmf_tx_gain", "DTMF Tx Gain", rs)
rset.set_doc('Value between 0-127')
group.append(rset)
# DTMF Rx Threshold
rs = RadioSettingValueInteger(0, 63, _dtmf.dtmf_rx_threshold)
rset = RadioSetting("dtmf.dtmf_rx_threshold", "DTMF Rx Threshold", rs)
rset.set_doc('Value between 0-63')
group.append(rset)
# DTMF Control
rs = RadioSettingValueBoolean(_dtmf.dtmf_control)
rset = RadioSetting("dtmf.dtmf_control", "DTMF Control", rs)
group.append(rset)
# Time Calibrate
rs = RadioSettingValueBoolean(_dtmf.time_calibrate)
rset = RadioSetting("dtmf.time_calibrate", "Time Calibrate", rs)
group.append(rset)
def apply_code(setting, obj, length):
code = ""
for char in str(setting.value):
if char in DTMF_CHARS:
code += char
else:
code += ""
obj.code_length = len(str(code))
obj.code = code.ljust(length, chr(255))
# DTMF Codes
for i in range(0, 16):
_codeobj = _dtmf.dtmf_codes[i].code
_code = str(_codeobj).rstrip('\xFF')
rs = RadioSettingValueString(0, 14, _code, False, DTMF_CHARS)
rset = RadioSetting("dtmf.dtmf_codes/%i.code" % i,
"Code %i" % (i + 1), rs)
rset.set_apply_callback(apply_code, _dtmf.dtmf_codes[i], 14)
group.append(rset)
# Remote Stun
_codeobj = _dtmf.remote_stun.code
_code = str(_codeobj).rstrip('\xFF')
rs = RadioSettingValueString(0, 14, _code, False, DTMF_CHARS)
rset = RadioSetting("dtmf.remote_stun", "Remote Stun", rs)
rset.set_apply_callback(apply_code, _dtmf.remote_stun, 14)
group.append(rset)
# Remote Kill
_codeobj = _dtmf.remote_kill.code
_code = str(_codeobj).rstrip('\xFF')
rs = RadioSettingValueString(0, 14, _code, False, DTMF_CHARS)
rset = RadioSetting("dtmf.remote_kill", "Remote Kill", rs)
rset.set_apply_callback(apply_code, _dtmf.remote_kill, 14)
group.append(rset)
# Remote Wake
_codeobj = _dtmf.remote_wake.code
_code = str(_codeobj).rstrip('\xFF')
rs = RadioSettingValueString(0, 14, _code, False, DTMF_CHARS)
rset = RadioSetting("dtmf.remote_wake", "Remote Wake", rs)
rset.set_apply_callback(apply_code, _dtmf.remote_wake, 14)
group.append(rset)
# Remote Monitor
_codeobj = _dtmf.remote_monitor.code
_code = str(_codeobj).rstrip('\xFF')
rs = RadioSettingValueString(0, 14, _code, False, DTMF_CHARS)
rset = RadioSetting("dtmf.remote_monitor", "Remote Monitor", rs)
rset.set_apply_callback(apply_code, _dtmf.remote_monitor, 14)
group.append(rset)
def get_gps_settings(self, group):
_settings = self._memobj.settings
_gps = self._memobj.gps
# GPS Enable
rs = RadioSettingValueBoolean(_settings.gps_enable)
rset = RadioSetting("gps_enable", "GPS Enable", rs)
group.append(rset)
# GPS Baud Rate
rs = RadioSettingValueList(
LIST_GPSBAUD, current_index=_settings.gps_baud_rate)
rset = RadioSetting("gps_baud_rate", "Baud Rate", rs)
group.append(rset)
# GPS UTC Zone
rs = RadioSettingValueList(
LIST_GPSUTC, current_index=_settings.gps_utc_zone)
rset = RadioSetting("gps_utc_zone", "UTC Zone", rs)
group.append(rset)
# GPS Coordinate Type
rs = RadioSettingValueList(
LIST_GPSCOORD, current_index=_gps.gps_coord_type)
rset = RadioSetting("gps.gps_coord_type", "Coordinate Type", rs)
group.append(rset)
# GPS Speed Unit
rs = RadioSettingValueList(
LIST_GPSSPEED, current_index=_gps.gps_speed_unit)
rset = RadioSetting("gps.gps_speed_unit", "Speed Unit", rs)
group.append(rset)
# GPS Distance Unit
rs = RadioSettingValueList(
LIST_GPSDIST, current_index=_gps.gps_distance_unit)
rset = RadioSetting("gps.gps_distance_unit", "Distance Unit", rs)
group.append(rset)
# GPS Altitude Unit
rs = RadioSettingValueList(
LIST_GPSALT, current_index=_gps.gps_altitude_unit)
rset = RadioSetting("gps.gps_altitude_unit", "Altitude Unit", rs)
group.append(rset)
# GPS Mileage Type
rs = RadioSettingValueList(
LIST_GPSMILEAGE, current_index=_gps.gps_mileage_type)
rset = RadioSetting("gps.gps_mileage_type", "Mileage Type", rs)
group.append(rset)
# GPS Auto Record
rs = RadioSettingValueBoolean(_settings.gps_auto_record)
rset = RadioSetting("gps_auto_record", "Auto Record", rs)
group.append(rset)
# Fixed coordinates are stored as strings with an implicit decimal
def fixed_coord_to_float(val, wholeDigits):
valStr = str(val).replace("-", "0").replace('\00', "0")
return float(f"{valStr[:wholeDigits]}.{valStr[wholeDigits:]}")
def apply_fixed_coord(setting, obj, width, precision):
setattr(obj, setting.get_name()
.rsplit('.', 1)[-1],
f"{float(setting.value):0{width}.{precision}f}"
.replace(".", ""))
def fixed_coord_dir_index(list, val):
try:
return list.index(str(val))
except ValueError:
return 0
def apply_fixed_coord_dir(setting, obj, dirArray):
setattr(obj, setting.get_name().rsplit('.', 1)[-1],
dirArray[int(setting.value)])
# GPS Fixed Latitude
val = fixed_coord_to_float(_gps.gps_fixed_lat, 2)
rs = RadioSettingValueFloat(0, 99.99999, val, 0.00001, 5)
rset = RadioSetting("gps.gps_fixed_lat", "Fixed Latitude", rs)
rset.set_apply_callback(apply_fixed_coord, _gps, 8, 5)
group.append(rset)
# GPS Fixed Latitude Direction
val = fixed_coord_dir_index(LIST_GPSLATDIR, _gps.gps_fixed_lat_dir)
rs = RadioSettingValueList(LIST_GPSLATDIR, current_index=val)
rset = RadioSetting("gps.gps_fixed_lat_dir",
"Fixed Latitude Direction", rs)
rset.set_apply_callback(apply_fixed_coord_dir, _gps, LIST_GPSLATDIR)
group.append(rset)
# GPS Fixed Longitude
val = fixed_coord_to_float(_gps.gps_fixed_long, 3)
rs = RadioSettingValueFloat(0, 999.99999, val, 0.00001, 5)
rset = RadioSetting("gps.gps_fixed_long", "Fixed Longitude", rs)
rset.set_apply_callback(apply_fixed_coord, _gps, 9, 5)
group.append(rset)
# GPS Fixed Longitude Direction
val = fixed_coord_dir_index(LIST_GPSLONGDIR, _gps.gps_fixed_long_dir)
rs = RadioSettingValueList(LIST_GPSLONGDIR, current_index=val)
rset = RadioSetting("gps.gps_fixed_long_dir",
"Fixed Longitude Direction", rs)
rset.set_apply_callback(apply_fixed_coord_dir, _gps, LIST_GPSLONGDIR)
group.append(rset)
# GPS Fixed Altitude
val = fixed_coord_to_float(_gps.gps_fixed_altitude, 6)
rs = RadioSettingValueFloat(0, 999999.9, val, 0.1, 1)
rset = RadioSetting("gps.gps_fixed_altitude", "Fixed Altitude", rs)
rset.set_apply_callback(apply_fixed_coord, _gps, 8, 1)
group.append(rset)
def get_aprs_settings(self, group):
_aprs = self._memobj.aprs
# APRS Enable
rs = RadioSettingValueBoolean(_aprs.aprs_enable)
rset = RadioSetting("aprs.aprs_enable", "APRS Enable", rs)
group.append(rset)
# APRS Station Mode
rs = RadioSettingValueList(
LIST_APRSSTATIONMODE, current_index=_aprs.aprs_station_mode)
rset = RadioSetting("aprs.aprs_station_mode", "Station Mode", rs)
group.append(rset)
# APRS Callsign
_codeobj = _aprs.aprs_callsign
_code = str(_codeobj).rstrip('\xFF')
rs = RadioSettingValueString(0, 6, _code, True)
rset = RadioSetting("aprs.aprs_callsign", "Callsign", rs)
rset.set_apply_callback(self.apply_string, _aprs)
group.append(rset)
# APRS SSID
rs = RadioSettingValueList(
chirp_common.APRS_SSID, current_index=_aprs.aprs_ssid)
rset = RadioSetting("aprs.aprs_ssid", "SSID", rs)
group.append(rset)
# APRS Symbol Table
rs = RadioSettingValueList(
LIST_APRSSYMBOLTABLE, current_index=_aprs.aprs_symbol_table)
rset = RadioSetting("aprs.aprs_symbol_table", "Symbol Table", rs)
group.append(rset)
# APRS Symbol
rs = RadioSettingValueList(
LIST_APRSSYMBOLS, current_index=_aprs.aprs_symbol)
rset = RadioSetting("aprs.aprs_symbol", "Symbol", rs)
group.append(rset)
# APRS Comment
_codeobj = _aprs.aprs_comment
_code = str(_codeobj).rstrip('\xFF')
rs = RadioSettingValueString(0, 128, _code, True)
rset = RadioSetting("aprs.aprs_comment", "Comment", rs)
rset.set_apply_callback(self.apply_string, _aprs)
group.append(rset)
# APRS MIC-E Enable
rs = RadioSettingValueBoolean(_aprs.aprs_mice_enable)
rset = RadioSetting("aprs.aprs_mice_enable", "MIC-E Enable", rs)
group.append(rset)
# APRS MIC-E Mode
rs = RadioSettingValueList(
LIST_APRESMICEMODE, current_index=_aprs.aprs_mice_mode)
rset = RadioSetting("aprs.aprs_mice_mode", "MIC-E Mode", rs)
group.append(rset)
# APRS Path 1
_codeobj = _aprs.aprs_path_1
_code = str(_codeobj).rstrip('\xFF')
rs = RadioSettingValueString(0, 6, _code, True)
rset = RadioSetting("aprs.aprs_path_1", "Path 1", rs)
rset.set_apply_callback(self.apply_string, _aprs)
group.append(rset)
# APRS Path 1 Count
rs = RadioSettingValueInteger(0, 9, _aprs.aprs_path_1_count)
rset = RadioSetting("aprs.aprs_path_1_count", "Path 1 Count", rs)
rset.set_doc('Value between 0-9')
group.append(rset)
# APRS Path 2
_codeobj = _aprs.aprs_path_2
_code = str(_codeobj).rstrip('\xFF')
rs = RadioSettingValueString(0, 6, _code, True)
rset = RadioSetting("aprs.aprs_path_2", "Path 2", rs)
rset.set_apply_callback(self.apply_string, _aprs)
group.append(rset)
# APRS Path 2 Count
rs = RadioSettingValueInteger(0, 9, _aprs.aprs_path_2_count)
rset = RadioSetting("aprs.aprs_path_2_count", "Path 2 Count", rs)
rset.set_doc('Value between 0-9')
group.append(rset)
# APRS Tx Voltage
rs = RadioSettingValueBoolean(_aprs.aprs_tx_voltage)
rset = RadioSetting("aprs.aprs_tx_voltage", "Tx Voltage", rs)
group.append(rset)
# APRS Tx Star
rs = RadioSettingValueBoolean(_aprs.aprs_tx_star)
rset = RadioSetting("aprs.aprs_tx_star", "Tx Star", rs)
group.append(rset)
# APRS Tx Mileage
rs = RadioSettingValueBoolean(_aprs.aprs_tx_mileage)
rset = RadioSetting("aprs.aprs_tx_mileage", "Tx Mileage", rs)
group.append(rset)
# APRS Rx Channel
rs = RadioSettingValueList(
LIST_APRSCHANNEL, current_index=_aprs.aprs_rx_ch)
rset = RadioSetting("aprs.aprs_rx_ch", "Rx Channel", rs)
group.append(rset)
# APRS Tx Channel
rs = RadioSettingValueList(
LIST_APRSCHANNELCOMBOS, current_index=_aprs.aprs_tx_ch)
rset = RadioSetting("aprs.aprs_tx_ch", "Tx Channel", rs)
group.append(rset)
# APRS PTT Priority
rs = RadioSettingValueList(
LIST_APRSPTTPRIORITY, current_index=_aprs.aprs_ptt_priority)
rset = RadioSetting("aprs.aprs_ptt_priority", "PTT Priority", rs)
group.append(rset)
# APRS PTT Delay
rs = RadioSettingValueList(
LIST_APRSPTTDELAY, current_index=_aprs.aprs_ptt_delay)
rset = RadioSetting("aprs.aprs_ptt_delay", "PTT Delay", rs)
group.append(rset)
# APRS Beacon Popup
rs = RadioSettingValueBoolean(_aprs.aprs_beacon_popup)
rset = RadioSetting("aprs.aprs_beacon_popup", "Beacon Popup", rs)
group.append(rset)
# APRS Beacon After
rs = RadioSettingValueList(
LIST_APRSBEACONAFTER, current_index=_aprs.aprs_beacon_ptt_after)
rset = RadioSetting("aprs.aprs_beacon_ptt_after", "Beacon After", rs)
group.append(rset)
# APRS Smart Beacon
rs = RadioSettingValueList(
LIST_APRSSMARTBEACON, current_index=_aprs.aprs_smart_beacon)
rset = RadioSetting("aprs.aprs_smart_beacon", "Smart Beacon", rs)
group.append(rset)
# APRS Beacon Time Mode
rs = RadioSettingValueBoolean(_aprs.aprs_beacon_time_mode)
rset = RadioSetting("aprs.aprs_beacon_time_mode",
"Beacon Time Mode", rs)
group.append(rset)
# APRS Beacon Time
rs = RadioSettingValueInteger(10, 99999999, _aprs.aprs_beacon_time)
rset = RadioSetting("aprs.aprs_beacon_time", "Beacon Time", rs)
rset.set_doc('Value between 10-99999999')
group.append(rset)
# APRS Queue Beacon
rs = RadioSettingValueBoolean(_aprs.aprs_queue_beacon)
rset = RadioSetting("aprs.aprs_queue_beacon", "Queue Beacon", rs)
group.append(rset)
# APRS Queue Interval
rs = RadioSettingValueInteger(0, 59, _aprs.aprs_queue_interval)
rset = RadioSetting("aprs.aprs_queue_interval", "Queue Interval", rs)
rset.set_doc('Value between 0-59')
group.append(rset)
# APRS Demod Tone
rs = RadioSettingValueBoolean(_aprs.aprs_demod_tone)
rset = RadioSetting("aprs.aprs_demod_tone", "Demod Tone", rs)
group.append(rset)
# APRS Tx Level
rs = RadioSettingValueList(
LIST_APRSLEVEL, current_index=_aprs.aprs_tx_level)
rset = RadioSetting("aprs.aprs_tx_level", "Tx Level", rs)
group.append(rset)
# APRS Rx Level
rs = RadioSettingValueList(
LIST_APRSLEVEL, current_index=_aprs.aprs_rx_level)
rset = RadioSetting("aprs.aprs_rx_level", "Rx Level", rs)
group.append(rset)
# APRS Beacon Save
rs = RadioSettingValueList(
LIST_APRSBEACONSAVE, current_index=_aprs.aprs_beacon_save)
rset = RadioSetting("aprs.aprs_beacon_save", "Beacon Save", rs)
group.append(rset)
# APRS Beacon After Channel
rs = RadioSettingValueList(
LIST_APRSCHANNELCOMBOS,
current_index=_aprs.aprs_beacon_ptt_after_ch)
rset = RadioSetting("aprs.aprs_beacon_ptt_after_ch",
"Beacon After Channel", rs)
group.append(rset)
# APRS Rx Channel Mute
rs = RadioSettingValueBoolean(_aprs.aprs_rx_ch_mute)
rset = RadioSetting("aprs.aprs_rx_ch_mute", "Rx Channel Mute", rs)
group.append(rset)
digi_group = RadioSettingGroup("aprs_digi", "Digipeater")
group.append(digi_group)
# APRS Digipeater Channel
rs = RadioSettingValueList(
LIST_APRSCHANNELCOMBOS, current_index=_aprs.aprs_digi_rep_ch)
rset = RadioSetting("aprs.aprs_digi_rep_ch", "Channel", rs)
digi_group.append(rset)
# APRS Digipeater 1 Enable
rs = RadioSettingValueBoolean(_aprs.aprs_digi_1_enable)
rset = RadioSetting("aprs.aprs_digi_1_enable",
"Digipeater 1 Enable", rs)
digi_group.append(rset)
# APRS Digipeater 1 Name
_codeobj = _aprs.aprs_digi_1_name
_code = str(_codeobj).rstrip('\xFF')
rs = RadioSettingValueString(0, 6, _code, True)
rset = RadioSetting("aprs.aprs_digi_1_name", "Digipeater 1 Name", rs)
rset.set_apply_callback(self.apply_string, _aprs)
digi_group.append(rset)
# APRS Digipeater 2 Enable
rs = RadioSettingValueBoolean(_aprs.aprs_digi_2_enable)
rset = RadioSetting("aprs.aprs_digi_2_enable",
"Digipeater 2 Enable", rs)
digi_group.append(rset)
# APRS Digipeater 2 Name
_codeobj = _aprs.aprs_digi_2_name
_code = str(_codeobj).rstrip('\xFF')
rs = RadioSettingValueString(0, 6, _code, True)
rset = RadioSetting("aprs.aprs_digi_2_name", "Digipeater 2 Name", rs)
rset.set_apply_callback(self.apply_string, _aprs)
digi_group.append(rset)
# APRS Digipeater Wait Before Repeat
rs = RadioSettingValueInteger(0, 9, _aprs.aprs_digi_wait_before_rep)
rset = RadioSetting("aprs.aprs_digi_wait_before_rep",
"Wait Before Repeat", rs)
rset.set_doc('Value between 0-9')
digi_group.append(rset)
# APRS Digipeater Remote Password
_codeobj = _aprs.aprs_digi_remote_password
_code = str(_codeobj).rstrip('\xFF')
rs = RadioSettingValueString(0, 6, _code, True)
rset = RadioSetting("aprs.aprs_digi_remote_password",
"Remote Password", rs)
digi_group.append(rset)
def get_time_settings(self, group):
_settings = self._memobj.settings
# Automatic Power Off
rs = RadioSettingValueBoolean(_settings.auto_power_off)
rset = RadioSetting("auto_power_off",
"Automatic Power Off", rs)
group.append(rset)
# Automatic Power Off Time
rs = RadioSettingValueInteger(0, 235929599,
_settings.auto_power_off_time)
rset = RadioSetting("auto_power_off_time", "Auto Power Off Time", rs)
rset.set_doc('Value between 0-235929599')
group.append(rset)
# Automatic Wake Up
rs = RadioSettingValueBoolean(_settings.auto_wake_up)
rset = RadioSetting("auto_wake_up",
"Automatic Wake Up", rs)
group.append(rset)
# Automatic Wake Up Time
rs = RadioSettingValueInteger(0, 235929599,
_settings.auto_wake_up_time)
rset = RadioSetting("auto_wake_up_time", "Auto Wake Up Time", rs)
rset.set_doc('Value between 0-235929599')
group.append(rset)
def set_settings(self, 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 = self._memobj.settings
setting = element.get_name()
if element.has_apply_callback():
LOG.debug("Using apply callback")
element.run_apply_callback()
elif element.value.get_mutable():
LOG.debug("Setting %s = %s" % (setting, element.value))
setattr(obj, setting, element.value)
except Exception as e:
LOG.debug(element.get_name(), e)
raise
chirp-20261030/chirp/drivers/radtel_rt900.py 0000644 0001750 0001750 00000245451 15213006024 017404 0 ustar hibby hibby # Copyright 2025, 2026 Fred Trimble
# Derived from prior copyrighted work:
# Copyright 2024 Pavel Moravec, OK2MOP
# Copyright 2023 Jim Unroe
#
# CHIRP driver for Ratel RT-900 Series radios
#
# 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 .
import logging
from chirp import (
bitwise,
chirp_common,
directory,
errors,
memmap,
util,
)
from chirp.settings import (
RadioSetting,
RadioSettingGroup,
RadioSettingSubGroup,
RadioSettings,
RadioSettingValueBoolean,
RadioSettingValueInvertedBoolean,
RadioSettingValueInteger,
RadioSettingValueList,
RadioSettingValueString,
RadioSettingValueMap,
MemSetting,
InvalidValueError,
)
from textwrap import dedent
from chirp.drivers import (
baofeng_uv17Pro,
mml_jc8810,
baofeng_common as bfc,
)
import struct
LOG = logging.getLogger(__name__)
# FHSS Code is a 24-bit per-channel value. The OEM CPS represents an
# unset code as 0xFFFFFF in raw memory and as a blank field in the UI.
# When a code is set, the OEM also writes 0xA0 into the adjacent flag
# byte; when the code is cleared, that flag byte is restored to 0xFF.
FHSS_CODE_NULL = 0xFFFFFF
FHSS_CODE_FLAG_ACTIVE = 0xA0
FHSS_CODE_FLAG_NULL = 0xFF
def _fhss_code_to_text(raw_code):
raw = int(raw_code)
if raw == FHSS_CODE_NULL:
return ""
return "%06X" % (raw & 0x7FFFFF)
def _validate_fhss_code(value):
s = str(value).strip()
if s == "":
return value
try:
v = int(s, 16)
except ValueError:
raise InvalidValueError(
"FHSS Code must be a hex value (e.g. 1A2B3C) or blank")
if not (0 <= v <= 0x7FFFFF):
raise InvalidValueError(
"FHSS Code must be between 000000 and 7FFFFF")
return value
MEM_FORMAT = """
struct {
lbcd rxfreq[4]; // 0-3
lbcd txfreq[4]; // 4-7
ul16 rxtone; // 8-9
ul16 txtone; // A-B
u8 unknown1:4, // C
scode:4; // Signaling
u8 unknown2:6, // D
pttid:2; // PTT-ID
u8 scramble:6, // Scramble
txpower:2; // Power Level 0 = H, 1 = L, 2 = M
u8 unknown4:1, // F
narrow:1, // Bandwidth 0 = Wide, 1 = Narrow
encrypt:2, // Encrypt
bcl:1, // BCL
scan:1, // Scan 0 = Skip, 1 = Scan
am_modulation:1, // Per chan AM modulation
learning:1; // FHSS Learning
ul24 code; // 0-2 FHSS Code (little-endian, 0-0x7FFFFF)
u8 code_flag; // 3 0xA0 when Code set, 0xFF when blank
char name[12]; // 4-F 12-character Alpha Tag
} memory[%d];
// vfo settings, 32 bytes
struct vfo {
u8 freq[8]; // vfo freq
ul16 rxtone; // coded RX CTCSS/DTC tone
ul16 txtone; // coded TX CTCSS/DTC tone
u8 unknown0[2];
u8 unknown1:2,
sftd:2, // offset direction, 0 == OFF, 1 == +, 2 == -
scode:4; // scode 0-15
u8 unknown2;
u8 unknown3:3,
scramble:3, // 0b0 == OFF, 0b100 == ON
txpower:2; // TX power, 0b0 == HIGH, 0b1 == Middle,
// 0b2 == LOW
u8 unknown5:1,
widenarr:1, // Bandwidth, 0b0 == WIDE, 0b1 == NARROW
voicepri:2, // Voice privacy encryption, 0b00 == OFF,
// 0b01 == ENCRY1, 2 == ENCRY2, 0b11 == ENCRY3
unknown6:2,
rxmod:1, // RX Modulation, 0b0 == FM, 0b1 == AM
unknown7:1;
u8 unknown8;
u8 step; // vfo tuning step size index: 0 == 2.5K, ...
// 6 == 50K, 7 == 8.33K: 0x8013, 0x8033
u8 offset[6]; // TX freq offset: 0x814, 0x834
u8 unknown9[6];
};
#seekto 0x8000; // vfo a & b
struct {
struct vfo a;
struct vfo b;
} vfo;
#seekto 0x9000;
struct {
u8 unused:4, // 9000
sql:4; // Squelch Level
u8 unused_9001:6, // 9001
save:2; // Battery Save
u8 unused_9002:4, // 9002
vox:4; // VOX Level
u8 unused_9003:4, // 9003
abr:4; // Auto BackLight
u8 unused_9004:7, // 9004
tdr:1; // TDR
u8 unused_9005:5, // 9005
tot:3; // Time-out Timer
u8 unused_9006:7, // 9006
beep:1; // Beep
u8 unused_9007:7, // 9007
voice:1; // Voice Prompt
u8 unused_9008:7, // 9008
language:1; // Language
u8 unused_9009:6, // 9009
dtmfst:2; // DTMF ST
u8 unused_900a:6, // 900A
screv:2; // Scan Mode
u8 unused_900B:4, // 900B
pttid:4; // PTT-ID
u8 unused_900c:5, // 900C
pttlt:3; // PTT Delay
u8 unused_900d:6, // 900D
mdfa:2; // Channel_A Display
u8 unused_900e:6, // 900E
mdfb:2; // Channel_B Display
u8 busy_lock; // Busy Lock
u8 unused_9010:4, // 9010
autolk:4; // Key Auto Lock
u8 unused_9011:6, // 9011
almode:2; // Alarm Mode
u8 unused_9012:7, // 9012
alarmsound:1; // Alarm Sound
u8 unused_9013:6, // 9013
dualtx:2; // Dual TX
u8 unused_9014:7, // 9014
ste:1; // Tail Noise Clear
u8 unused_9015:4, // 9015
rpste:4; // RPT Noise Clear
u8 unused_9016:4, // 9016
rptrl:4; // RPT Noise Delay
u8 unused_9017:6, // 9017
roger:2; // Roger
u8 tx_ab; // TX A/B
u8 unused_9019:7, // 9019
fmradio:1; // FM Radio
u8 unused_901a1:3, // 901A
vfomrb:1, // WorkMode B
unused_901a2:3, //
vfomra:1; // WorkMode A
u8 unused_901b:7, // 901B
kblock:1; // KB Lock
u8 unused_901c:7, // 901C
ponmsg:1; // Power On Msg
u8 bluetooth; // Bluetooth ON/OFF
u8 unused_901e:6, // 901E
tone:2; // Pilot
u8 unknown_901f; // 901F
u8 unused_9020:4, // 9020
voxd:4; // VOX Delay
u8 unused_9021:4, // 9021
menuquit:4; // Menu Auto Quit
u8 unused_9022:7, // 9022
tailcode:1; // Tail Code (RT-470L)
u8 unknown_9023; // 9023
u8 single_mode ; // 9024 Single Mode (RT-900 BT)
u8 unknown_9025; // 9025 Always 0x01 on all RT-900 variants
u8 unknown_9026; // 9026
u8 unknown_9027; // 9027
u8 zone_or_channel; // 9028 Zone or Channel (RT-920 fw V0.16)
// 0: Zone mode, 1: Channel mode
u8 unused_9029:6, // 9029
qtsave:2; // QT Save Type
u8 skey2_sp0; // 902A Skey2 Short RT-900BT
u8 skey2_sp1; // 902B Skey2 Short, Long RT-900BT
u8 skey2_lp; // 902C Skey2 Long, Skey3 Short RT-900BT
u8 skey3_sp; // 902D Skey3 Short
u8 topkey_sp; // 902E Top Key (RT-470L)
u8 unused_902f:6, // 902F
rxendtail:2; // RX END TAIL (RT-470)
u8 am_mode ; // 9030 AM Mode (RT-900)
u8 noise_reduction; // 9031 NOISE REDUCTION
u8 unknown_9032; // 9032
u8 unknown_9033; // 9033
u8 fm_interrupt; // 9034
u8 unknown_9035; // 9035
u8 unknown_9036; // 9036
u8 unknown_9037; // 9037
u8 unknown_9038; // 9038
u8 unknown_9039; // 9039
u8 unknown_903a; // 903a
u8 unknown_903b; // 903b
u8 dis_s_table; // 903b DIS S TABLE (A36plus 8w)
} settings;
#seekto 0xA006;
struct {
u8 unknown_A006; // A006
u8 unused_a007:5, // A007
dtmfon:3; // DTMF Speed (on time)
u8 unused_a008:5, // A008
dtmfoff:3; // DTMF Speed (off time)
} dtmf;
#seekto 0xA020;
struct {
u8 code[6]; // 6-character DTMF PTT-IDs
u8 unused[10];
} pttid[15];
#seekto 0xD000; // radio operationg mode
struct {
u8 radio_mode;
} opmode;
"""
CMD_ACK = b"\x06"
DTCS = tuple(sorted(chirp_common.DTCS_CODES + (645,)))
DTMF_CHARS = "0123456789 *#ABCD"
TXPOWER_HIGH = 0x00
TXPOWER_MID = 0x01
TXPOWER_LOW = 0x02
ABR_LIST = ["On", "5 seconds", "10 seconds", "15 seconds", "20 seconds",
"30 seconds", "1 minute", "2 minutes", "3 minutes"]
ALMODE_LIST = ["On Site", "Send Sound", "Send Code"]
AUTOLK_LIST = ["Off"] + ABR_LIST[1:4]
DTMFSPEED_LIST = ["50 ms", "100 ms", "200 ms", "300 ms", "500 ms"]
DTMFST_LIST = ["Off", "DT-ST", "ANI-ST", "DT+ANI"]
DUALTX_LIST = ["Off", "A", "B"]
LANGUAGE_LIST = ["English", "Chinese"]
MDF_LIST = ["Name", "Frequency", "Channel"]
MENUQUIT_LIST = ["%s seconds" % x for x in range(5, 55, 5)] + ["60 seconds"]
OFF1TO9_LIST = ["Off"] + ["%s" % x for x in range(1, 10)]
PONMSG_LIST = ["Logo", "Voltage"]
PTTID_LIST = ["Off", "BOT", "EOT", "Both"]
PTTIDCODE_LIST = ["%s" % x for x in range(1, 16)]
PTTLT_LIST = ["None", "100 ms"] + \
["%s ms" % x for x in range(200, 1200, 200)]
QTSAVE_LIST = ["All", "TX", "RX"]
ROGER_LIST = ["OFF", "BEEP", "TONE1200"]
RPSTE_LIST = ["Off"] + ["%s ms" % x for x in range(100, 1100, 100)]
SAVE_LIST = ["Off", "Normal Mode", "Super Mode", "Deep Mode"]
SCREV_LIST = ["Time (TO)", "Carrier (CO)", "Search (SE)"]
TONE_LIST = ["1000 Hz", "1450 Hz", "1750 Hz", "2100 Hz"]
TOT_LIST = ["Off", "15S", "30S", "45S", "60S",
"75S"]
VOXD_LIST = ["%s seconds" % str(x / 10) for x in range(5, 21)]
WORKMODE_LIST = ["VFO Mode", "Channel Mode"]
def get_default_features(self):
rf = chirp_common.RadioFeatures()
rf.has_settings = True
rf.has_bank = False
rf.has_ctone = True
rf.has_cross = True
rf.has_rx_dtcs = True
rf.has_tuning_step = False
rf.can_odd_split = True
rf.has_name = True
rf.valid_name_length = 12
rf.valid_characters = self._valid_chars
rf.valid_skips = ["", "S"]
rf.valid_tmodes = ["", "Tone", "TSQL", "DTCS", "Cross"]
rf.valid_cross_modes = ["Tone->Tone", "Tone->DTCS", "DTCS->Tone",
"->Tone", "->DTCS", "DTCS->", "DTCS->DTCS"]
rf.valid_power_levels = self.POWER_LEVELS
rf.valid_duplexes = ["", "-", "+", "split", "off"]
rf.valid_modes = ["FM", "NFM"] # 25 kHz, 12.5 kHz.
rf.valid_dtcs_codes = DTCS
rf.memory_bounds = (1, self._upper)
rf.valid_tuning_steps = [2.5, 5., 6.25, 8.33, 10., 12.5, 20., 25., 50.]
rf.valid_bands = self.VALID_BANDS
return rf
def _enter_programming_mode(radio):
serial = radio.pipe
mml_jc8810._enter_programming_mode(radio)
try:
ident = serial.read(8)
serial.write(radio._cryptsetup)
ack = serial.read(1)
if ack != CMD_ACK:
raise errors.RadioError("Error setting up encryption")
except Exception:
raise errors.RadioError("Error communicating with radio")
if ident not in radio._fingerprint2:
LOG.debug(util.hexprint(ident))
raise errors.RadioError("Radio returned unknown secondary"
" identification string")
def _exit_programming_mode(radio):
mml_jc8810._exit_programming_mode(radio)
def _read_block(radio, block_addr, block_size):
serial = radio.pipe
cmd = struct.pack(">cHb", b'R', block_addr, block_size)
expectedresponse = b"R" + cmd[1:]
try:
serial.write(cmd)
response = serial.read(4 + block_size)
if response[:4] != expectedresponse:
raise Exception("Error reading block %04x." % (block_addr))
block_data = response[4:]
except Exception:
raise errors.RadioError("Failed to read block at %04x" % block_addr)
if block_addr >= 0xf000:
return block_data
else:
return baofeng_uv17Pro._crypt(1, block_data)
def _write_block(radio, block_addr, block_size):
serial = radio.pipe
cmd = struct.pack(">cHb", b'W', block_addr, block_size)
data = radio.get_mmap()[block_addr:block_addr + block_size]
if block_addr < 0xf000:
data = baofeng_uv17Pro._crypt(1, data)
try:
serial.write(cmd + data)
if serial.read(1) != CMD_ACK:
raise Exception("No ACK")
except Exception:
raise errors.RadioError("Failed to send block "
"to radio at %04x" % block_addr)
def do_download(radio):
LOG.debug("Downloading...")
_enter_programming_mode(radio)
data = b""
status = chirp_common.Status()
status.msg = "Cloning from radio"
status.cur = 0
status.max = radio._memsize
for addr in range(0, radio._memsize, radio.BLOCK_SIZE):
status.cur = addr + radio.BLOCK_SIZE
radio.status_fn(status)
block = _read_block(radio, addr, radio.BLOCK_SIZE)
data += block
radio.pipe.log('Sending request for %04x' % addr)
return memmap.MemoryMapBytes(data)
def do_upload(radio):
LOG.debug("Uploading...")
status = chirp_common.Status()
status.msg = "Uploading to radio"
_enter_programming_mode(radio)
status.cur = 0
# calc actual upload size
for r in radio._ranges:
status.max += (r[1] - r[0]) + 1
for start_addr, end_addr in radio._ranges:
for addr in range(start_addr, end_addr, radio.BLOCK_SIZE_UP):
status.cur = addr + radio.BLOCK_SIZE_UP
radio.status_fn(status)
radio.pipe.log('Sending address %04x' % addr)
_write_block(radio, addr, radio.BLOCK_SIZE_UP)
@directory.register
class RT900BT(chirp_common.CloneModeRadio):
# ==========
# Notice to developers:
# The RT-900 BT support in this driver is currently based upon V1.20P
# firmware.
# ==========
"""Radtel RT-900 BT 999"""
VENDOR = "Radtel"
MODEL = "RT-900_BT"
BAUD_RATE = 57600
BLOCK_SIZE = 0x40
BLOCK_SIZE_UP = 0x40
VALID_BANDS = [(18000000, 1000000000)]
_AIRBAND = (108000000, 136975000)
_MIL_AIRBAND = (225000000, 399950000)
_AIRBANDS = [_AIRBAND] # + [_MIL_AIRBAND]
# secret Power-ON + PTT + combos for some modes:
# 8: Super
# OK: GMRS
# EXIT: Factory
# 2: 144-146/430-440
_RADIO_MODE_MAP = [
("Default", 0xff), ("GMRS", 0xa5),
("PMR", 0x66), ("144-146/430-440", 0x55),
("Super", 0x56), ("Factory", 0x00),
("unknown Mode 2", 0x28),
]
POWER_LEVELS = [
chirp_common.PowerLevel("High", watts=8.00),
chirp_common.PowerLevel("Middle", watts=4.00),
chirp_common.PowerLevel("Low", watts=1.00),
]
SKEY_LIST = ["Radio",
"TX Power",
"Scan",
"Search",
"NOAA",
"SOS",
"Switch AM/FM",
"Bluetooth",]
SKEY_SP_LIST = SKEY_LIST
_upper = 999 # fw 1.20 expands from 512 to 999 channels_steps,
_mem_params = (_upper # number of channels
)
_magic = b"PROGRAMBT80U"
_fingerprint = [b"\x01\x36\x01\x80\x04\x00\x05\x20",
b"\x01\x00\x01\x80\x04\x00\x05\x20",
] # fw V1.20P for RT-900 BT
_fingerprint2 = [b"\x02\x00\x02\x60\x01\x03\x30\x04",
] # fw V1.20P for RT-900 BT
_cryptsetup = (b'SEND \x01\x01\x00\x00\x00\x00\x00\x00\x00\x00' +
b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00')
_ranges = [
(0x0000, 0x7CE0), # FW V1.20P,
# 0x7CE0 equals 999 channels of 32 bytes each
# 999 * 32 = 0x7ce0
(0x8000, 0x8040),
(0x9000, 0x9040),
(0xA000, 0xA140),
(0xD000, 0xD040), # Radio mode hidden setting
]
_calibration = (0xF000, 0xF250) # Calibration data
_memsize = 0xF250 # Including calibration data
_has_bt_denoise = True
_has_am_per_channel = True
_has_am_switch = not _has_am_per_channel
_has_single_mode = True
_has_zone_or_channel = False
_has_zone_names = False
_has_sp0 = True # spkey addresses are shifted -1
_has_hf = False
_valid_chars = chirp_common.CHARSET_ALPHANUMERIC + \
"`~!@#$%^&*()-=_+[]\\{}|;':\",./<>?"
_steps = [2.5, 5.0, 6.25, 10.0, 12.5, 20.0, 25.0, 50.0, 8.33]
_step_map = [("2.5 K", 0), ("5.0 K", 1), ("6.25 K", 2), ("8.33 K", 8),
("10.0 K", 3), ("12.5 K", 4), ("20.0 K", 5), ("25.0 K", 6),
("50.0 K", 7),]
_bandwidth_list = ["Wide (25 KHz)", "Narrow (12.5 KHz)"]
_offset_list = ["Off", "+", "-"]
_rx_modulation_list = ["FM", "AM"]
_scode_list = ["%s" % x for x in range(1, 16)]
_voicepri_list = ["Off", "ENCRY1", "ENCRY2", "ENCRY3"]
_scramble_map = [("Off", 0x00), ("On", 0x04)]
_code_list_ctcss = ["%2.1fHz" % x for x in sorted(chirp_common.TONES)]
_code_list_ctcss.insert(0, "Off")
_dcs = tuple(sorted(chirp_common.DTCS_CODES + (645,)))
_code_list_dcsn = ["D%03iN" % x for x in _dcs]
_code_list_dcsi = ["D%03iI" % x for x in _dcs]
_code_list = _code_list_ctcss + _code_list_dcsn + _code_list_dcsi
def sync_in(self):
"""Download from radio"""
try:
data = do_download(self)
except errors.RadioError:
# Pass through any real errors we raise
raise
except Exception:
# If anything unexpected happens, make sure we raise
# a RadioError and log the problem
LOG.exception('Unexpected error during download')
raise errors.RadioError('Unexpected error communicating '
'with the radio')
finally:
_exit_programming_mode(self)
self._mmap = data
self.process_mmap()
def sync_out(self):
"""Upload to radio"""
try:
do_upload(self)
except errors.RadioError:
raise
except Exception:
# If anything unexpected happens, make sure we raise
# a RadioError and log the problem
LOG.exception('Unexpected error during upload')
raise errors.RadioError('Unexpected error communicating '
'with the radio')
finally:
_exit_programming_mode(self)
def get_settings(self):
_dtmf = self._memobj.dtmf
_settings = self._memobj.settings
_radio_mode = self._memobj.opmode.radio_mode
basic = RadioSettingGroup("basic", "Basic Settings")
group = RadioSettings(basic)
# Menu 12: TOT
rs = RadioSettingValueList(TOT_LIST, current_index=_settings.tot)
rset = MemSetting("settings.tot", "Time Out Timer", rs)
rset.set_doc("Set the Time Out Timer for transmit (Seconds).")
basic.append(rset)
# Menu 00: SQL
rs = RadioSettingValueInteger(0, 9, _settings.sql)
rset = MemSetting("settings.sql", "Squelch Level", rs)
rset.set_doc("Set the receive squelch level. Setting to zero will "
"disable the squlech.")
basic.append(rset)
# Menu 13: VOX
rs = RadioSettingValueList(OFF1TO9_LIST, current_index=_settings.vox)
rset = MemSetting("settings.vox", "VOX", rs)
rset.set_doc("Set the VOX sensitivity level.")
basic.append(rset)
# Menu 39: VOX DELAY
rs = RadioSettingValueList(VOXD_LIST, current_index=_settings.voxd)
rset = MemSetting("settings.voxd", "VOX Delay", rs)
rset.set_doc("Set the VOX delay time (Seconds).")
basic.append(rset)
# Menu 15: VOICE
rs = RadioSettingValueBoolean(_settings.voice)
rset = MemSetting("settings.voice", "Voice Prompts", rs)
rset.set_doc("Set the radio voice prompts.")
basic.append(rset)
# Menu 17: LANGUAGE
rs = RadioSettingValueList(LANGUAGE_LIST,
current_index=_settings.language)
rset = MemSetting("settings.language", "Language", rs)
rset.set_doc("Set the radio menu and voice promopts language.")
basic.append(rset)
# Menu 23: ABR
rs = RadioSettingValueList(ABR_LIST, current_index=_settings.abr)
rset = MemSetting("settings.abr", "Auto BackLight", rs)
rset.set_doc("Set the radio Backlight auto switch off time (Seconds).")
basic.append(rset)
# Menu 19: SC-REV
rs = RadioSettingValueList(SCREV_LIST, current_index=_settings.screv)
rset = MemSetting("settings.screv", "Scan Resume Method", rs)
rset.set_doc("Set the Scan Resume method: TO (time scanning), "
"CO (carrier detect), SE (search scanning).")
basic.append(rset)
# Menu 10: SAVE
rs = RadioSettingValueList(SAVE_LIST,
current_index=_settings.save)
rset = MemSetting("settings.save", "Battery Save Mode", rs)
rset.set_doc("Set the Battery power save mode.")
basic.append(rset)
# Menu 42: MDF-A
rs = RadioSettingValueList(MDF_LIST, current_index=_settings.mdfa)
rset = MemSetting("settings.mdfa", "Memory Display Format A", rs)
rset.set_doc("Set A band display mode.")
basic.append(rset)
# Menu 43: MDF-B
rs = RadioSettingValueList(MDF_LIST, current_index=_settings.mdfb)
rset = MemSetting("settings.mdfb", "Memory Display Format B", rs)
rset.set_doc("Set B band display mode.")
basic.append(rset)
# Menu 33: DTMFST (DTMF ST)
rs = RadioSettingValueList(DTMFST_LIST, current_index=_settings.dtmfst)
rset = MemSetting("settings.dtmfst", "DTMF Side Tone", rs)
rset.set_doc("Set how DTMF tones are heard when a PTT ID DTMF Code "
"is sent.")
basic.append(rset)
# Menu 37: PTT-LT
rs = RadioSettingValueList(PTTLT_LIST, current_index=_settings.pttlt)
rset = MemSetting("settings.pttlt", "PTT Delay", rs)
rset.set_doc("Set the PTT delay time.")
basic.append(rset)
# Menu 36: TONE
rs = RadioSettingValueList(TONE_LIST, current_index=_settings.tone)
rset = MemSetting("settings.tone", "Tone-burst Frequency", rs)
rset.set_doc("Set the Tone-burst Frequency.")
basic.append(rset)
# Mneu 29: POWER ON MSG
rs = RadioSettingValueList(PONMSG_LIST, current_index=_settings.ponmsg)
rset = MemSetting("settings.ponmsg", "Power On Message", rs)
rset.set_doc("Set the Power On Message.")
basic.append(rset)
# Menu 46: STE
rs = RadioSettingValueBoolean(_settings.ste)
rset = MemSetting("settings.ste",
"Squelch Tail Eliminate (HT to HT)", rs)
rset.set_doc("Set the Squelch Tail Eliminate for HT to HT "
"communication.")
basic.append(rset)
# Menu 40: RP-STE
rs = RadioSettingValueList(RPSTE_LIST, current_index=_settings.rpste)
rset = MemSetting("settings.rpste",
"Squelch Tail Eliminate (repeater)", rs)
rset.set_doc("Set the Squelch Tail Eliminate time for repeater "
"communication.")
basic.append(rset)
# Menu 41: RPT-RL
rs = RadioSettingValueList(RPSTE_LIST, current_index=_settings.rptrl)
rset = MemSetting("settings.rptrl", "STE Repeater Delay", rs)
rset.set_doc("Set the delay time for Squelch Tail Eliminate "
"when using a repeater.")
basic.append(rset)
# Menu 38: MENU EXIT TIME
rs = RadioSettingValueList(MENUQUIT_LIST,
current_index=_settings.menuquit)
rset = MemSetting("settings.menuquit", "Menu Auto Quit", rs)
rset.set_doc("Set the time for the radio to automatically exit the "
"menu system.")
basic.append(rset)
# Menu 34: AUTOLOCK
rs = RadioSettingValueList(AUTOLK_LIST, current_index=_settings.autolk)
rset = MemSetting("settings.autolk", "Key Auto Lock", rs)
rset.set_doc("Set the time for the radio to automatically lock "
"the keypad.")
basic.append(rset)
# Menu 28: CTCSS SAVE MODE
rs = RadioSettingValueList(QTSAVE_LIST, current_index=_settings.qtsave)
rset = MemSetting("settings.qtsave", "QT Save Type", rs)
rset.set_doc("Set how the CTCSS/DCS codes are saved to a channel.")
basic.append(rset)
# Menu 47: AL-MODE
rs = RadioSettingValueList(ALMODE_LIST, current_index=_settings.almode)
rset = MemSetting("settings.almode", "Alarm Mode", rs)
rset.set_doc("Set how the radio will send an Alarm.")
basic.append(rset)
# Menu 11: ROGER
rs = RadioSettingValueList(ROGER_LIST, current_index=_settings.roger)
rset = MemSetting("settings.roger", "Roger", rs)
rset.set_doc("Set the Roger Beep mode for when the PTT is released.")
basic.append(rset)
# Menu 44: Alarm ON/OFF
rs = RadioSettingValueBoolean(_settings.alarmsound)
rset = MemSetting("settings.alarmsound", "Alarm Sound", rs)
rset.set_doc("Enable to allow the radio to send an alarm.")
basic.append(rset)
if self._has_hf:
s1 = "FM/AM/SSB Radio"
s2 = "Allow access to FM/AM/SSB RX channels on radio."
else:
s1 = "FM Radio"
s2 = "Allow access to FM broadcast RX channels on radio."
rs = RadioSettingValueInvertedBoolean(not _settings.fmradio)
rset = MemSetting("settings.fmradio", s1, rs)
rset.set_doc(s2)
basic.append(rset)
rs = RadioSettingValueBoolean(_settings.kblock)
rset = MemSetting("settings.kblock", "KB Lock", rs)
rset.set_doc("Enable radio keypad locked by default.")
basic.append(rset)
# Menu 16: BEEP PROMPT
rs = RadioSettingValueBoolean(_settings.beep)
rset = MemSetting("settings.beep", "Beep", rs)
rset.set_doc("Enable radio keypad Beep when pressing keys.")
basic.append(rset)
dtmf = RadioSettingGroup("dtmf", "DTMF Settings")
group.append(dtmf)
def apply_code(setting, obj, length):
code = []
for j in range(0, length):
try:
code.append(DTMF_CHARS.index(str(setting.value)[j]))
except IndexError:
code.append(0xFF)
obj.code = code
for i in range(0, 15):
_codeobj = self._memobj.pttid[i].code
_code = "".join([DTMF_CHARS[x] for x in _codeobj if int(x) < 0x1F])
rs = RadioSettingValueString(0, 6, _code, False)
rs.set_charset(DTMF_CHARS)
rset = RadioSetting("pttid/%i.code" % i,
"PTT-ID Code %i" % (i + 1), rs)
rset.set_apply_callback(apply_code, self._memobj.pttid[i], 6)
rset.set_doc("Set the PTT-ID % i Code DTMF sequence." % (i + 1))
dtmf.append(rset)
rs = RadioSettingValueList(DTMFSPEED_LIST,
current_index=_dtmf.dtmfon)
rset = MemSetting("dtmf.dtmfon", "DTMF Speed (On)", rs)
rset.set_doc("Set the duration for how long each DMTF tone is ON.")
dtmf.append(rset)
rs = RadioSettingValueList(DTMFSPEED_LIST,
current_index=_dtmf.dtmfoff)
rset = MemSetting("dtmf.dtmfoff", "DTMF Speed (Off)", rs)
rset.set_doc("Set the duration for how long the pause is between "
"DTMF tones.")
dtmf.append(rset)
rs = RadioSettingValueList(
PTTID_LIST, current_index=_settings.pttid)
rset = MemSetting("settings.pttid", "PTT ID", rs)
rset.set_doc("Set when the PTT-ID is to be sent.")
dtmf.append(rset)
spec = RadioSettingGroup("spec", self.MODEL + " Specific")
group.append(spec)
# secret radio mode setting (no menu). PTT + 8 for Super Mode
rs = RadioSettingValueMap(self._RADIO_MODE_MAP, _radio_mode)
rset = MemSetting("opmode.radio_mode", "Radio Operating Mode", rs)
rset.set_warning(
'This should only be used to change the operating MODE of your '
'radio if you understand the legalities and implications of '
'doing so. The change may enable the radio to transmit on '
'frequencies it is not Type Accepted to do and may be in '
'violation of FCC and other governing agency regulations.\n\n'
'It may make your saved image files incompatible with the radio '
'and non-usable until you change the radio MODE back to the '
'MODE in effect when the image file was saved. After the '
'changed image is uploaded, the radio may have to turned OFF '
'and back ON to have the MODE changes take full effect.\n'
'DO NOT attempt to edit any settings until uploading to and '
'downloading from the radio with the new operating MODE.')
rset.set_doc("Set the radio's Operating Mode.")
spec.append(rset)
if self._has_bt_denoise:
# Menu 46: Noise reduction
rs = RadioSettingValueBoolean(_settings.noise_reduction)
rset = MemSetting("settings.noise_reduction",
"Noise reduction", rs)
rset.set_doc("Set the radio's automatic Noise Reduction feature.")
spec.append(rset)
# Menu 47: Bluetooth
rs = RadioSettingValueBoolean(_settings.bluetooth)
rset = MemSetting("settings.bluetooth", "Bluetooth", rs)
rset.set_doc("Enable Bluetooth (BLE). "
"Bluetooth can be used to program the radio over a "
"BLE connection.")
spec.append(rset)
# Menu 23: PF2 Short
if self._has_sp0:
skey2_sp = _settings.skey2_sp0
key = "settings.skey2_sp0"
else:
skey2_sp = _settings.skey2_sp1
key = "settings.skey2_sp1"
rs = RadioSettingValueList(self.SKEY_SP_LIST,
current_index=skey2_sp)
rset = MemSetting(key, "PF2 Key (Short Press)", rs)
rset.set_doc("Set what a Short Press on the PF2 Key will do.")
spec.append(rset)
# Menu 24: PF2 Long
if self._has_sp0:
skey2_lp = _settings.skey2_sp1
key = "settings.skey2_sp1"
else:
skey2_lp = _settings.skey2_lp
key = "settings.skey2_lp"
rs = RadioSettingValueList(
self.SKEY_LIST, current_index=skey2_lp)
rset = MemSetting(key, "PF2 Key (Long Press)", rs)
rset.set_doc("Set what a Long Press on the PF2 Key will do.")
spec.append(rset)
# Menu 25: PF3 Short
if self._has_sp0:
skey3_sp = _settings.skey2_lp
key = "settings.skey2_lp"
else:
skey3_sp = _settings.skey3_sp
key = "settings.skey3_sp"
rs = RadioSettingValueList(
self.SKEY_LIST, current_index=skey3_sp)
rset = MemSetting(key, "PF3 Key (Short Press)", rs)
rset.set_doc("Set what a Short Press on the PF3 Key will do.")
spec.append(rset)
if self.MODEL not in ["RT-900_BT", "RT-920"]:
# Menu 50: AM/FM Mode
if self._has_am_switch:
rs = RadioSettingValueBoolean(_settings.am_mode)
rset = MemSetting("settings.am_mode", "AM Mode", rs)
rset.set_doc("Enable to set the RX modulation mode to AM.")
spec.append(rset)
# Menu 7: TDR - Dual freq standby
rs = RadioSettingValueBoolean(_settings.tdr)
rset = MemSetting("settings.tdr", "TDR - Dual frequency standby", rs)
rset.set_doc("Set Dual Frequency Standby (Dual Watch).")
spec.append(rset)
if self._has_single_mode:
# Menu 51: Single Mode - Single freq channel display
rs = RadioSettingValueBoolean(_settings.single_mode)
rset = MemSetting("settings.single_mode", "Single Mode", rs)
rset.set_doc("Set radio to display a single frequency/channel.")
spec.append(rset)
if self.MODEL in ["RT-920"]:
# Menu 53: FM Interupt
rs = RadioSettingValueBoolean(_settings.fm_interrupt)
rset = MemSetting("settings.fm_interrupt", "FM Interrupt", rs)
rset.set_doc("While using broadcast FM, "
"allow incoming RX to interrupt the FM "
"broadcast audio.")
spec.append(rset)
if self._has_zone_or_channel:
# Menu 10: Zone or Channel
rs = RadioSettingValueList(self._zone_or_channel_list,
current_index=_settings.zone_or_channel)
rset = MemSetting("settings.zone_or_channel",
"Zone or Channel", rs)
rset.set_warning(
"Changing this setting requires saving the .IMG file and "
"opening it back up to enable/disable the Banks (Zones) "
"preview tab.") # and the Zone Name sub-menu.")
rset.set_doc(
"Set Zone Mode (static banks) or Full Channel "
"(flat) memmory channel mode.")
spec.append(rset)
# VFO A/B settings
abblock = RadioSettingGroup("abblock", "VFO A/B Channel")
spec.append(abblock)
vfo = self._memobj.vfo
# Menu 21: VFO A/B BCL (Busy lock)
rs = RadioSettingValueBoolean(_settings.busy_lock)
rset = MemSetting("settings.busy_lock", "BCL", rs)
rset.set_doc("Set Busy Channel Lockout. When selected, disables PTT "
"when the current frequency is busy.")
abblock.append(rset)
# Menu 30 VFO DTMF code
rs = RadioSettingValueList(
PTTID_LIST,
current_index=_settings.pttid
)
rset = MemSetting("settings.pttid", "DTMF Code", rs)
rset.set_doc("Set when to send PTT-ID DTMF codes")
abblock.append(rset)
# Menu 42: TX-A/B
rs = RadioSettingValueList(
DUALTX_LIST, current_index=_settings.dualtx)
rset = MemSetting("settings.dualtx", "TX-A/B", rs)
rset.set_doc("Set which Band will be used for TX when PTT is pressed.")
abblock.append(rset)
# VFO A channel sub menu
achannel = RadioSettingSubGroup("achannel", "VFO A Channel")
abblock.append(achannel)
# Work Mode A
rs = RadioSettingValueList(WORKMODE_LIST,
current_index=_settings.vfomra)
rset = MemSetting("settings.vfomra", "Work Mode", rs)
rset.set_doc("Set A Band Work Mode.")
achannel.append(rset)
# VFO A Freq
def freq_validate(value):
_radio_mode = self._memobj.opmode.radio_mode
_vhf_lower = 0.0
_uhf_upper = 0.0
if _radio_mode in [0x00, 0x56, 0xff]: # Factory,Super,Default
_vhf_lower = 18.0
else:
_vhf_lower = 108.0
_vhf_upper = 299.99875
_uhf_lower = 300.0
if _radio_mode in [0x00, 0x56]: # Factory,Super
_uhf_upper = 999.99875
else:
_uhf_upper = 519.99875
value = chirp_common.parse_freq(value)
msg = ("Can't be less than %i.0000")
if value < _vhf_lower * 1000000:
raise InvalidValueError(msg % _vhf_lower)
msg = ("Can't be between %i.9975-%i.0000")
if _vhf_upper * 1000000 <= value and \
value < _uhf_lower * 1000000:
raise InvalidValueError(msg % (_vhf_upper - 1, _uhf_lower))
msg = ("Can't be greater than %i.9975")
if value > _uhf_upper * 1000000:
raise InvalidValueError(msg % (_uhf_upper - 1))
return chirp_common.format_freq(value)
def apply_freq(setting, obj):
value = chirp_common.parse_freq(str(setting.value)) / 10
for i in range(7, -1, -1):
obj.freq[i] = value % 10
value /= 10
rs = RadioSettingValueString(0, 10, bfc.bcd_decode_freq(vfo.a.freq))
rs.set_validate_callback(freq_validate)
rset = RadioSetting("vfo.a.freq", "Frequency", rs)
rset.set_apply_callback(apply_freq, vfo.a)
rset.set_doc("Set A Band VFO frequency.")
achannel.append(rset)
# Menu 01: A Step
rs = RadioSettingValueMap(self._step_map, vfo.a.step)
rset = MemSetting("vfo.a.step", "Tuning Step", rs)
rset.set_doc("Set A Band frequency Tuning Step.")
achannel.append(rset)
# Menu 02: TX Power
rs = RadioSettingValueList([str(x) for x in self.POWER_LEVELS],
current_index=vfo.a.txpower)
rset = MemSetting("vfo.a.txpower", "TX Power", rs)
rset.set_doc("Set A Band TX Power.")
achannel.append(rset)
# Menu 05: Wide/Narrow Band
rs = RadioSettingValueList(self._bandwidth_list,
current_index=vfo.a.widenarr)
rset = MemSetting("vfo.a.widenarr", "Bandwidth", rs)
rset.set_doc("Set A Band Bandwidth.")
achannel.append(rset)
# Menu 12,13: RX ctcss/dtsc
rs = RadioSettingValueList(self._code_list,
current_index=self._code_list.index
(self.decode(vfo.a.rxtone)))
rset = RadioSetting("vfo.a.rxtone", "RX CTCSS/DCS", rs)
rset.set_apply_callback(self.apply_vfo_tone,
self._memobj.vfo.a, "rxtone")
rset.set_doc("Set A Band RX CTCSS or DCS code.")
achannel.append(rset)
# Menu 14,15: TX ctcss/dtsc
rs = RadioSettingValueList(self._code_list,
current_index=self._code_list.index
(self.decode(vfo.a.txtone)))
rset = RadioSetting("vfo.a.txtone", "TX CTCSS/DCS", rs)
rset.set_apply_callback(self.apply_vfo_tone,
self._memobj.vfo.a, "txtone")
rset.set_doc("Set A Band TX CTCSS or DCS code.")
achannel.append(rset)
# Menu 16: Voice Privacy (encryption)
rs = RadioSettingValueList(self._voicepri_list,
current_index=vfo.a.voicepri)
rset = RadioSetting("vfo.a.voicepri",
"Voice Privacy - Subtone Encryption", rs)
rset.set_doc("Set A Band Voice Privacy - subtone voice encryption.")
achannel.append(rset)
def convert_bytes_to_offset(bytes):
real_offset = 0
for byte in bytes:
real_offset = (real_offset * 10) + byte
return chirp_common.format_freq(real_offset * 1000)
def apply_offset(setting, obj):
value = chirp_common.parse_freq(str(setting.value)) / 1000
for i in range(5, -1, -1):
obj.offset[i] = value % 10
value /= 10
# Menu 26: Offset
rs = RadioSettingValueString(0, 10,
convert_bytes_to_offset(vfo.a.offset))
rset = RadioSetting("vfo.a.offset", "Offset (MHz)", rs)
rset.set_apply_callback(apply_offset, vfo.a)
rset.set_doc("Set A Band TX frequency offset value.")
achannel.append(rset)
# Menu 27: Offset direction
rs = RadioSettingValueList(self._offset_list,
current_index=vfo.a.sftd)
rset = MemSetting("vfo.a.sftd", "Offset Direction", rs)
rset.set_doc("Set A Band TX frequency offset direction.")
achannel.append(rset)
# Menu 29: S-Code DTMF 1-15
rs = RadioSettingValueList(self._scode_list,
current_index=vfo.a.scode)
rset = MemSetting("vfo.a.scode", "S-CODE", rs)
rset.set_doc("Set the A Band DTMF code ID to use for PTT ID.")
achannel.append(rset)
# Menu 45: Scramble
rs = RadioSettingValueMap(self._scramble_map, vfo.a.scramble)
rset = MemSetting("vfo.a.scramble", "Scramble", rs)
rset.set_doc("Set A Band voice Scramble.")
achannel.append(rset)
# Menu 50: RX Modulation
if self._has_am_per_channel:
rs = RadioSettingValueList(self._rx_modulation_list,
current_index=vfo.a.rxmod)
rset = MemSetting("vfo.a.rxmod", "RX Modulation", rs)
rset.set_doc("Set A Band RX Modulation.")
achannel.append(rset)
# VFO B channel sub menu
bchannel = RadioSettingSubGroup("bchannel", "VFO B Channel")
abblock.append(bchannel)
# Work Mode B
rs = RadioSettingValueList(WORKMODE_LIST,
current_index=_settings.vfomrb)
rset = MemSetting("settings.vfomrb", "Work Mode", rs)
rset.set_doc("Set B Band Work Mode.")
bchannel.append(rset)
# VFO B Freq
rs = RadioSettingValueString(0, 10,
bfc.bcd_decode_freq(vfo.b.freq))
rs.set_validate_callback(freq_validate)
rset = RadioSetting("vfo.b.freq", "Frequency", rs)
rset.set_apply_callback(apply_freq, vfo.b)
rset.set_doc("Set B Band VFO frequency.")
bchannel.append(rset)
# Menu 01: B Step
rs = RadioSettingValueMap(self._step_map, vfo.b.step)
rset = MemSetting("vfo.b.step", "Tuning Step", rs)
rset.set_doc("Set B Band frequency Tuning Step.")
bchannel.append(rset)
# Menu 02: TX Power
rs = RadioSettingValueList([str(x) for x in self.POWER_LEVELS],
current_index=vfo.b.txpower)
rset = MemSetting("vfo.b.txpower", "TX Power", rs)
rset.set_doc("Set B Band TX Power.")
bchannel.append(rset)
# Menu 05: Wide/Narrow Band
rs = RadioSettingValueList(self._bandwidth_list,
current_index=vfo.b.widenarr)
rset = MemSetting("vfo.b.widenarr", "Bandwidth", rs)
rset.set_doc("Set B Band Bandwidth.")
bchannel.append(rset)
# Menu 12,13: RX ctcss/dtsc
rs = RadioSettingValueList(self._code_list,
current_index=self._code_list.index
(self.decode(vfo.b.rxtone)))
rset = RadioSetting("vfo.b.rxtone", "RX CTCSS/DCS", rs)
rset.set_apply_callback(self.apply_vfo_tone,
self._memobj.vfo.b, "rxtone")
rset.set_doc("Set B Band RX CTCSS or DCS code.")
bchannel.append(rset)
# Menu 14,15: TX ctcss/dtsc
rs = RadioSettingValueList(self._code_list,
current_index=self._code_list.index
(self.decode(vfo.b.txtone)))
rset = RadioSetting("vfo.b.txtone", "TX CTCSS/DCS", rs)
rset.set_apply_callback(self.apply_vfo_tone,
self._memobj.vfo.b, "txtone")
rset.set_doc("Set B Band TX CTCSS or DCS code.")
bchannel.append(rset)
# Menu 16: Voice Privacy (encryption)
rs = RadioSettingValueList(self._voicepri_list,
current_index=vfo.b.voicepri)
rset = MemSetting("vfo.b.voicepri",
"Voice Privacy - Subtone Encryption", rs)
rset.set_doc("Set B Band Voice Privacy - subtone voice encryption.")
bchannel.append(rset)
# Menu 26: Offset
rs = RadioSettingValueString(0, 10,
convert_bytes_to_offset(vfo.b.offset))
rset = RadioSetting("vfo.b.offset", "Offset (MHz)", rs)
rset.set_apply_callback(apply_offset, vfo.b)
rset.set_doc("Set B Band TX frequency offset value.")
bchannel.append(rset)
# Menu 27: Offset direction
rs = RadioSettingValueList(self._offset_list, current_index=vfo.b.sftd)
rset = MemSetting("vfo.b.sftd", "Offset Direction", rs)
rset.set_doc("Set B Band TX frequency offset direction.")
bchannel.append(rset)
# Menu 29: S-Code DTMF 1-15
rs = RadioSettingValueList(
self._scode_list, current_index=vfo.b.scode)
rset = MemSetting("vfo.b.scode", "S-CODE", rs)
rset.set_doc("Set the B Band DTMF code ID to use for PTT ID.")
bchannel.append(rset)
# Menu 45: Scramble
rs = RadioSettingValueMap(self._scramble_map, vfo.b.scramble)
rset = MemSetting("vfo.b.scramble", "Scramble", rs)
rset.set_doc("Set B Band voice Scramble.")
bchannel.append(rset)
# Menu 50: RX Modulation
if self._has_am_per_channel:
rs = RadioSettingValueList(self._rx_modulation_list,
current_index=vfo.b.rxmod)
rset = MemSetting("vfo.b.rxmod", "RX Modulation", rs)
rset.set_doc("Set B Band RX Modulation.")
bchannel.append(rset)
# SSB Settings, RT-920, BJ7800 Only
if self._has_hf:
ssbblock = RadioSettingGroup("ssbblock", "FM/AM/SSB Modulation")
spec.append(ssbblock)
# HF mode submenu
modes = RadioSettingSubGroup("modes", "Modes")
ssbblock.append(modes)
# SSB Work Mode
ssbwm = self._memobj.ssb_settings
rs = RadioSettingValueList(
self._ssb_workmode_list,
current_index=ssbwm.workmode
)
rset = MemSetting("ssb_settings.workmode", "Work Mode", rs)
rset.set_doc("Set HF Workmode.")
modes.append(rset)
# SSB Modulation Mode
ssb = self._memobj.ssb
rs = RadioSettingValueList(
self._ssb_modulation_list,
current_index=ssb.modulation
)
rset = MemSetting("ssb.modulation", "Modulation Mode", rs)
rset.set_doc("Set HF RX Modulation Mode.")
modes.append(rset)
# AM mode submenu
amsettings = RadioSettingSubGroup("amsettings", "AM Settings")
ssbblock.append(amsettings)
# AM Step Freq
stepfreq = self._memobj.stepfreq
rs = RadioSettingValueList(
self._am_step_freq_list,
current_index=stepfreq.am
)
rset = MemSetting("stepfreq.am", "Step Freq", rs)
rset.set_doc("Set HF AM RX Step Frequency.")
amsettings.append(rset)
# AM RX Gain
rs = RadioSettingValueList(
self._rx_gain_list,
current_index=ssb.am_rxgain
)
rset = MemSetting("ssb.am_rxgain", "RX Gain", rs)
rset.set_doc("Set HF AM RX Gain value.")
amsettings.append(rset)
# SSB mode submenu
ssbsettings = RadioSettingSubGroup("ssbsettings", "SSB Settings")
ssbblock.append(ssbsettings)
# SSB Step Freq
rs = RadioSettingValueList(
self._ssb_step_freq_list,
current_index=stepfreq.ssb
)
rset = MemSetting("stepfreq.ssb", "Step Freq", rs)
rset.set_doc("Set HF SSB RX Step Frequency.")
ssbsettings.append(rset)
# SSB RX Gain
rs = RadioSettingValueList(
self._rx_gain_list,
current_index=stepfreq.ssb_rxgain
)
rset = MemSetting("stepfreq.ssb_rxgain", "RX Gain", rs)
rset.set_doc("Set HF SSB RX Gain value.")
ssbsettings.append(rset)
return group
@classmethod
def get_prompts(cls):
rp = chirp_common.RadioPrompts()
rp.experimental = \
('This driver is a beta version ONLY for the RT-900 BT'
' running Firmware V.1.20P.\n'
'\n'
'Please save an unedited copy of your first successful\n'
'download to a CHIRP Radio Images(*.img) file.\n\n'
'PROCEED AT YOUR OWN RISK!'
)
rp.pre_download = (dedent("""\
1. Turn radio off.
2. Connect cable to mic/spkr connector.
3. Make sure connector is firmly connected.
4. Turn radio on (volume may need to be set at 100%).
5. Ensure that the radio is tuned to channel with no activity.
6. Click OK to download image from device."""))
rp.pre_upload = (dedent("""\
1. Turn radio off.
2. Connect cable to mic/spkr connector.
3. Make sure connector is firmly connected.
4. Turn radio on (volume may need to be set at 100%).
5. Ensure that the radio is tuned to channel with no activity.
6. Click OK to upload image to device."""))
return rp
def _decode_tone(self, memval):
"""Parse the tone data to decode from memval, it returns:
Mode (''|DTCS|Tone), Value (None|###), Polarity (None,N,R)"""
pol = None
if memval in [0, 0xFFFF]:
return "", None, None
elif memval >= 0x0258:
a = memval / 10.0
return "Tone", a, pol
else:
if memval > 0x69:
index = memval - 0x6A
pol = "R"
else:
index = memval - 1
pol = "N"
tone = DTCS[index]
return "DTCS", tone, pol
def _encode_tone(self, mem, memval, mode, value, pol):
"""Parse the tone data to encode from UI to mem"""
match mode:
case "" | None:
memval.fill_raw(b"\x00")
case "Tone" | "TSQL":
memval.set_value(int(value * 10))
case "DTCS":
try:
index = DTCS.index(value)
if pol == "N":
index += 1
else:
index += 0x6A
memval.set_value(index)
except ValueError:
msg = "DTCS Code '%d' is not supported" % value
LOG.error(msg)
raise errors.RadioError(msg)
case _:
raise Exception("Internal error: invalid mode '%s'" % mode)
def decode(self, code):
"""decode radio stored VFO tone code into human readable form"""
if code in [0, 0xffff]:
tone = 'Off' # Off
elif code >= 0x0258: # CTCSS
tone = "%2.1fHz" % (int(code) / 10.0)
elif code <= 0x0258: # DCS
if code > 0x69: # inverse
index = code - 0x6a
dtcs_pol = 'I'
else: # normal
index = code - 1
dtcs_pol = 'N'
tone = 'D' + "%03i" % (self._dcs[index]) + dtcs_pol
else:
msg = "Invalid tone code from radio: %s" % hex(code)
LOG.exception(msg)
raise InvalidValueError(msg)
return tone
def apply_vfo_tone(self, setting, obj, which):
"""encode VFO tone from UI into radio storable tone code
and apply to mem"""
mem = getattr(obj, which)
try:
tone = self._code_list[int(setting.value)]
if tone == "Off":
code = 0
elif tone.endswith('Hz'): # CTCSS
code = int(float(tone[0:tone.index('Hz')]) * 10)
elif tone.startswith('D'): # DCS
index = self._dcs.index(int(tone[1:4]))
if tone.endswith('I'): # inverse
code = index + 0x6a
elif tone.endswith('N'): # normal
code = index + 1
except (IndexError, ValueError):
msg = "Unknown CTCSS/DTC tone: %s" % tone
LOG.exception(msg)
raise InvalidValueError(msg)
mem.set_value(code)
def get_features(self):
rf = get_default_features(self)
rf.valid_bands = self.VALID_BANDS
rf.valid_modes = ["FM", "NFM", "AM", "NAM"] # 25kHz, 12.5kHz, AM, NAM
rf.valid_tuning_steps = self._steps
return rf
def get_raw_memory(self, number):
return repr(self._memobj.memory[number - 1])
def get_memory(self, number):
"""Get the mem representation from the radio image"""
_mem = self._memobj.memory[number - 1]
# Create a high-level memory object to return to the UI
mem = chirp_common.Memory()
# Memory number
mem.number = number
if _mem.get_raw()[:1] == b"\xff":
mem.empty = True
return mem
# Freq and offset
mem.freq = int(_mem.rxfreq) * 10
if mem.freq == 0:
mem.empty = True
# tx freq can be blank
if _mem.txfreq.get_raw() == b"\xff\xff\xff\xff":
# TX freq not set
mem.offset = 0
mem.duplex = "off"
else:
# TX freq set
offset = (int(_mem.txfreq) * 10) - mem.freq
if offset != 0:
if chirp_common.is_split(self.get_features().valid_bands,
mem.freq, int(_mem.txfreq) * 10):
mem.duplex = "split"
mem.offset = int(_mem.txfreq) * 10
elif offset < 0:
mem.offset = abs(offset)
mem.duplex = "-"
elif offset > 0:
mem.offset = offset
mem.duplex = "+"
else:
mem.offset = 0
for char in _mem.name:
if str(char) == "\xFF":
char = " " # may have 0xFF mid-name
mem.name += str(char)
mem.name = mem.name.rstrip().replace('\x00', '')
txtone = rxtone = None
txtone = self._decode_tone(_mem.txtone)
rxtone = self._decode_tone(_mem.rxtone)
chirp_common.split_tone_decode(mem, txtone, rxtone)
if not _mem.scan:
mem.skip = "S"
_levels = self.POWER_LEVELS
if _mem.txpower == TXPOWER_HIGH:
mem.power = _levels[0]
elif _mem.txpower == TXPOWER_MID:
mem.power = _levels[1]
elif _mem.txpower == TXPOWER_LOW:
mem.power = _levels[2]
else:
LOG.error('%s: get_mem: unhandled power level: 0x%02x' %
(mem.name, _mem.txpower))
mem.mode = _mem.narrow and "NFM" or "FM"
if chirp_common.in_range(
mem.freq, self._AIRBANDS
) or _mem.am_modulation == 0b1:
mem.mode = _mem.narrow and "NAM" or 'AM'
mem.extra = RadioSettingGroup("Extra", "extra")
# Encryption
rs = RadioSettingValueList(
self._voicepri_list,
current_index=_mem.encrypt
)
rset = RadioSetting("encrypt", "Voice Privacy", rs)
mem.extra.append(rset)
# Scramble
rs = RadioSettingValueMap(self._scramble_map, _mem.scramble)
rset = RadioSetting("scramble", "Scramble", rs)
mem.extra.append(rset)
# BCL (Busy Channel Lockout)
rs = RadioSettingValueBoolean(_mem.bcl)
rset = RadioSetting("bcl", "BCL", rs)
rset.set_doc("Set Busy Channel Lockout. When selected, disables PTT "
"when the current channel is busy.")
mem.extra.append(rset)
# LearnFHSS (per-channel learn / FHSS flag). The OEM CPS labels
# this column "LearnFHSS"; the open-source firmware reads the
# same bit as chFlag3.b0 / fhssFlag (Core/Radio.c).
rs = RadioSettingValueBoolean(_mem.learning)
rset = RadioSetting("learning", "Learn FHSS", rs)
rset.set_doc("Enable to allow automatic FHSS code selection.")
mem.extra.append(rset)
# FHSS Code (24-bit little-endian, range 0x000000-0x7FFFFF).
# Displayed and accepted as a 6-digit uppercase hex string, or
# blank to clear the code (raw 0xFFFFFF). Validation is hoisted
# to a module-level function so this RadioSetting stays
# picklable for clipboard copy.
rs = RadioSettingValueString(0, 6, _fhss_code_to_text(_mem.code))
rs.set_validate_callback(_validate_fhss_code)
rset = RadioSetting("fhss_code", "FHSS Code (hex)", rs)
rset.set_doc("Enter manual FHSS Code using HEX digits.")
mem.extra.append(rset)
# PTT-ID
rs = RadioSettingValueList(PTTID_LIST, current_index=_mem.pttid)
rset = RadioSetting("pttid", "PTT ID", rs)
rset.set_doc("Set when the PTT-ID is to be sent.")
mem.extra.append(rset)
# Signal (DTMF Encoder Group #)
rs = RadioSettingValueList(PTTIDCODE_LIST, current_index=_mem.scode)
rset = RadioSetting("scode", "PTT ID Code", rs)
rset.set_doc("Set the DTMF code ID to use for PTT ID.")
mem.extra.append(rset)
return mem
def set_memory(self, mem):
_mem = self._memobj.memory[mem.number - 1]
if mem.empty:
_mem.fill_raw(b"\xff")
return
_mem.set_raw("\x00" * 16 + "\xff" * 16)
_mem.rxfreq = mem.freq / 10
if mem.duplex == "off":
_mem.txfreq.fill_raw(b"\xff")
elif mem.duplex == "split":
_mem.txfreq = mem.offset / 10
elif mem.duplex == "+":
_mem.txfreq = (mem.freq + mem.offset) / 10
elif mem.duplex == "-":
_mem.txfreq = (mem.freq - mem.offset) / 10
else:
_mem.txfreq = mem.freq / 10
_namelength = self.get_features().valid_name_length
for i in range(_namelength):
try:
_mem.name[i] = mem.name[i]
except IndexError:
_mem.name[i] = "\xff"
txmode = rxmode = ''
txval = rxval = 0
txpol = rxpol = None
((txmode, txval, txpol),
(rxmode, rxval, rxpol)) = chirp_common.split_tone_encode(mem)
self._encode_tone(mem, _mem.txtone, txmode, txval, txpol)
self._encode_tone(mem, _mem.rxtone, rxmode, rxval, rxpol)
_mem.scan = mem.skip != "S"
_mem.narrow = mem.mode == "NFM"
_levels = self.POWER_LEVELS
if mem.power is None:
_mem.txpower = TXPOWER_HIGH
elif mem.power == _levels[0]:
_mem.txpower = TXPOWER_HIGH
elif mem.power == _levels[1]:
_mem.txpower = TXPOWER_MID
elif mem.power == _levels[2]:
_mem.txpower = TXPOWER_LOW
else:
LOG.error('%s: set_mem: unhandled power level: %s' %
(mem.name, mem.power))
match mem.mode:
case 'AM': # radio allows AM per channel
_mem.am_modulation = 0b1
_mem.narrow = 0b0
case "NAM": # radio allows narrow AM per channel
_mem.am_modulation = 0b1
_mem.narrow = 0b1
case 'FM':
_mem.am_modulation = 0b0
_mem.narrow = 0b0
case 'NFM':
_mem.am_modulation = 0b0
_mem.narrow = 0b1
for setting in mem.extra:
if setting.get_name() == 'fhss_code':
# Mirror OEM CPS: blank input clears Code (0xFFFFFF) and
# restores the adjacent flag byte to 0xFF; any value
# writes Code and sets the flag byte to 0xA0.
s = str(setting.value).strip()
if s == "":
_mem.code = FHSS_CODE_NULL
_mem.code_flag = FHSS_CODE_FLAG_NULL
else:
_mem.code = int(s, 16)
_mem.code_flag = FHSS_CODE_FLAG_ACTIVE
else:
setattr(_mem, setting.get_name(), setting.value)
def validate_memory(self, mem):
msgs = []
in_range = chirp_common.in_range
AM_mode = 'AM' in mem.mode
if in_range(mem.freq, self._AIRBANDS) and not AM_mode:
msgs.append(chirp_common.ValidationMessage(
_('Frequency in this range requires AM mode')))
return msgs + super().validate_memory(mem)
def set_settings(self, settings):
# apply all Memsettings
all_other_settings = settings.apply_to(self._memobj)
for setting in all_other_settings:
if setting.has_apply_callback():
# use callbacks on Radiosettings that need postprocessing
setting.run_apply_callback()
def process_mmap(self):
mem_format = MEM_FORMAT % self._mem_params
self._memobj = bitwise.parse(mem_format, self._mmap)
@directory.register
class RT900(RT900BT):
# ==========
# Notice to developers:
# The RT-900 support in this driver is currently based upon stock
# factory firmware, V0.14P
# ==========
"""Radtel RT-900 (without Bluetooth)"""
VENDOR = "Radtel"
MODEL = "RT-900"
_has_bt_denoise = False
_has_am_per_channel = False
_has_am_switch = not _has_am_per_channel
_has_single_mode = False
_has_hf = False
_upper = 999 # fw V1.14P expands from 512 to 999 channels
SKEY_LIST = ["FM Radio",
"TX Power Level",
"Scan",
"Search",
"NOAA Weather",
"SOS",
"Flashlight" # Not used in BT model
]
SKEY_SP_LIST = SKEY_LIST
def get_features(self):
rf = super().get_features()
# So far no firmware update to test new range availability yet
# rf.valid_bands = [(18000000, 1000000000)]
return rf
@classmethod
def get_prompts(cls):
rp = super().get_prompts()
rp.experimental = \
('This driver is a beta version for the RT-900'
' running Firmware V0.14P\n'
'\n'
'Please save an unedited copy of your first successful\n'
'download to a CHIRP Radio Images(*.img) file.\n\n'
'PROCEED AT YOUR OWN RISK!'
)
return rp
@directory.register
class RT910BT(RT900BT):
# ==========
# Notice to developers:
# The RT-910 BT support in this driver is currently based upon V0.24P
# firmware with 15 banks/zones of 64 channels steps.
# ==========
"""Radtel RT-910_BT 960"""
VENDOR = "Radtel"
MODEL = "RT-910_BT"
SKEY_LIST = ["Radio",
"TX Power",
"Scan",
"Search",
"NOAA",
"SOS",
"Spectrum"]
SKEY_SP_LIST = SKEY_LIST + ["PTTB"]
_upper = 960 # fw V0.24P supports 960 channels
_mem_params = (_upper, # number of channels
)
_ranges = [
(0x0000, 0x7800), # 15 zones of 64 frequencies,
# equals 960 channels of 32 bytes each
# 15 * 64 * 32 = 0x7800
(0x8000, 0x8040),
(0x9000, 0x9040),
(0xA000, 0xA140),
(0xD000, 0xD040) # Radio mode hidden setting
]
_has_bt_denoise = True
_has_am_per_channel = True
_has_am_switch = not _has_am_per_channel
_has_single_mode = False
def get_bank_model(self):
return chirp_common.StaticBankModel(self, banks=15)
def get_features(self):
rf = super().get_features()
rf.has_bank = True # Firmware V0.24P supports 15
# "static zones" of 64 frequencies
rf.valid_tuning_steps = self._steps
return rf
@classmethod
def get_prompts(cls):
rp = super().get_prompts()
rp.experimental = \
('This driver is a beta version for the RT-910 BT'
' running Firmware V0.24P\n'
'\n'
'Please save an unedited copy of your first successful\n'
'download to a CHIRP Radio Images(*.img) file.\n\n'
'PROCEED AT YOUR OWN RISK!'
)
return rp
@directory.register
class RT910(RT910BT):
# ==========
# Notice to developers:
# The RT-910 support in this driver is currently based upon V0.11
# firmware with 15 banks/zones of 64 channel steps.
# ==========
"""Radtel RT-910 960 (without Bluetooth)"""
VENDOR = "Radtel"
MODEL = "RT-910"
_upper = 960 # fw V0.11 supports 960 channels
_mem_params = (_upper, # number of channels
)
_ranges = [
(0x0000, 0x7800), # 15 zones of 64 frequencies,
# equals 960 channels of 32 bytes each
# 15 * 64 * 32 = 0x7800
(0x8000, 0x8040),
(0x9000, 0x9040),
(0xA000, 0xA140),
(0xD000, 0xD040) # Radio mode hidden setting
]
_has_bt_denoise = False
_has_am_per_channel = True
_has_am_switch = not _has_am_per_channel
_has_single_mode = True
_has_hf = False
def get_bank_model(self):
return chirp_common.StaticBankModel(self, banks=15)
@classmethod
def get_prompts(cls):
rp = super().get_prompts()
rp.experimental = \
('This driver is a beta version for the RT-910'
' Non-Bluetooth running Firmware V0.11\n'
'\n'
'Please save an unedited copy of your first successful\n'
'download to a CHIRP Radio Images(*.img) file.\n\n'
'PROCEED AT YOUR OWN RISK!'
)
return rp
class RT920Bank(chirp_common.NamedBank):
"""RT-920 Bank"""
def get_name(self):
"""decode bank name from char array into string"""
_bank = self._model._radio._memobj.zones[self._index - 1]
name = str(_bank.name).rstrip(" \n\t\x00\xff") # remove filler chars
if len(name) == 0:
name = "ZONE %i" % self._index
return name
def set_name(self, name):
"""encode bank name into \xff filled char array"""
_bank = self._model._radio._memobj.zones[self._index - 1]
_bank.name = name[:len(_bank.name)].rstrip()\
.ljust(len(_bank.name), "\xff")
class RT920BankModel(chirp_common.StaticBankModel):
"""RT-920 Bank Model"""
def __init__(self, radio, name, banks):
super(RT920BankModel, self).__init__(radio, name, banks)
self._num_banks = banks
self._rf = radio.get_features()
self._banks = []
for i in range(self._num_banks):
self._banks.append(RT920Bank(self, i + 1, "ZONE %i" % (i + 1)))
@directory.register
class RT920(RT900BT):
# ==========
# Notice to developers:
# The RT-920 support in this driver is currently based upon V0.18
# firmware with 10 banks/zones of 99 channels.
# ==========
"""Radtel RT-920"""
VENDOR = "Radtel"
MODEL = "RT-920"
_magic = b"PROGRAMBT11U" # RT-920
# step of 1.25 to allow UK CB Freqs
_steps = [1.25, 2.5, 5.0, 6.25, 8.33, 10.0, 12.5, 20.0, 25.0, 50.0]
SKEY_LIST = ["Radio",
"TX Power",
"Scan",
"Search",
"NOAA",
"SOS",
"Spectrum"]
SKEY_SP_LIST = SKEY_LIST + ["PTTB"]
_zone_or_channel_list = ["Zone Mode", "Full Channel"]
_ssb_modulation_list = [
"FM",
"AM",
"LSB",
"USB",
"CW",
]
_ssb_workmode_list = [
"VFO Mode",
"CH Mode",
]
_am_step_freq_list = [
"1K",
"5K",
"9K",
"10K",
"100K",
]
_ssb_step_freq_list = [
"1K",
"5K",
"10K",
"100K",
"500K",
"1000K",
]
_rx_gain_list = ["AGC"] + \
["%ddB" % x for x in range(0, -36, -1)]
_upper = 990 # fw V0.18 supports 990 channels
_mem_params = (_upper, # number of channels
)
_banks = 10
_ranges = [
(0x0000, 0x7BC0), # 10 zones of 99 frequencies,
# equals 990 channels of 32 bytes each
# 10 * 99 * 32 = 0x7BC0
(0x8000, 0x8040),
(0x9000, 0x9040),
(0xA000, 0xA140),
(0xB000, 0xB400), # FM, AM, HF frequencies
(0xC000, 0xC400), # FM, AM, HF names
(0xC800, 0XC8A0), # static bank names, 10 banks * 16 bytes ea = 0xA0
(0xD000, 0xD040), # Radio mode hidden setting
]
_rt920_specific_fmt = """
#seekto 0xb000;
// 2 byte fm vfo freq
struct {
ul16 freq;
} fm_vfo;
// 15ea 2 byte fm chan freq
struct {
ul16 freq;
} fm_freqs[%d];
#seekto 0xb021; // SSB settings
struct {
u8 workmode; // VFO/Ch
} ssb_settings;
#seekto 0xb022;
// 2 byte am vfo freq
struct {
ul16 freq;
} am_vfo;
// 15ea 2 byte am chan freq
struct {
ul16 freq;
} am_freqs[%d];
#seekto 0xb043; // ssb
struct {
u8 modulation; // modulation mode
u8 am_rxgain; // AM RX gain
} ssb;
#seekto 0xb045;
// 5 byte hf vfo chan
struct {
ul16 freq;
u8 bandwidth;
il16 beatfreq; // can be negitive
} hf_vfo;
// 15ea 5 byte hf chans
struct {
ul16 freq;
u8 bandwidth;
il16 beatfreq; // can be negitive
} hf_freqs[%d];
#seekto 0xb096;
struct {
u8 ssb;
u8 am;
u8 ssb_rxgain;
} stepfreq;
#seekto 0xc010; // 15ea 16 byte fm chan names
struct {
char name[12];
u8 unused[4];
} fm_names[%d];
#seekto 0xc110; // 15ea 16 byte am chan names (RT-920 fw v 0.xx)
struct {
char name[12];
u8 unused[4];
} am_names[%d];
#seekto 0xc210; // 15ea 16 byte ssb chan names (RT-920 fw v 0.xx)
struct {
char name[12];
u8 unused[4];
} hf_names[%d];
#seekto 0xC800; // 10ea 16 byte zone names (RT-920 fw V0.17)
struct {
char name[10];
u8 unused[6];
} zones[%d];
"""
_has_bt_denoise = True
_has_am_per_channel = True
_has_am_switch = not _has_am_per_channel
_has_single_mode = True
_has_zone_or_channel = True
_has_zone_names = True
_has_sp0 = False # spkey addresses are not shifted
_has_hf = True
def get_bank_model(self):
return RT920BankModel(self, "Zones", self._banks)
def get_features(self):
rf = super().get_features()
# Firmware V0.18 supports 10
# optional "static zones" of 99 frequencies
# or 990 flat chanels
if self._memobj is not None and hasattr(self._memobj, "settings"):
rf.has_bank = not self._memobj.settings.zone_or_channel
else:
rf.has_bank = False # default to flat mode if settings not init'ed
rf.has_bank_names = self._has_zone_names
rf.valid_tuning_steps = self._steps
rf.has_sub_devices = self._has_hf
return rf
def process_mmap(self):
AUX_CHANS_FM = AUX_CHANS_AM = AUX_CHANS_HF = 15
mem_format = MEM_FORMAT % self._mem_params + \
self._rt920_specific_fmt % (
AUX_CHANS_FM,
AUX_CHANS_AM,
AUX_CHANS_HF,
AUX_CHANS_FM,
AUX_CHANS_AM,
AUX_CHANS_HF,
self._banks,
)
self._memobj = bitwise.parse(mem_format, self._mmap)
@classmethod
def get_prompts(cls):
rp = super().get_prompts()
rp.experimental = \
('This driver is a beta version for the RT-920'
' running Firmware V0.18\n'
'\n'
'Please save an unedited copy of your first successful\n'
'download to a CHIRP Radio Images(*.img) file.\n\n'
'PROCEED AT YOUR OWN RISK!'
)
return rp
def get_sub_devices(self):
return [RT920VhfUfh(self._mmap),
RT920FM(self._mmap),
RT920AM(self._mmap),
RT920HF(self._mmap),
]
class RT920VhfUfh(RT920):
"""Radtel RT-920 VHF/UHF subdevice"""
VENDOR = "Radtel"
MODEL = "RT-920"
VARIANT = "VHF/UHF"
class RT920FM(RT920):
"""Radtel RT-920 FM broadcast subdevice"""
VENDOR = "Radtel"
MODEL = "RT-920"
VARIANT = "FM Broadcast"
_upper = 15
_mem_params = (_upper, # number of channels
)
_valid_bands = [(64000000, 108000000)] # in Mhz, 64.0-108 MHz
SPECIAL_CHANNELS = ["VFO"]
def get_features(self):
rf = chirp_common.RadioFeatures()
rf.valid_bands = self._valid_bands
rf.memory_bounds = (1, self._upper)
rf.can_delete = True
rf.can_odd_split = False
rf.has_bank = False
rf.has_bank_index = False
rf.has_bank_names = False
rf.has_comment = False
rf.has_cross = False
rf.has_ctone = False
rf.has_dtcs = False
rf.has_dtcs_polarity = False
rf.has_mode = True
rf.has_offset = False
rf.has_settings = False
rf.has_sub_devices = False
rf.has_tuning_step = False
rf.valid_characters = RT900._valid_chars
rf.valid_cross_modes = []
rf.valid_dtcs_codes = []
rf.valid_dtcs_pols = []
rf.valid_duplexes = []
rf.valid_modes = ["AM", "WFM", "Auto"]
rf.valid_name_length = 12
rf.valid_skips = []
rf.valid_special_chans = self.SPECIAL_CHANNELS
rf.valid_tuning_steps = [
1.0, 2.5, 5.0, 6.25, 8.33, 10.0, 12.5, 20.0, 25.0, 50.0,
]
rf.valid_tmodes = []
rf.valid_tones = []
return rf
def get_raw_memory(self, number):
if isinstance(number, str):
return repr(self._memobj.fm_vfo)
else:
return (repr(self._memobj.fm_freqs[number - 1]) +
repr(self._memobj.fm_names[number - 1]))
def get_memory(self, number):
mem = chirp_common.Memory()
if isinstance(number, str):
mem.number = self._upper + self.SPECIAL_CHANNELS.index(number) + 1
mem.extd_number = number
_mem = self._memobj.fm_vfo
else:
mem.number = number
_mem = self._memobj.fm_freqs[number - 1]
_name = self._memobj.fm_names[number - 1]
freq = int(_mem.freq) * 10000
if freq == 0:
mem.empty = True
return mem
mem.freq = freq
if mem.number > self._upper:
mem.immutable += ["name"]
else:
mem.name = str(_name.name).rstrip("\xff ")
mem.mode = "WFM"
mem.immutable += ["mode", "ctone", "rtone"]
return mem
def set_memory(self, mem):
if mem.number > self._upper:
_mem = self._memobj.fm_vfo
else:
_mem = self._memobj.fm_freqs[mem.number - 1]
_name = self._memobj.fm_names[mem.number - 1]
if mem.empty:
_mem.freq.fill_raw(b"\x00")
if mem.number <= self._upper:
_name.name.fill_raw(b"\xff")
return
_mem.freq = int(mem.freq / 10000)
if mem.number <= self._upper:
_name.name = mem.name.ljust(12, "\xff")
class RT920AM(RT920FM):
"""Radtel RT-920 AM broadcast subdevice"""
VENDOR = "Radtel"
MODEL = "RT-920"
VARIANT = "AM Broadcast"
_valid_bands = [(153000, 279000), # in Mhz, 153-279 KHz
(520000, 1710000), # 520-1710 KHz
(2300000, 26100000), # 2300-26100 KHz
]
def get_raw_memory(self, number):
if isinstance(number, str):
return repr(self._memobj.am_vfo)
else:
return (repr(self._memobj.am_freqs[number - 1]) +
repr(self._memobj.am_names[number - 1]))
def get_memory(self, number):
mem = chirp_common.Memory()
if isinstance(number, str):
mem.number = self._upper + self.SPECIAL_CHANNELS.index(number) + 1
mem.extd_number = number
_mem = self._memobj.am_vfo
else:
mem.number = number
_mem = self._memobj.am_freqs[number - 1]
_name = self._memobj.am_names[number - 1]
freq = int(_mem.freq) * 1000
if freq == 0:
mem.empty = True
return mem
mem.freq = freq
if mem.number > self._upper:
mem.immutable += ["name"]
else:
mem.name = str(_name.name).rstrip("\xff ")
mem.mode = "AM"
mem.immutable += ["mode", "ctone", "rtone"]
return mem
def set_memory(self, mem):
if mem.number > self._upper:
_mem = self._memobj.am_vfo
else:
_mem = self._memobj.am_freqs[mem.number - 1]
_name = self._memobj.am_names[mem.number - 1]
if mem.empty:
_mem.freq.fill_raw(b"\x00")
if mem.number <= self._upper:
_name.name.fill_raw(b"\xff")
return
_mem.freq = int(mem.freq / 1000)
if mem.number <= self._upper:
_name.name = mem.name.ljust(12, "\xff")
class RT920HF(RT920FM):
"""Radtel RT-920 HF LSB, USB, CW subdevice"""
VENDOR = "Radtel"
MODEL = "RT-920"
VARIANT = "HF"
_valid_bands = [(150000, 30000000)] # in Mhz, 150-30000 KHz
_ssb_bandwidth_list = [
"0.5K", "1.0K", "1.2K", "2.2K", "3.0K", "4.0K",
]
def get_raw_memory(self, number):
if isinstance(number, str):
return repr(self._memobj.hf_vfo)
else:
return (repr(self._memobj.hf_freqs[number - 1]) +
repr(self._memobj.hf_names[number - 1]))
def get_memory(self, number):
mem = chirp_common.Memory()
if isinstance(number, str):
mem.number = self._upper + self.SPECIAL_CHANNELS.index(number) + 1
mem.extd_number = number
_mem = self._memobj.hf_vfo
else:
mem.number = number
_mem = self._memobj.hf_freqs[number - 1]
_name = self._memobj.hf_names[number - 1]
freq = int(_mem.freq) * 1000
if freq == 0:
mem.empty = True
return mem
mem.freq = freq
if mem.number > self._upper:
mem.immutable += ["name"]
else:
mem.name = str(_name.name).rstrip("\xff ")
# use Modulation Mode from SSB settings to determine Mode
# defaut to LSB if AM or FM is selected as SSB Modulation Mode
_ssb_mod = self._memobj.ssb.modulation
mem.mode = "LSB" if self._ssb_modulation_list[_ssb_mod] \
in ["AM", "FM"] else \
self._ssb_modulation_list[_ssb_mod]
mem.immutable += ["mode", "ctone", "rtone"]
mem.extra = RadioSettingGroup("Extra", "extra")
# Bandwidth
rs = RadioSettingValueList(
self._ssb_bandwidth_list,
current_index=int(_mem.bandwidth)
)
rset = RadioSetting("bandwidth", "Bandwidth", rs)
rset.set_doc("SSB Channel Bandwidth (KHz).")
mem.extra.append(rset)
# Beat Freq
rs = RadioSettingValueInteger(-32760, 27240, int(_mem.beatfreq), 1)
rset = RadioSetting("beatfreq", "Beat Freq Offset (Hz)", rs)
rset.set_doc("SSB Beat Frequency Offset (Hz).")
mem.extra.append(rset)
return mem
def set_memory(self, mem):
if mem.number > self._upper:
_mem = self._memobj.hf_vfo
else:
_mem = self._memobj.hf_freqs[mem.number - 1]
_name = self._memobj.hf_names[mem.number - 1]
if mem.empty:
_mem.fill_raw(b"\x00")
if mem.number <= self._upper:
_name.name.fill_raw(b"\xff")
return
_mem.freq = int(mem.freq / 1000)
if mem.number <= self._upper:
_name.name = mem.name.ljust(12, "\xff")
for setting in mem.extra:
setattr(_mem, setting.get_name(), int(setting.value))
@directory.register
class RadioddityGS10B(RT900BT):
# ==========
# Radioddity GS-10B: RT-900 BT variant with 256 channels.
# Same magic (PROGRAMBT80U), fingerprints, and protocol as the
# RT-900 BT. Memory layout is identical but with fewer channels.
# Based on firmware V0.03.
# ==========
"""Radioddity GS-10B"""
VENDOR = "Radioddity"
MODEL = "GS-10B"
_upper = 256 # 256 channels
_has_single_mode = False
_mem_params = (_upper, # number of channels
)
_ranges = [
(0x0000, 0x2000), # 256 channels of 32 bytes each
# 256 * 32 = 0x2000
(0x8000, 0x8040),
(0x9000, 0x9040),
(0xA000, 0xA140),
(0xD000, 0xD040), # Radio mode hidden setting
]
@classmethod
def get_prompts(cls):
rp = super().get_prompts()
rp.experimental = \
('This driver is a beta version for the Radioddity GS-10B\n'
'\n'
'Please save an unedited copy of your first successful\n'
'download to a CHIRP Radio Images(*.img) file.\n\n'
'PROCEED AT YOUR OWN RISK!'
)
return rp
@directory.register
class BJ7800(RT920):
# ==========
# Notice to developers:
# The BJ7800 support in this driver is currently based upon V0.15P
# firmware with 15 banks/zones of 64 channels.
# Should also work with the SHU H28Y Pro
# ==========
"""Bajeton BJ7800"""
VENDOR = "Bajeton"
MODEL = "BJ7800"
_upper = 960 # fw V0.15P supports 960 channels
_mem_params = (_upper, # number of channels
)
_banks = 15
_ranges = [
(0x0000, 0x7800), # 15 zones of 64 frequencies,
# equals 960 channels of 32 bytes each
# 15 * 64 * 32 = 0X7800
(0x8000, 0x8040),
(0x9000, 0x9040),
(0xB000, 0xB400), # FM, AM, HF frequencies
(0xC000, 0xC400), # FM, AM, HF names
(0xA000, 0xA140),
(0xD000, 0xD040), # Radio mode hidden setting
]
_has_bt_denoise = True
_has_am_per_channel = True
_has_am_switch = not _has_am_per_channel
_has_single_mode = False
_has_zone_or_channel = False
_has_zone_names = False # dosen't have named zones like the RT-920
_has_hf = True
def get_bank_model(self):
return chirp_common.StaticBankModel(self, banks=self._banks)
def get_features(self):
rf = super().get_features()
# Firmware V0.15P supports 15
# "static zones" of 64 frequencies
# for 960 channels
rf.has_bank = True
rf.has_bank_names = self._has_zone_names
rf.valid_tuning_steps = self._steps
rf.has_sub_devices = self._has_hf
return rf
@classmethod
def get_prompts(cls):
rp = super().get_prompts()
rp.experimental = \
('This driver is a beta version for the BJ7800'
' running Firmware V0.15P\n'
'\n'
'Please save an unedited copy of your first successful\n'
'download to a CHIRP Radio Images(*.img) file.\n\n'
'PROCEED AT YOUR OWN RISK!'
)
return rp
def get_sub_devices(self):
return [BJ7800VhfUfh(self._mmap),
RT920FM(self._mmap),
RT920AM(self._mmap),
RT920HF(self._mmap),
]
class BJ7800VhfUfh(BJ7800):
"""Bajeton BJ7800 VHF/UHF subdevice"""
VENDOR = "Bajeton"
MODEL = "BJ7800"
VARIANT = "VHF/UHF"
chirp-20261030/chirp/drivers/radtel_t18.py 0000644 0001750 0001750 00000152730 15255434330 017152 0 ustar hibby hibby # 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 .
import struct
import logging
from chirp import chirp_common, directory, memmap
from chirp import bitwise, errors, util
from chirp.settings import RadioSetting, RadioSettingGroup, \
RadioSettingValueInteger, RadioSettingValueList, \
RadioSettingValueBoolean, RadioSettings
LOG = logging.getLogger(__name__)
MEM_FORMAT = """
#seekto 0x0010;
struct {
lbcd rxfreq[4];
lbcd txfreq[4];
lbcd rxtone[2];
lbcd txtone[2];
u8 speccode:1, // Retevis RB87 PTT ID
compander:1,
scramble:1,
skip:1,
highpower:1,
narrow:1,
unknown2:1,
bcl:1;
u8 unknown3[3];
} memory[%d];
#seekto 0x02B0;
struct {
u8 voicesw; // Voice SW +
u8 voiceselect; // Voice Select
u8 scan; // Scan +
u8 vox; // VOX +
u8 voxgain; // Vox Gain +
u8 voxnotxonrx; // Rx Disable Vox +
u8 hivoltnotx; // High Vol Inhibit TX +
u8 lovoltnotx; // Low Vol Inhibit TX +
u8 rxemergency; // RX Emergency
} settings2;
#seekto 0x03C0;
struct {
u8 codesw:1, // Retevis RB29 code switch
// Retevis H777H code switch
scanmode:1,
vox:1, // Retevis RB19 VOX
speccode:1, // Retevis H777H VOX
voiceprompt:2,
batterysaver:1,
beep:1; // Retevis RB87 vox
u8 squelchlevel;
u8 sidekey2; // Retevis RT22S setting
// Retevis RB85 sidekey 1 short
// Retevis RB19 sidekey 2 long
// Retevis RT47 sidekey 1 long
// Retevis RB87 sidekey 1 long
// Retevis H777H roger
u8 timeouttimer;
u8 voxlevel;
u8 sidekey2S;
u8 unused; // Selected channel
u8 voxdelay;
u8 sidekey1L;
u8 sidekey2L;
u8 unused2[3];
u8 unknown3:4,
unknown4:1,
unknown5:2,
power10w:1; // Retevis RT85 power 10w on/off
// Retevis RT75 stop TX with low voltage
} settings;
"""
MEM_FORMAT_RB18 = """
#seekto 0x0000;
struct {
lbcd rxfreq[4];
lbcd txfreq[4];
lbcd rxtone[2];
lbcd txtone[2];
u8 jumpcode:1,
unknown1:2,
skip:1,
highpower:1,
narrow:1,
unknown2:1,
bcl:1;
u8 unknown3[3];
} memory[%d];
#seekto 0x0630;
struct {
u8 unk630:7,
voice:1;
u8 unk631:7,
language:1;
u8 unk632:7,
scan:1;
u8 unk633:7,
vox:1;
u8 unk634:5,
vox_level:3;
u8 unk635;
u8 unk636:7,
lovoltnotx:1;
u8 unk637:7,
hivoltnotx:1;
u8 unknown2[8];
u8 unk640:5,
rogerbeep:1,
batterysaver:1,
beep:1;
u8 squelchlevel;
u8 unk642;
u8 timeouttimer;
u8 unk644:7,
tail:1;
u8 channel;
} settings;
"""
MEM_FORMAT_T20FRS = """
// #seekto 0x0000;
struct {
lbcd rxfreq[4];
lbcd txfreq[4];
lbcd rxtone[2];
lbcd txtone[2];
u8 jumpcode:1,
unknown1:2,
skip:1,
highpower:1,
narrow:1,
unknown2:1,
bcl:1;
u8 unknown3[3];
} memory[%d];
#seekto 0x02B0;
struct {
u8 voicesw; // Voice SW +
u8 voiceselect; // Voice Select
u8 scan; // Scan +
u8 vox; // VOX +
u8 voxgain; // Vox Gain +
u8 voxnotxonrx; // Rx Disable Vox +
u8 hivoltnotx; // High Vol Inhibit TX +
u8 lovoltnotx; // Low Vol Inhibit TX +
u8 rxemergency; // RX Emergency
} settings2;
#seekto 0x02C0;
struct {
u8 unk:6,
batterysaver:1,
beep:1;
u8 squelchlevel;
u8 sidekey2;
u8 timeouttimer;
} settings;
"""
CMD_ACK = b"\x06"
VOICE_LIST = ["Off", "Chinese", "English"]
VOICE_LIST2 = ["English", "Chinese"]
VOICE_LIST3 = ["Off", "English", "Chinese"]
VOICE_LIST4 = ["Chinese", "English"]
SCANMODE_LIST = ["Carrier", "Time"]
VOXLEVEL_LIST = ["Off", "1", "2", "3", "4", "5", "6", "7", "8", "9"]
VOXDELAY_LIST = ["0.5 seconds", "1.0 seconds", "1.5 seconds",
"2.0 seconds", "2.5 seconds", "3.0 seconds"]
SIDEKEY19_LIST = ["Off", "Scan", "Emergency Alarm"]
SIDEKEY2_LIST = SIDEKEY19_LIST + ["Display Battery"]
SIDEKEY29_LIST = ["Off", "Scan", "VOX", "Busy Lock", "Emergency Alarm"]
SIDEKEY85SHORT_LIST = ["Off",
"Noise Cansellation On",
"Continuous Monitor",
"High/Low Power",
"Emergency Alarm",
"Show Battery",
"Scan",
"VOX",
"Busy Channel Lock"]
SIDEKEY85LONG_LIST = ["Off",
"Noise Cansellation On",
"Continuous Monitor",
"Monitor Momentary",
"High/Low Power",
"Emergency Alarm",
"Show Battery",
"Scan",
"VOX",
"Busy Channel Lock"]
SPECCODE_LIST = ["SpeCode 1", "SpeCode 2"]
SIDEKEY75_LIST = ["Off",
"Monitor Momentary",
"Scan",
"VOX",
"Monitor",
"Announciation"]
SIDEKEY47_LIST = ["Monitor Momentary",
"Channel Lock",
"Scan",
"VOX"]
SIDEKEYV8A_LIST = ["Off",
"Monitor",
"High/Low Power",
"Alarm"]
SIDEKEY87_LIST = ["Scan", "Emergency Alarm"]
SIDEKEYH777H_LIST = ["Off",
"VOX",
"SCAN",
"Monitor"]
FRS_FREQS1 = [462562500, 462587500, 462612500, 462637500, 462662500,
462687500, 462712500]
FRS_FREQS2 = [467562500, 467587500, 467612500, 467637500, 467662500,
467687500, 467712500]
FRS_FREQS3 = [462550000, 462575000, 462600000, 462625000, 462650000,
462675000, 462700000, 462725000]
FRS_FREQS = FRS_FREQS1 + FRS_FREQS2 + FRS_FREQS3
FRS16_FREQS = [462562500, 462587500, 462612500, 462637500,
462662500, 462625000, 462725000, 462687500,
462712500, 462550000, 462575000, 462600000,
462650000, 462675000, 462700000, 462725000]
GMRS_FREQS = FRS_FREQS1 + FRS_FREQS2 + FRS_FREQS3 * 2
MURS_FREQS = [151820000, 151880000, 151940000, 154570000, 154600000]
PMR_FREQS1 = [446006250, 446018750, 446031250, 446043750, 446056250,
446068750, 446081250, 446093750]
PMR_FREQS2 = [446106250, 446118750, 446131250, 446143750, 446156250,
446168750, 446181250, 446193750]
PMR_FREQS = PMR_FREQS1 + PMR_FREQS2
def _t18_enter_programming_mode(radio):
serial = radio.pipe
_magic = b"\x02" + radio._magic
try:
serial.write(_magic)
if radio._echo:
chew = serial.read(len(_magic)) # Chew the echo
ack = serial.read(1)
except:
raise errors.RadioError("Error communicating with radio")
if not ack:
raise errors.RadioNoResponse()
elif ack != CMD_ACK:
raise errors.RadioError("Radio refused to enter programming mode")
try:
serial.write(b"\x02")
if radio._echo:
serial.read(1) # Chew the echo
ident = serial.read(8)
except:
raise errors.RadioError("Error communicating with radio")
# check if ident is OK
for fp in radio._fingerprint:
if ident.startswith(fp):
break
else:
LOG.debug("Incorrect model ID, got this:\n\n" + util.hexprint(ident))
raise errors.RadioError("Radio identification failed.")
try:
serial.write(CMD_ACK)
if radio._echo:
serial.read(1) # Chew the echo
ack = serial.read(1)
except:
raise errors.RadioError("Error communicating with radio")
if radio.MODEL == "RT647":
if ack != b"\xF0":
raise errors.RadioError("Radio refused to enter programming mode")
else:
if ack != CMD_ACK:
raise errors.RadioError("Radio refused to enter programming mode")
def _t18_exit_programming_mode(radio):
serial = radio.pipe
try:
serial.write(radio.CMD_EXIT)
if radio._echo:
chew = serial.read(1) # Chew the echo
except:
raise errors.RadioError("Radio refused to exit programming mode")
def _t18_read_block(radio, block_addr, block_size):
serial = radio.pipe
cmd = struct.pack(">cHb", b'R', block_addr, block_size)
expectedresponse = b"W" + cmd[1:]
LOG.debug("Reading block %04x..." % (block_addr))
try:
serial.write(cmd)
if radio._echo:
serial.read(4) # Chew the echo
response = serial.read(4 + block_size)
if response[:4] != expectedresponse:
raise Exception("Error reading block %04x." % (block_addr))
block_data = response[4:]
if radio.ACK_BLOCK:
serial.write(CMD_ACK)
if radio._echo:
serial.read(1) # Chew the echo
ack = serial.read(1)
except:
raise errors.RadioError("Failed to read block at %04x" % block_addr)
if radio.ACK_BLOCK:
if ack != CMD_ACK:
raise Exception("No ACK reading block %04x." % (block_addr))
return block_data
def _t18_write_block(radio, block_addr, block_size):
serial = radio.pipe
cmd = struct.pack(">cHb", b'W', block_addr, block_size)
data = radio.get_mmap()[block_addr:block_addr + block_size]
LOG.debug("Writing Data:")
LOG.debug(util.hexprint(cmd + data))
try:
serial.write(cmd + data)
if radio._echo:
serial.read(4 + len(data)) # Chew the echo
if serial.read(1) != CMD_ACK:
raise Exception("No ACK")
except:
raise errors.RadioError("Failed to send block "
"to radio at %04x" % block_addr)
def do_download(radio):
LOG.debug("download")
radio._enter_programming_mode()
data = b""
status = chirp_common.Status()
status.msg = "Cloning from radio"
status.cur = 0
status.max = radio._memsize
for addr in range(0, radio._memsize, radio.BLOCK_SIZE):
status.cur = addr + radio.BLOCK_SIZE
radio.status_fn(status)
block = _t18_read_block(radio, addr, radio.BLOCK_SIZE)
data += block
LOG.debug("Address: %04x" % addr)
LOG.debug(util.hexprint(block))
_t18_exit_programming_mode(radio)
return memmap.MemoryMapBytes(data)
def do_upload(radio):
status = chirp_common.Status()
status.msg = "Uploading to radio"
radio._enter_programming_mode()
status.cur = 0
status.max = radio._memsize
for start_addr, end_addr in radio._ranges:
for addr in range(start_addr, end_addr, radio.BLOCK_SIZE):
status.cur = addr + radio.BLOCK_SIZE
radio.status_fn(status)
_t18_write_block(radio, addr, radio.BLOCK_SIZE)
_t18_exit_programming_mode(radio)
def model_match(cls, data):
"""Match the opened/downloaded image to the correct version"""
if len(data) == cls._memsize:
rid = data[0x03D0:0x03D8]
return b"P558" in rid
else:
return False
@directory.register
class T18Radio(chirp_common.CloneModeRadio):
"""radtel T18"""
VENDOR = "Radtel"
MODEL = "T18"
BAUD_RATE = 9600
BLOCK_SIZE = 0x08
CMD_EXIT = b"b"
ACK_BLOCK = True
VALID_BANDS = [(400000000, 470000000)]
TIMEOUTTIMER_LIST = ["Off", "30 seconds", "60 seconds", "90 seconds",
"120 seconds", "150 seconds", "180 seconds",
"210 seconds", "240 seconds", "270 seconds",
"300 seconds"]
_magic = b"1ROGRAM"
_fingerprint = [b"SMP558" + b"\x00\x00"]
_upper = 16
_mem_params = (_upper # number of channels
)
_frs = _frs16 = _murs = _pmr = _gmrs = False
_echo = False
_reserved = False
_ranges = [
(0x0000, 0x03F0),
]
_memsize = 0x03F0
def get_features(self):
rf = chirp_common.RadioFeatures()
rf.has_settings = True
rf.valid_modes = ["NFM", "FM"] # 12.5 kHz, 25 kHz.
rf.valid_skips = ["", "S"]
rf.valid_tmodes = ["", "Tone", "TSQL", "DTCS", "Cross"]
rf.valid_duplexes = ["", "-", "+", "split", "off"]
if self.MODEL != "T18" and \
self.MODEL != "RB618" and \
self.MODEL != "RT647":
rf.valid_power_levels = self.POWER_LEVELS
rf.can_odd_split = True
rf.has_rx_dtcs = True
rf.has_ctone = True
rf.has_cross = True
rf.valid_cross_modes = [
"Tone->Tone",
"DTCS->",
"->DTCS",
"Tone->DTCS",
"DTCS->Tone",
"->Tone",
"DTCS->DTCS"]
rf.has_tuning_step = False
rf.has_bank = False
rf.has_name = False
rf.memory_bounds = (1, self._upper)
rf.valid_bands = self.VALID_BANDS
rf.valid_tuning_steps = chirp_common.TUNING_STEPS
return rf
def process_mmap(self):
self._memobj = bitwise.parse(MEM_FORMAT % self._mem_params, self._mmap)
def _enter_programming_mode(self):
_t18_enter_programming_mode(self)
def sync_in(self):
self._mmap = do_download(self)
self.process_mmap()
def sync_out(self):
do_upload(self)
def get_raw_memory(self, number):
return repr(self._memobj.memory[number - 1])
def _decode_tone(self, val):
val = int(val)
if val == 16665:
return '', None, None
elif val >= 12000:
return 'DTCS', val - 12000, 'R'
elif val >= 8000:
return 'DTCS', val - 8000, 'N'
else:
return 'Tone', val / 10.0, None
def _encode_tone(self, memval, mode, value, pol):
if mode == '':
memval[0].set_raw(0xFF)
memval[1].set_raw(0xFF)
elif mode == 'Tone':
memval.set_value(int(value * 10))
elif mode == 'DTCS':
flag = 0x80 if pol == 'N' else 0xC0
memval.set_value(value)
memval[1].set_bits(flag)
else:
raise Exception("Internal error: invalid mode `%s'" % mode)
def get_memory(self, number):
_mem = self._memobj.memory[number - 1]
mem = chirp_common.Memory()
mem.number = number
if self._reserved:
_rsvd = _mem.unknown3.get_raw()
mem.freq = int(_mem.rxfreq) * 10
# We'll consider any blank (i.e. 0 MHz frequency) to be empty
if mem.freq == 0:
mem.empty = True
return mem
if _mem.rxfreq.get_raw() == b"\xFF\xFF\xFF\xFF":
mem.freq = 0
mem.empty = True
return mem
if _mem.txfreq.get_raw() == b"\xFF\xFF\xFF\xFF":
mem.duplex = "off"
mem.offset = 0
elif int(_mem.rxfreq) == int(_mem.txfreq):
mem.duplex = ""
mem.offset = 0
else:
mem.duplex = int(_mem.rxfreq) > int(_mem.txfreq) and "-" or "+"
mem.offset = abs(int(_mem.rxfreq) - int(_mem.txfreq)) * 10
mem.mode = not _mem.narrow and "FM" or "NFM"
mem.skip = _mem.skip and "S" or ""
txtone = self._decode_tone(_mem.txtone)
rxtone = self._decode_tone(_mem.rxtone)
chirp_common.split_tone_decode(mem, txtone, rxtone)
if self.MODEL != "T18" and self.MODEL != "RB618":
mem.power = self.POWER_LEVELS[1 - _mem.highpower]
mem.extra = RadioSettingGroup("Extra", "extra")
rs = RadioSetting("bcl", "Busy Channel Lockout",
RadioSettingValueBoolean(not _mem.bcl))
rs.set_doc(
"Prevents transmitting on a channel that is already in use. "
"Pressing PTT on a busy channel sounds an alert tone instead of "
"keying the transmitter.")
mem.extra.append(rs)
if self.MODEL != "RB18" and self.MODEL != "RB618" and \
self.MODEL != "FRS-B1" and self.MODEL != "BF-V8A" and \
self.MODEL != "RB29" and self.MODEL != "RB629" and \
self.MODEL != "BF-T20FRS":
if self.MODEL not in ["RB87",
"RT47V"]:
rs = RadioSetting("scramble", "Scramble",
RadioSettingValueBoolean(not _mem.scramble))
mem.extra.append(rs)
rs = RadioSetting("compander", "Compander",
RadioSettingValueBoolean(not _mem.compander))
mem.extra.append(rs)
if self.MODEL != "RT47V" and self.MODEL != "T8" and \
self.MODEL != "RB17" and self.MODEL != "RB617" and \
self.MODEL != "RB75" and self.MODEL != "RB19P":
if self.MODEL == "RB87":
rs = RadioSettingValueBoolean(not _mem.speccode)
rset = RadioSetting("speccode", "PTT-ID", rs)
mem.extra.append(rset)
else:
rs = RadioSettingValueBoolean(not _mem.speccode)
rset = RadioSetting("speccode", "Spec Code", rs)
mem.extra.append(rset)
immutable = []
if self._frs:
if mem.freq in FRS_FREQS:
if mem.number >= 1 and mem.number <= 22:
FRS_FREQ = FRS_FREQS[mem.number - 1]
mem.freq = FRS_FREQ
mem.duplex = ''
mem.offset = 0
mem.mode = "NFM"
if mem.number >= 8 and mem.number <= 14:
mem.power = self.POWER_LEVELS[1]
immutable = ["empty", "freq", "duplex", "offset", "mode",
"power"]
else:
immutable = ["empty", "freq", "duplex", "offset", "mode"]
elif self._frs16:
if mem.freq in FRS16_FREQS:
if mem.number >= 1 and mem.number <= 16:
FRS_FREQ = FRS16_FREQS[mem.number - 1]
mem.freq = FRS_FREQ
mem.duplex = ''
mem.offset = 0
mem.mode = "NFM"
immutable = ["empty", "freq", "duplex", "offset", "mode"]
elif self._murs:
if mem.freq in MURS_FREQS:
if mem.number >= 1 and mem.number <= 5:
MURS_FREQ = MURS_FREQS[mem.number - 1]
mem.freq = MURS_FREQ
mem.duplex = ''
mem.offset = 0
if mem.number <= 3:
mem.mode = "NFM"
immutable = ["empty", "freq", "duplex", "offset", "mode"]
else:
immutable = ["empty", "freq", "duplex", "offset"]
elif self._pmr:
if mem.freq in PMR_FREQS:
if mem.number >= 1 and mem.number <= 16:
PMR_FREQ = PMR_FREQS[mem.number - 1]
mem.freq = PMR_FREQ
mem.duplex = ''
mem.offset = 0
mem.mode = "NFM"
mem.power = self.POWER_LEVELS[1]
immutable = ["empty", "freq", "duplex", "offset", "mode",
"power"]
elif self._gmrs:
if mem.freq in GMRS_FREQS:
if mem.number >= 1 and mem.number <= 30:
GMRS_FREQ = GMRS_FREQS[mem.number - 1]
mem.freq = GMRS_FREQ
immutable = ["freq"]
if mem.number >= 1 and mem.number <= 7:
mem.duplex = ''
mem.offset = 0
immutable += ["duplex", "offset"]
elif mem.number >= 8 and mem.number <= 14:
mem.duplex = ''
mem.offset = 0
mem.mode = "NFM"
mem.power = self.POWER_LEVELS[1]
immutable += ["duplex", "offset", "mode", "power"]
elif mem.number >= 15 and mem.number <= 22:
mem.duplex = ''
mem.offset = 0
immutable += ["duplex", "offset"]
elif mem.number >= 23 and mem.number <= 30:
mem.duplex = '+'
mem.offset = 5000000
immutable += ["duplex", "offset"]
mem.immutable = immutable
return mem
def set_memory(self, mem):
# Get a low-level memory object mapped to the image
_mem = self._memobj.memory[mem.number - 1]
if self._reserved:
_rsvd = _mem.unknown3.get_raw()
if mem.empty:
if self._reserved:
_mem.set_raw(b"\xFF" * 13 + _rsvd)
else:
_mem.set_raw(b"\xFF" * (_mem.size() // 8))
return
if self._reserved:
_mem.set_raw(b"\x00" * 13 + _rsvd)
elif self.MODEL == "BF-V8A" or self.MODEL == "BF-T20FRS":
_mem.set_raw(b"\x00" * 12 + b"\x09\xFF\xFF\xFF")
elif self.MODEL == "RB29" or self.MODEL == "RB629":
_mem.set_raw(b"\x00" * 12 + b"\xF7\xFF\xFF\xFF")
else:
_mem.set_raw(b"\x00" * 12 + b"\xF9\xFF\xFF\xFF")
_mem.rxfreq = mem.freq / 10
if mem.duplex == "off":
for i in range(0, 4):
_mem.txfreq[i].set_raw(b"\xFF")
elif mem.duplex == "split":
_mem.txfreq = mem.offset / 10
elif mem.duplex == "+":
_mem.txfreq = (mem.freq + mem.offset) / 10
elif mem.duplex == "-":
_mem.txfreq = (mem.freq - mem.offset) / 10
else:
_mem.txfreq = mem.freq / 10
txtone, rxtone = chirp_common.split_tone_encode(mem)
self._encode_tone(_mem.txtone, *txtone)
self._encode_tone(_mem.rxtone, *rxtone)
if self.MODEL != "T18" and self.MODEL != "RB18":
_mem.highpower = mem.power == self.POWER_LEVELS[0]
_mem.narrow = 'N' in mem.mode
_mem.skip = mem.skip == "S"
_mem.unknown2 = 1
for setting in mem.extra:
# NOTE: Only three settings right now, all are inverted
setattr(_mem, setting.get_name(), not int(setting.value))
def get_settings(self):
_settings = self._memobj.settings
if self.MODEL == "FRS-B1" or self.MODEL == "BF-V8A" or \
self.MODEL == "BF-T20FRS":
_settings2 = self._memobj.settings2
basic = RadioSettingGroup("basic", "Basic Settings")
top = RadioSettings(basic)
rs = RadioSetting("squelchlevel", "Squelch level",
RadioSettingValueInteger(
0, 9, _settings.squelchlevel))
rs.set_doc(
"Sets how strong a received signal must be before the speaker "
"unmutes. 0 leaves the squelch open so you hear noise all the "
"time, higher values reject more weak signals and noise. Raise "
"this value if the speaker keeps unmuting on interference, lower "
"it if distant stations are being cut off.")
basic.append(rs)
rs = RadioSetting("timeouttimer", "Timeout timer",
RadioSettingValueList(
self.TIMEOUTTIMER_LIST,
current_index=_settings.timeouttimer))
rs.set_doc(
"Limits how long a single transmission may last. When the time "
"runs out the radio stops transmitting until PTT is released, "
"which prevents overheating and stops a stuck PTT from blocking "
"the channel for everyone else.")
basic.append(rs)
if self.MODEL == "RB18" or self.MODEL == "RB618":
rs = RadioSetting("scan", "Scan",
RadioSettingValueBoolean(_settings.scan))
basic.append(rs)
elif self.MODEL == "FRS-B1" or self.MODEL == "BF-V8A" or \
self.MODEL == "BF-T20FRS":
rs = RadioSetting("settings2.scan", "Scan",
RadioSettingValueBoolean(_settings2.scan))
rs.set_doc(
"Enables channel scanning. The radio steps through the "
"programmed channels, stops on the first one it finds busy "
"and resumes a few seconds after that channel goes quiet. "
"Channels marked to skip are left out, and at least two "
"channels have to remain in the scan list for scanning to "
"start.")
basic.append(rs)
else:
rs = RadioSetting("scanmode", "Scan mode",
RadioSettingValueList(
SCANMODE_LIST,
current_index=_settings.scanmode))
basic.append(rs)
if self.MODEL == "RT20":
rs = RadioSetting("voiceprompt", "Voice prompts",
RadioSettingValueList(
VOICE_LIST4,
current_index=_settings.voiceprompt))
basic.append(rs)
elif self.MODEL == "RT22S":
rs = RadioSetting("voiceprompt", "Voice prompts",
RadioSettingValueBoolean(_settings.voiceprompt))
basic.append(rs)
elif self.MODEL == "RB18" or self.MODEL == "RB618":
rs = RadioSetting("voice", "Voice prompts",
RadioSettingValueBoolean(_settings.voice))
basic.append(rs)
elif self.MODEL == "FRS-B1" or self.MODEL == "BF-V8A" or \
self.MODEL == "BF-T20FRS":
rs = RadioSetting("settings2.voicesw", "Voice prompts",
RadioSettingValueBoolean(_settings2.voicesw))
rs.set_doc(
"Announces the channel number and other operations out loud "
"as you use the radio. It can be intrusive in quiet "
"surroundings.")
basic.append(rs)
elif self.MODEL == "RT15":
rs = RadioSetting("voiceprompt", "Voice prompts",
RadioSettingValueList(
VOICE_LIST3,
current_index=_settings.voiceprompt))
basic.append(rs)
else:
if self.MODEL != "RB87":
rs = RadioSetting("voiceprompt", "Voice prompts",
RadioSettingValueList(
VOICE_LIST,
current_index=_settings.voiceprompt))
basic.append(rs)
rs = RadioSetting("batterysaver", "Battery saver",
RadioSettingValueBoolean(_settings.batterysaver))
rs.set_doc(
"Cuts standby power consumption by switching the receiver off and "
"on in a repeating cycle once no signal has been received for a "
"few seconds. It considerably extends battery life, at the cost "
"of occasionally clipping the first moment of an incoming "
"transmission.")
basic.append(rs)
if self.MODEL not in ["RB29",
"RB75",
"RB87",
"RB629",
"RT15"
]:
rs = RadioSetting("beep", "Beep",
RadioSettingValueBoolean(_settings.beep))
rs.set_doc(
"Sounds a short confirmation tone as you operate the radio. "
"Turn it off when the radio has to be used discreetly.")
basic.append(rs)
if self.MODEL == "RB19" or self.MODEL == "RB19P" \
or self.MODEL == "RB619":
rs = RadioSetting("vox", "VOX",
RadioSettingValueBoolean(_settings.vox))
basic.append(rs)
if self.MODEL == "RB87":
rs = RadioSetting("beep", "VOX",
RadioSettingValueBoolean(_settings.beep))
basic.append(rs)
if self.MODEL != "RB18" and self.MODEL != "RB618" \
and self.MODEL != "FRS-B1" \
and self.MODEL != "BF-V8A" \
and self.MODEL != "BF-T20FRS":
rs = RadioSetting("voxlevel", "Vox level",
RadioSettingValueList(
VOXLEVEL_LIST,
current_index=_settings.voxlevel))
basic.append(rs)
rs = RadioSetting("voxdelay", "VOX delay",
RadioSettingValueList(
VOXDELAY_LIST,
current_index=_settings.voxdelay))
basic.append(rs)
if self.MODEL == "RT22S":
rs = RadioSetting("sidekey2", "Side Key 2(Long)",
RadioSettingValueList(
SIDEKEY2_LIST,
current_index=_settings.sidekey2))
basic.append(rs)
if self.MODEL == "RB18" or self.MODEL == "RB618":
rs = RadioSetting("language", "Language",
RadioSettingValueList(
VOICE_LIST2,
current_index=_settings.language))
basic.append(rs)
rs = RadioSetting("tail", "Tail",
RadioSettingValueBoolean(_settings.tail))
basic.append(rs)
rs = RadioSetting("hivoltnotx", "High voltage no TX",
RadioSettingValueBoolean(_settings.hivoltnotx))
basic.append(rs)
rs = RadioSetting("lovoltnotx", "Low voltage no TX",
RadioSettingValueBoolean(_settings.lovoltnotx))
basic.append(rs)
rs = RadioSetting("vox", "VOX",
RadioSettingValueBoolean(_settings.vox))
basic.append(rs)
if _settings.vox_level > 4:
val = 1
else:
val = _settings.vox_level + 1
rs = RadioSetting("vox_level", "VOX level",
RadioSettingValueInteger(1, 5, val))
basic.append(rs)
rs = RadioSetting("rogerbeep", "Roger beep",
RadioSettingValueBoolean(_settings.rogerbeep))
basic.append(rs)
if self.MODEL == "RB85":
rs = RadioSetting("speccode", "SpecCode Select",
RadioSettingValueList(
SPECCODE_LIST,
current_index=_settings.speccode))
basic.append(rs)
rs = RadioSetting("sidekey2", "Side Key 1(Short)",
RadioSettingValueList(
SIDEKEY85SHORT_LIST,
current_index=_settings.sidekey2))
basic.append(rs)
rs = RadioSetting("sidekey1L", "Side Key 1(Long)",
RadioSettingValueList(
SIDEKEY85LONG_LIST,
current_index=_settings.sidekey1L))
basic.append(rs)
rs = RadioSetting("sidekey2S", "Side Key 2(Short)",
RadioSettingValueList(
SIDEKEY85SHORT_LIST,
current_index=_settings.sidekey2S))
basic.append(rs)
rs = RadioSetting("sidekey2L", "Side Key 2(Long)",
RadioSettingValueList(
SIDEKEY85LONG_LIST,
current_index=_settings.sidekey2L))
basic.append(rs)
rs = RadioSetting("power10w", "Power 10W",
RadioSettingValueBoolean(_settings.power10w))
basic.append(rs)
if self.MODEL == "RB75":
rs = RadioSetting("sidekey2", "Side Key 1(Short)",
RadioSettingValueList(
SIDEKEY75_LIST,
current_index=_settings.sidekey2))
basic.append(rs)
rs = RadioSetting("sidekey1L", "Side Key 1(Long)",
RadioSettingValueList(
SIDEKEY75_LIST,
current_index=_settings.sidekey1L))
basic.append(rs)
rs = RadioSetting("sidekey2S", "Side Key 2(Short)",
RadioSettingValueList(
SIDEKEY75_LIST,
current_index=_settings.sidekey2S))
basic.append(rs)
rs = RadioSetting("sidekey2L", "Side Key 2(Long)",
RadioSettingValueList(
SIDEKEY75_LIST,
current_index=_settings.sidekey2L))
basic.append(rs)
rs = RadioSetting("power10w", "Low Voltage Stop TX",
RadioSettingValueBoolean(_settings.power10w))
basic.append(rs)
if self.MODEL == "RB87":
rs = RadioSetting("sidekey2", "Side Key 1(Long)",
RadioSettingValueList(
SIDEKEY87_LIST,
current_index=_settings.sidekey2))
basic.append(rs)
if self.MODEL == "FRS-B1":
rs = RadioSetting("settings2.hivoltnotx",
"High Voltage Inhibit TX",
RadioSettingValueBoolean(_settings2.hivoltnotx))
basic.append(rs)
rs = RadioSetting("settings2.lovoltnotx", "Low Voltage Inhibit TX",
RadioSettingValueBoolean(_settings2.lovoltnotx))
basic.append(rs)
rs = RadioSetting("settings2.vox", "Vox",
RadioSettingValueBoolean(_settings2.vox))
basic.append(rs)
rs = RadioSetting("settings2.voxnotxonrx", "Rx Disable VOX",
RadioSettingValueBoolean(_settings2.voxnotxonrx))
basic.append(rs)
rs = RadioSetting("settings2.voxgain", "Vox Gain",
RadioSettingValueInteger(
1, 5, _settings2.voxgain))
basic.append(rs)
if self.MODEL == "RB19" or self.MODEL == "RB19P" \
or self.MODEL == "RB619":
rs = RadioSetting("sidekey2", "Left Navigation Button(Long)",
RadioSettingValueList(
SIDEKEY19_LIST,
current_index=_settings.sidekey2))
basic.append(rs)
if self.MODEL == "RT47" or self.MODEL == "RT47V" or \
self.MODEL == "RT647":
rs = RadioSetting("sidekey2", "Side Key 1(Long)",
RadioSettingValueList(
SIDEKEY47_LIST,
current_index=_settings.sidekey2))
basic.append(rs)
rs = RadioSetting("sidekey2S", "Side Key 2(Long)",
RadioSettingValueList(
SIDEKEY47_LIST,
current_index=_settings.sidekey2S))
basic.append(rs)
if self.MODEL == "BF-V8A" or self.MODEL == "BF-T20FRS":
rs = RadioSetting("sidekey2", "Side key",
RadioSettingValueList(
SIDEKEYV8A_LIST,
current_index=_settings.sidekey2))
rs.set_doc(
"Chooses what the programmable key does: Monitor opens the "
"squelch so that signals too weak to break it can still be "
"heard, High/Low Power switches the transmit power, and "
"Alarm triggers the emergency alarm.")
basic.append(rs)
rs = RadioSetting("settings2.rxemergency", "RX emergency",
RadioSettingValueBoolean(_settings2.rxemergency))
rs.set_doc(
"Lets this radio respond to an emergency alarm sent by "
"another radio on the same channel instead of ignoring it.")
basic.append(rs)
rs = RadioSetting("settings2.voiceselect", "Language",
RadioSettingValueList(
VOICE_LIST2,
current_index=_settings2.voiceselect))
rs.set_doc(
"Selects the language the spoken voice prompts use. It has "
"no effect unless Voice prompts are enabled.")
basic.append(rs)
rs = RadioSetting("settings2.hivoltnotx",
"High voltage inhibit TX",
RadioSettingValueBoolean(_settings2.hivoltnotx))
rs.set_doc(
"Blocks transmitting while the battery voltage is above the "
"safe range, which protects the radio when an out-of-spec "
"battery or charger is fitted.")
basic.append(rs)
rs = RadioSetting("settings2.lovoltnotx", "Low voltage inhibit TX",
RadioSettingValueBoolean(_settings2.lovoltnotx))
rs.set_doc(
"Blocks transmitting once the battery is nearly flat. "
"Transmitting draws far more current than receiving, so "
"this keeps the remaining charge available for listening.")
basic.append(rs)
rs = RadioSetting("settings2.vox", "VOX",
RadioSettingValueBoolean(_settings2.vox))
rs.set_doc(
"Voice operated transmit. The radio starts transmitting "
"when you speak towards the microphone, so hands-free "
"conversation is possible without pressing PTT.")
basic.append(rs)
rs = RadioSetting("settings2.voxnotxonrx", "RX disable VOX",
RadioSettingValueBoolean(_settings2.voxnotxonrx))
rs.set_doc(
"Suspends VOX while a signal is being received, so that "
"your own voice does not key the transmitter on top of the "
"station you are listening to.")
basic.append(rs)
rs = RadioSetting("settings2.voxgain", "Vox Gain",
RadioSettingValueInteger(
1, 5, _settings2.voxgain))
rs.set_doc(
"Sets the VOX sensitivity. Too sensitive a setting keys the "
"transmitter on the noise around the radio, too insensitive "
"a setting fails to pick up your voice.")
basic.append(rs)
if self.MODEL == "RB29" or self.MODEL == "RB629":
rs = RadioSetting("codesw", "Code Switch",
RadioSettingValueBoolean(_settings.codesw))
basic.append(rs)
rs = RadioSetting("sidekey2S", "Side Key(Short)",
RadioSettingValueList(
SIDEKEY29_LIST,
current_index=_settings.sidekey2S))
basic.append(rs)
rs = RadioSetting("sidekey2L", "Side Key(Long)",
RadioSettingValueList(
SIDEKEY29_LIST,
current_index=_settings.sidekey2L))
basic.append(rs)
if self.MODEL == "H777H_FRS" or self.MODEL == "H777H_PMR":
rs = RadioSetting("codesw", "Code Switch",
RadioSettingValueBoolean(_settings.codesw))
basic.append(rs)
rs = RadioSetting("speccode", "VOX",
RadioSettingValueBoolean(_settings.speccode))
basic.append(rs)
rs = RadioSetting("sidekey2L", "Side Key 2(Long)",
RadioSettingValueList(
SIDEKEYH777H_LIST,
current_index=_settings.sidekey2L))
basic.append(rs)
rs = RadioSetting("sidekey2", "Roger",
RadioSettingValueBoolean(_settings.sidekey2))
basic.append(rs)
return top
def set_settings(self, settings):
for element in settings:
if not isinstance(element, RadioSetting):
self.set_settings(element)
continue
else:
try:
if "." in element.get_name():
bits = element.get_name().split(".")
obj = self._memobj
for bit in bits[:-1]:
obj = getattr(obj, bit)
setting = bits[-1]
else:
obj = self._memobj.settings
setting = element.get_name()
if element.has_apply_callback():
LOG.debug("Using apply callback")
element.run_apply_callback()
elif setting == "vox_level":
setattr(obj, setting, int(element.value) - 1)
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):
if cls.MODEL == "T18":
match_size = False
match_model = False
# testing the file data size
if len(filedata) == cls._memsize:
match_size = True
# testing the model fingerprint
match_model = model_match(cls, filedata)
if match_size and match_model:
return True
else:
return False
else:
# Radios that have always been post-metadata, so never do
# old-school detection
return False
@directory.register
class RT20Radio(T18Radio):
"""RETEVIS RT20"""
VENDOR = "Retevis"
MODEL = "RT20"
ACK_BLOCK = True
BLOCK_SIZE = 0x08
POWER_LEVELS = [chirp_common.PowerLevel("High", watts=2.00),
chirp_common.PowerLevel("Low", watts=0.50)]
_magic = b"8AOGRAM"
_fingerprint = [b"SMP558" + b"\x02"]
_upper = 16
_mem_params = (_upper # number of channels
)
@directory.register
class RT22SRadio(T18Radio):
"""RETEVIS RT22S"""
VENDOR = "Retevis"
MODEL = "RT22S"
ACK_BLOCK = False
POWER_LEVELS = [chirp_common.PowerLevel("High", watts=2.00),
chirp_common.PowerLevel("Low", watts=0.50)]
_magic = b"9COGRAM"
_fingerprint = [b"SMP558" + b"\x02"]
_upper = 22
_mem_params = (_upper # number of channels
)
_frs = True
_pmr = False
@directory.register
class RB18Radio(T18Radio):
"""RETEVIS RB18"""
VENDOR = "Retevis"
MODEL = "RB18"
BLOCK_SIZE = 0x10
CMD_EXIT = b"E"
POWER_LEVELS = [chirp_common.PowerLevel("High", watts=2.00),
chirp_common.PowerLevel("Low", watts=0.50)]
_magic = b"PROGRAL"
_fingerprint = [b"P3107" + b"\xF7"]
_upper = 22
_mem_params = (_upper # number of channels
)
_frs = True
_pmr = False
_ranges = [
(0x0000, 0x0660),
]
_memsize = 0x0660
def process_mmap(self):
self._memobj = bitwise.parse(MEM_FORMAT_RB18 %
self._mem_params, self._mmap)
@classmethod
def match_model(cls, filedata, filename):
# This radio has always been post-metadata, so never do
# old-school detection
return False
@directory.register
class RB618Radio(RB18Radio):
"""RETEVIS RB618"""
VENDOR = "Retevis"
MODEL = "RB618"
_upper = 16
_mem_params = (_upper # number of channels
)
_frs = False
_pmr = True
@directory.register
class RT68Radio(T18Radio):
"""RETEVIS RT68"""
VENDOR = "Retevis"
MODEL = "RT68"
ACK_BLOCK = False
CMD_EXIT = b""
POWER_LEVELS = [chirp_common.PowerLevel("High", watts=2.00),
chirp_common.PowerLevel("Low", watts=0.50)]
_magic = b"83OGRAM"
_fingerprint = [b"\x06\x00\x00\x00\x00\x00\x00\x00"]
_upper = 16
_mem_params = (_upper # number of channels
)
_frs16 = True
_pmr = False
@classmethod
def match_model(cls, filedata, filename):
# This radio has always been post-metadata, so never do
# old-school detection
return False
@directory.register
class RT668Radio(RT68Radio):
"""RETEVIS RT668"""
VENDOR = "Retevis"
MODEL = "RT668"
_frs16 = False
_pmr = True
@directory.register
class RB17Radio(RT68Radio):
"""RETEVIS RB17"""
VENDOR = "Retevis"
MODEL = "RB17"
_magic = b"A5OGRAM"
_fingerprint = [b"\x53\x00\x00\x00\x00\x00\x00\x00"]
_frs16 = True
_pmr = False
_murs = False
@directory.register
class RB617Radio(RB17Radio):
"""RETEVIS RB617"""
VENDOR = "Retevis"
MODEL = "RB617"
_frs16 = False
_pmr = True
_murs = False
@directory.register
class RB17VRadio(RB17Radio):
"""RETEVIS RB17V"""
VENDOR = "Retevis"
MODEL = "RB17V"
VALID_BANDS = [(136000000, 174000000)]
_upper = 5
_frs16 = False
_pmr = False
_murs = True
@directory.register
class RB85Radio(T18Radio):
"""Retevis RB85"""
VENDOR = "Retevis"
MODEL = "RB85"
ACK_BLOCK = False
POWER_LEVELS = [chirp_common.PowerLevel("High", watts=10.00),
chirp_common.PowerLevel("Low", watts=5.00)]
_magic = b"H19GRAM"
_fingerprint = [b"SMP558" + b"\x02"]
@directory.register
class RB75Radio(T18Radio):
"""Retevis RB75"""
VENDOR = "Retevis"
MODEL = "RB75"
ACK_BLOCK = False
POWER_LEVELS = [chirp_common.PowerLevel("High", watts=5.00),
chirp_common.PowerLevel("Low", watts=0.50)]
_magic = b"KVOGRAM"
_fingerprint = [b"SMP558" + b"\x00"]
_upper = 30
_mem_params = (_upper # number of channels
)
_gmrs = False # sold as GMRS radio but supports full band TX/RX
@directory.register
class FRSB1Radio(T18Radio):
"""BTECH FRS-B1"""
VENDOR = "BTECH"
MODEL = "FRS-B1"
ACK_BLOCK = True
POWER_LEVELS = [chirp_common.PowerLevel("High", watts=2.00),
chirp_common.PowerLevel("Low", watts=0.50)]
_magic = b"PROGRAM"
_fingerprint = [b"P3107" + b"\xF7\x00"]
_upper = 22
_mem_params = (_upper # number of channels
)
_frs = True
@directory.register
class RB19Radio(T18Radio):
"""Retevis RB19"""
VENDOR = "Retevis"
MODEL = "RB19"
ACK_BLOCK = False
POWER_LEVELS = [chirp_common.PowerLevel("High", watts=2.00),
chirp_common.PowerLevel("Low", watts=0.50)]
_magic = b"9COGRAM"
_fingerprint = [b"SMP558" + b"\x02"]
_upper = 22
_mem_params = (_upper # number of channels
)
_frs = True
@directory.register
class RB19PRadio(T18Radio):
"""Retevis RB19P"""
VENDOR = "Retevis"
MODEL = "RB19P"
ACK_BLOCK = False
POWER_LEVELS = [chirp_common.PowerLevel("High", watts=3.00),
chirp_common.PowerLevel("Low", watts=0.50)]
_magic = b"70OGRAM"
_fingerprint = [b"SMP558" + b"\x02"]
_upper = 30
_mem_params = (_upper # number of channels
)
_gmrs = True
@directory.register
class RB619Radio(T18Radio):
"""Retevis RB619"""
VENDOR = "Retevis"
MODEL = "RB619"
ACK_BLOCK = False
POWER_LEVELS = [chirp_common.PowerLevel("High", watts=0.500),
chirp_common.PowerLevel("Low", watts=0.499)]
_magic = b"9COGRAM"
_fingerprint = [b"SMP558" + b"\x02"]
_upper = 16
_mem_params = (_upper # number of channels
)
_pmr = True
@directory.register
class RT47Radio(T18Radio):
"""Retevis RT47"""
VENDOR = "Retevis"
MODEL = "RT47"
ACK_BLOCK = False
POWER_LEVELS = [chirp_common.PowerLevel("High", watts=2.000),
chirp_common.PowerLevel("Low", watts=0.500)]
_magic = b"47OGRAM"
_fingerprint = [b"\x06\x00\x00\x00\x00\x00\x00\x00"]
_upper = 16
_mem_params = (_upper # number of channels
)
_frs16 = True
_echo = True
@directory.register
class RT47VRadio(RT47Radio):
"""Retevis RT47V"""
VENDOR = "Retevis"
MODEL = "RT47V"
VALID_BANDS = [(136000000, 174000000)]
_upper = 5
_mem_params = (_upper # number of channels
)
_frs16 = False
_murs = True
@directory.register
class RT647Radio(RT47Radio):
"""Retevis RT647"""
VENDOR = "Retevis"
MODEL = "RT647"
_frs16 = False
_pmr = True
@directory.register
class BFV8ARadio(T18Radio):
"""Baofeng BF-V8A"""
VENDOR = "Baofeng"
MODEL = "BF-V8A"
ACK_BLOCK = True
POWER_LEVELS = [chirp_common.PowerLevel("High", watts=2.000),
chirp_common.PowerLevel("Low", watts=0.500)]
_magic = b"PROGRAM"
_fingerprint = [b"P3107" + b"\xF7\x00\x00"]
_upper = 16
_mem_params = (_upper # number of channels
)
_echo = False
@directory.register
class RB29Radio(T18Radio):
"""Retevis RB29"""
VENDOR = "Retevis"
MODEL = "RB29"
ACK_BLOCK = False
POWER_LEVELS = [chirp_common.PowerLevel("High", watts=2.00),
chirp_common.PowerLevel("Low", watts=0.50)]
_magic = b"S19GRAM"
_fingerprint = [b"SMP558" + b"\x02"]
_upper = 16
_mem_params = (_upper # number of channels
)
_frs16 = True
@directory.register
class RB629Radio(RB29Radio):
"""Retevis RB29"""
VENDOR = "Retevis"
MODEL = "RB629"
POWER_LEVELS = [chirp_common.PowerLevel("High", watts=0.500),
chirp_common.PowerLevel("Low", watts=0.499)]
_frs16 = False
_pmr = True
@directory.register
class RT15Radio(T18Radio):
"""RETEVIS RT15"""
VENDOR = "Retevis"
MODEL = "RT15"
ACK_BLOCK = False
CMD_EXIT = b"b"
POWER_LEVELS = [chirp_common.PowerLevel("High", watts=2.00),
chirp_common.PowerLevel("Low", watts=0.50)]
_magic = b"KAOGRAM"
_fingerprint = [b"\x06\x00\x00\x00\x00\x00\x00\x00",
b"\x06\x03\xE8\x08\xFF\xFF\xFF\xFF",
b"\x06MP558\xFF\xFF"]
_upper = 16
_mem_params = (_upper # number of channels
)
_frs16 = False # sold as FRS radio but supports full band TX/RX
@classmethod
def match_model(cls, filedata, filename):
# This radio has always been post-metadata, so never do
# old-school detection
return False
@directory.register
class RB87Radio(T18Radio):
"""RETEVIS RB87"""
VENDOR = "Retevis"
MODEL = "RB87"
ACK_BLOCK = False
CMD_EXIT = b""
ACK_BLOCK = False
POWER_LEVELS = [chirp_common.PowerLevel("High", watts=5.00),
chirp_common.PowerLevel("Low", watts=0.50)]
_magic = b"C8OGRAN"
_fingerprint = [b"SMP558"]
_upper = 30
_mem_params = (_upper # number of channels
)
_gmrs = True
@directory.register
class BFT20FRSRadio(T18Radio):
"""Baofeng BF-T20FRS"""
VENDOR = "Baofeng"
MODEL = "BF-T20FRS"
ACK_BLOCK = True
POWER_LEVELS = [chirp_common.PowerLevel("High", watts=2.000),
chirp_common.PowerLevel("Low", watts=0.500)]
_magic = b"PROGRAM"
_fingerprint = [b"P3107" + b"\xF7\x00\x00"]
_upper = 22
_mem_params = (_upper # number of channels
)
_frs = False # sold as FRS radio but supports full band TX/RX
_ranges = [
(0x0000, 0x0160),
(0x02B0, 0x02D0),
]
_memsize = 0x03F0
def process_mmap(self):
self._memobj = bitwise.parse(MEM_FORMAT_T20FRS %
self._mem_params, self._mmap)
@directory.register
class H777HFRSRadio(T18Radio):
"""Retevis H777H FRS"""
VENDOR = "Retevis"
MODEL = "H777H_FRS" # SKU: A9104J
ACK_BLOCK = False
POWER_LEVELS = [chirp_common.PowerLevel("High", watts=2.000),
chirp_common.PowerLevel("Low", watts=0.500)]
TIMEOUTTIMER_LIST = ["Off", "30 seconds", "60 seconds", "90 seconds",
"120 seconds", "150 seconds", "180 seconds"]
_magic = b"C701RAD"
_fingerprint = [b"SMP558" + b"\x02"]
_upper = 16
_mem_params = (_upper # number of channels
)
_reserved = True
_frs16 = True
_pmr = False
@directory.register
class H777HPMRRadio(H777HFRSRadio):
"""Retevis H777H PMR"""
VENDOR = "Retevis"
MODEL = "H777H_PMR" # SKU: A9104K
POWER_LEVELS = [chirp_common.PowerLevel("High", watts=0.500),
chirp_common.PowerLevel("Low", watts=0.500)]
_frs16 = False
_pmr = True
chirp-20261030/chirp/drivers/retevis_c2.py 0000644 0001750 0001750 00000135422 15172631605 017251 0 ustar hibby hibby # Copyright 2026 Tony Gies
#
# 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 .
"""Retevis C2 GMRS radio driver.
The C2 uses a DLE-stuffed STX/ETX serial protocol at 115200 baud.
"""
import logging
import time
from chirp import bandplan_na, bitwise, chirp_common, directory
from chirp import errors, memmap
from chirp.settings import (
MemSetting,
RadioSetting,
RadioSettingGroup,
RadioSettings,
RadioSettingValueBoolean,
RadioSettingValueInteger,
RadioSettingValueList,
RadioSettingValueMap,
RadioSettingValueString,
)
LOG = logging.getLogger(__name__)
MEM_FORMAT = """
#seekto 0x0000;
u8 ident_data[28]; // 0x0000 firmware/hardware ident (read-only)
ul32 local_id; // 0x001C
ul32 settings_0020; // 0x0020 bit0=voice prompt
ul32 unknown_0024; // 0x0024
ul32 power_save; // 0x0028
ul32 unknown_002c; // 0x002C
ul32 unknown_0030; // 0x0030
ul32 unknown_0034; // 0x0034
ul32 vox_packed; // 0x0038 vox_level*4 + (1=on, 2=off)
ul32 unknown_003c; // 0x003C
ul32 sk_short; // 0x0040 key assignment
ul32 sk_long; // 0x0044 key assignment (0xFFFFFFFF=None)
ul32 pf1_short; // 0x0048 key assignment
ul32 pf1_long; // 0x004C key assignment
ul32 pf2_short; // 0x0050 key assignment
ul32 pf2_long; // 0x0054 key assignment
#seekto 0x0080;
ul32 tot; // 0x0080 seconds (0=Off)
ul32 unknown_0084; // 0x0084
ul32 freq_step; // 0x0088 Hz
#seekto 0x0090;
ul32 scan_mode; // 0x0090 0=Search, 1=Carrier, 2=Time
ul32 keytone; // 0x0094 0=Off, 1=On
#seekto 0x0118;
ul32 vox_delay; // 0x0118 ms
#seekto 0x019C;
ul32 unknown_019c[4]; // 0x019C
#seekto 0x01BC;
ul32 recorder; // 0x01BC 0=Off, 1=On
ul32 work_mode; // 0x01C0 0=Channel, 1=Memory
ul32 unknown_01c4; // 0x01C4
ul32 squelch; // 0x01C8 1-9 (Sq Level)
ul32 unknown_01cc; // 0x01CC
ul32 unknown_01d0; // 0x01D0
ul32 ai_denoise_packed; // 0x01D4 bits 0:1 = AI denoise enable
ul32 unknown_01d8; // 0x01D8
ul32 ctcss_tail_tone; // 0x01DC CTCSS tail freq (freq*40+2)
ul32 cdcss_tail_tone; // 0x01E0 CDCSS tail freq (freq*40+2)
ul32 no_ctcss_tail_freq; // 0x01E4 NO CTCSS tail freq (Hz*10)
u8 unknown_01e8[6]; // 0x01E8
il16 micgain_analog; // 0x01EE -3 to 3
#seekto 0x0330;
char dtmf_local_id[8]; // 0x0330
u8 unknown_0338; // 0x0338
u8 unknown_0339; // 0x0339
char dtmf_sep_code; // 0x033A
char dtmf_group_code; // 0x033B
ul16 unknown_033c; // 0x033C
ul16 dtmf_first_time; // 0x033E 0.5ms units (e.g. 400=200ms)
#seekto 0x0347;
u8 tot_warning; // 0x0347 value << 4 (0=Off, 1-10)
#seekto 0x035C;
ul32 settings_035c; // 0x035C byte0=tx_start_beep, bytes1-3=pri_ch
#seekto 0x0360;
struct {
u8 cfg_unk0:1, // +00 bit 7
highpower:1, // bit 6
cfg_unk1:1, // bit 5
busylock:1, // bit 4
talkaround:1, // bit 3
cfg_unk2:3; // bits 2-0 (default 0b101)
ul16 bandwidth; // +01 25000=Wide, 12500=Narrow
u8 pad0; // +03
ul32 rxfreq; // +04 Hz
ul32 rxtone; // +08 custom encoding
ul32 txfreq; // +0C Hz
ul32 txtone; // +10 custom encoding
u8 unknown1[20]; // +14
char name[12]; // +28
ul16 unk15:1, // bit 15
compand:1, // bit 14
unk13_12:2, // bits 13-12
unk11:1, // bit 11 (default 1)
unk10:1, // bit 10
dtmf_group:4, // bits 9-6 (0-indexed)
ani:1, // bit 5
scanadd:1, // bit 4
unk3:1, // bit 3
pttid:3; // bits 2-0 (1=Off,2=Start,3=End,4=Both)
ul16 scramble; // +36 Hz (0xFFFF=Off)
} memory[256];
#seekto 0x3C64;
ul32 dwatch; // 0x3C64 1=Off, 2=On
#seekto 0x3C80;
ul32 group_call_ch; // 0x3C80 0-indexed channel number
ul32 noaa_ch; // 0x3C84 0-indexed (0=WX1 .. 6=WX7)
#seekto 0x3D34;
ul32 noaa_freq; // 0x3D34 NOAA freq in Hz (auto from noaa_ch)
#seekto 0x3D74;
ul32 key_lock; // 0x3D74 ms (0=Off)
ul32 settings_3d78; // 0x3D78 bits0:1=fm_auto
// (1=On,2=Off), bit2=allow_tx_beep
ul32 unknown_3d7c; // 0x3D7C
ul32 unknown_3d80[7]; // 0x3D80
ul32 lcd_brightness; // 0x3D9C 1-5
ul32 auto_bl_time; // 0x3DA0 ms
ul32 apo_time; // 0x3DA4 ms (0=Off)
ul32 password; // 0x3DA8 pwd*4 + flag (1=on, 0=off)
ul32 chnameshow; // 0x3DAC 0=Off, 1=On
ul32 tone_burst; // 0x3DB0 Hz (1750, 2100, etc.)
ul32 abswitch; // 0x3DB4 0=Auto, 1=On, 2=Off
ul32 css_packed; // 0x3DB8 bit0=css_vague, bit4=cancel_all_ctdt
ul32 unknown_3dbc; // 0x3DBC
ul32 unknown_3dc0; // 0x3DC0
ul32 show_bat; // 0x3DC4 0=Off, 1=On
ul32 dtmf_show; // 0x3DC8 0=Off, 1=On
ul32 unknown_3dcc; // 0x3DCC
ul32 group_switch; // 0x3DD0 0=Off, 1=On
ul32 unknown_3dd4; // 0x3DD4
ul32 noaa_warning; // 0x3DD8 0=Off, 0x40000=On
"""
# Protocol constants
STX = 0x02
ETX = 0x03
DLE = 0x10
# Bytes that must be DLE-escaped in the protocol
ESCAPE_MAP = {0x00: 0x13, 0x02: 0x01, 0x03: 0x11, 0x10: 0x12, 0xFF: 0x14}
UNESCAPE_MAP = {v: k for k, v in ESCAPE_MAP.items()}
# Protocol commands (byte 1 of request/response frames)
CMD_IDENT = 0x01
CMD_READ = 0x02
CMD_WRITE = 0x03
CMD_VERIFY = 0x04
CMD_FINALIZE = 0x05
# Table IDs (byte 2 — selects which data table to access)
TBL_SYSCONFIG = 0x01
TBL_RFCAL = 0x02
TBL_SETTINGS = 0x03
TBL_CHANNELS = 0x04
TBL_EXTRA = 0x05
TBL_FINAL = 0x07
# Memory layout — user data region (uploaded back to radio)
IDENT_SIZE = 28
SETTINGS_OFFSET = 0x001C
SETTINGS_SIZE = 836
CHANNELS_OFFSET = 0x0360
CHANNEL_SIZE = 56
NUM_CHANNELS = 256
EXTRA_OFFSET = 0x3B60
EXTRA_COUNT = 8
EXTRA_SIZE = 32
FINAL_OFFSET = 0x3C60
FINAL_SIZE = 480
# Calibration region (downloaded but not uploaded)
SYSCONFIG_OFFSET = 0x3E40
SYSCONFIG_SIZE = 1100
RFCAL_OFFSET = 0x428C
RFCAL_COUNT = 10
RFCAL_SIZE = 36
MEMSIZE = 17396
IMAGE_VERSION = 1
POWER_LEVELS = [
chirp_common.PowerLevel("High", watts=5.00),
chirp_common.PowerLevel("Low", watts=0.50),
]
# NOAA WX frequencies
NOAA_FREQS = [
162550000, # WX1: 162.550 MHz
162400000, # WX2: 162.400 MHz
162475000, # WX3: 162.475 MHz
162425000, # WX4: 162.425 MHz
162450000, # WX5: 162.450 MHz
162500000, # WX6: 162.500 MHz
162525000, # WX7: 162.525 MHz
]
# Valid GMRS TX frequencies (main channels + repeater outputs)
GMRS_TX_FREQS = (
set(bandplan_na.ALL_GMRS_FREQS) |
{f + 5000000 for f in bandplan_na.GMRS_HIRPT}
)
# Tail elimination frequency options (50.0-260.0 Hz in 0.1 Hz steps)
TAIL_TONES = [round(50.0 + i * 0.1, 1) for i in range(2101)]
# CTCSS/CDCSS tail freq encoding: freq_hz * 40 + 2
TAIL_TONE_MAP = [("%.1f" % t, int(round(t * 40)) + 2)
for t in TAIL_TONES]
# NO CTCSS tail freq encoding: freq_hz * 10
NO_CTCSS_TAIL_MAP = [("%.1f" % t, int(round(t * 10)))
for t in TAIL_TONES]
# Per-channel setting maps
PTTID_MAP = [("Off", 1), ("Start", 2), ("End", 3), ("Both", 4)]
SCRAMBLE_MAP = ([("Off", 0xFFFF)] +
[("%d Hz" % f, f) for f in range(2700, 3500, 100)])
# Lists used in get_settings / get_memory
LIST_NOAA = ["WX%d" % (i + 1) for i in range(7)]
LIST_PRIORITY_SCAN = ["Off"] + ["CH%03d" % (i + 1) for i in range(256)]
# Valid DTMF symbol charset
DTMF_CHARS = "0123456789ABCD*#"
# --- DCS tone encoding/decoding ---
def _reverse_bits(val, nbits):
"""Reverse the bit order of an integer."""
result = 0
for i in range(nbits):
if val & (1 << i):
result |= (1 << (nbits - 1 - i))
return result
def _golay_remainder(data_12):
"""Compute Golay(23,12) check bits using DCS polynomial 0xAE3."""
msg = data_12 << 11
for i in range(11, -1, -1):
if msg & (1 << (i + 11)):
msg ^= (0xAE3 << i)
return msg & 0x7FF
def _dcs_encode(code_oct, pol='N'):
"""Encode a DCS code and polarity to the C2's 32-bit tone value.
The radio stores these in a format similar to the DCS wire format.
The encoding uses a Golay(23,12) codeword with the 9-bit DCS code
stored at bits [10:2] of the tone value. The codeword is bit-reversed
relative to the standard Golay encoding. Bit 25 indicates inverted
polarity. CTCSS and DCS values are distinguished by bit 1 (DCS=1).
"""
code_dec = int(str(code_oct), 8)
if pol == 'R':
# Inverted: compute Golay for the complemented code
code_9bit = (~code_dec) & 0x1FF
low3 = 0x06
else:
code_9bit = code_dec
low3 = 0x01
data_12 = (_reverse_bits(code_9bit, 9) << 3) | low3
rem = _golay_remainder(data_12)
cw23 = (data_12 << 11) | rem
cw23_rev = _reverse_bits(cw23, 23)
tone = (cw23_rev << 2) | 0x02
if pol == 'R':
# Set inversion flag (bit 25 = bit 1 of byte 3)
tone = tone | (0x02 << 24)
return tone
def _dcs_decode(tone_val):
"""Decode a 32-bit DCS tone value to (code_oct, polarity)."""
raw9 = (tone_val >> 2) & 0x1FF
inv_flag = (tone_val >> 25) & 1
if inv_flag:
code = (~raw9) & 0x1FF
return int('%o' % code), 'R'
else:
return int('%o' % raw9), 'N'
def _decode_tone(val):
"""Decode a 32-bit tone value to (mode, value, polarity).
Returns ('', 0, 'N') for no tone, ('Tone', freq_hz, 'N') for CTCSS,
or ('DTCS', code, pol) for DCS.
"""
if val == 0:
return '', 0, 'N'
if not (val & 0x02):
# CTCSS: bit 1 clear, value = tone_hz * 40 + 1
tone_hz = (val - 1) / 40.0
return 'Tone', tone_hz, 'N'
# DCS: bit 1 set
code, pol = _dcs_decode(val)
return 'DTCS', code, pol
def _encode_tone(mode, value, pol):
"""Encode tone parameters to a 32-bit value."""
if mode in ('Tone', 'TSQL'):
return int(round(value * 40)) + 1
elif mode == 'DTCS':
return _dcs_encode(value, pol)
return 0
# --- Protocol framing ---
def _dle_escape(data):
"""Apply DLE byte-stuffing to raw data."""
result = bytearray()
for b in data:
if b in ESCAPE_MAP:
result.append(DLE)
result.append(ESCAPE_MAP[b])
else:
result.append(b)
return bytes(result)
def _dle_unescape(data):
"""Remove DLE byte-stuffing from escaped data."""
result = bytearray()
i = 0
while i < len(data):
if data[i] == DLE and i + 1 < len(data):
escaped = data[i + 1]
if escaped in UNESCAPE_MAP:
result.append(UNESCAPE_MAP[escaped])
else:
result.append(escaped)
i += 2
else:
result.append(data[i])
i += 1
return bytes(result)
def _make_frame(payload):
"""Build a complete STX/ETX frame with checksum and DLE escaping."""
chk = sum(payload) & 0xFF
raw = payload + bytes([chk])
return bytes([STX]) + _dle_escape(raw) + bytes([ETX])
def _parse_frame(frame_data):
"""Extract and verify payload from a received frame.
Returns the payload (without checksum) or raises RadioError.
"""
if not frame_data or frame_data[0] != STX or frame_data[-1] != ETX:
raise errors.RadioError("Invalid frame")
inner = _dle_unescape(frame_data[1:-1])
if len(inner) < 2:
raise errors.RadioError("Frame too short")
payload = inner[:-1]
chk = inner[-1]
expected = sum(payload) & 0xFF
if chk != expected:
raise errors.RadioError(
"Checksum mismatch: got 0x%02x, expected 0x%02x" %
(chk, expected))
return payload
# --- Serial communication ---
def _read_frame(pipe, timeout=5.0):
"""Read a complete STX...ETX frame from the serial port.
Uses byte-at-a-time blocking reads to track DLE byte-stuffing
and correctly identify the real ETX terminator.
"""
data = bytearray()
in_escape = False
start = time.monotonic()
while time.monotonic() - start < timeout:
try:
b = pipe.read(1)
except Exception as e:
raise errors.RadioError(
"Error reading from radio: %s" % e)
if not b:
continue
data.append(b[0])
if len(data) == 1:
if b[0] != STX:
data = bytearray()
continue
if in_escape:
in_escape = False
continue
if b[0] == DLE:
in_escape = True
continue
if b[0] == ETX:
return bytes(data)
if data:
LOG.debug("Incomplete frame (%d bytes): %s",
len(data), data[:20].hex())
return b''
class NoData(errors.RadioError):
"""Internal signal that we received nothing from the radio.
Used to distinguish between no response and incorrect response. Distinct
from errors.RadioNoResponse, which is raised _only_ when we have not
receieved any response from the radio in this session (compared to just
a sudden lack of response to a particular command).
"""
pass
def _send_recv(radio, payload):
"""Send a command and receive the response.
Retries up to 3 times on timeout to handle occasional
timing-related failures on some USB serial adapters.
Returns the response payload (header + data, no checksum).
"""
frame = _make_frame(payload)
pipe = radio.pipe
for attempt in range(3):
try:
pipe.write(frame)
pipe.flush()
except Exception as e:
raise errors.RadioError(
"Error communicating with radio: %s" % e)
resp_frame = _read_frame(pipe)
if resp_frame:
return _parse_frame(resp_frame)
LOG.warning("No response on attempt %d", attempt + 1)
# Drain any garbage data before retrying
try:
avail = pipe.in_waiting
if avail:
pipe.read(avail)
except Exception:
pass
raise NoData(
"No response from radio after 3 attempts (cmd=%s)" %
' '.join('%02x' % b for b in payload))
def _read_block(radio, cmd, table, index):
"""Read a data block from the radio.
Returns just the data portion (no header bytes).
"""
payload = bytes([0x01, cmd, table, index])
resp = _send_recv(radio, payload)
# Response format: [resp_type, cmd, table, index, ...data...]
if len(resp) < 4 or resp[1:4] != payload[1:4]:
raise errors.RadioError(
"Unexpected response header: got %s, expected cmd=%02x "
"tbl=%02x idx=%02x" % (resp[:4].hex(), cmd, table, index))
return resp[4:]
def _write_block(radio, cmd, table, index, data):
"""Write a data block to the radio."""
payload = bytes([0x01, cmd, table, index]) + data
frame = _make_frame(payload)
pipe = radio.pipe
try:
pipe.write(frame)
pipe.flush()
except Exception as e:
raise errors.RadioError("Error communicating with radio: %s" % e)
resp_frame = _read_frame(pipe)
if not resp_frame:
avail = pipe.in_waiting
if avail:
raw = pipe.read(avail)
LOG.error("Got non-frame data: %s", raw.hex())
raise errors.RadioError(
"No response to write (cmd=%02x tbl=%02x idx=%02x, %d bytes)" %
(cmd, table, index, len(data)))
resp = _parse_frame(resp_frame)
if len(resp) < 4 or resp[1:4] != payload[1:4]:
raise errors.RadioError(
"Unexpected write response: got %s, expected cmd=%02x "
"tbl=%02x idx=%02x" % (resp[:4].hex(), cmd, table, index))
return resp
# --- Download / Upload ---
def do_download(radio):
"""Download memory image from the radio."""
status = chirp_common.Status()
status.msg = "Cloning from radio"
status.cur = 0
status.max = MEMSIZE
mmap_data = bytearray(MEMSIZE)
offset = 0
# 1. Read ident
try:
data = _read_block(radio, CMD_IDENT, 0x00, 0x00)
except NoData:
raise errors.RadioNoResponse()
except Exception:
raise
mmap_data[offset:offset + len(data)] = data
offset += len(data)
status.cur = offset
radio.status_fn(status)
# 2. Read settings
data = _read_block(radio, CMD_READ, TBL_SETTINGS, 0x00)
mmap_data[offset:offset + len(data)] = data
offset += len(data)
status.cur = offset
radio.status_fn(status)
# 3. Read channels (256)
for i in range(NUM_CHANNELS):
data = _read_block(radio, CMD_READ, TBL_CHANNELS, i)
mmap_data[offset:offset + len(data)] = data
offset += len(data)
status.cur = offset
if i % 16 == 0:
radio.status_fn(status)
# 4. Read extra blocks
for i in range(EXTRA_COUNT):
data = _read_block(radio, CMD_READ, TBL_EXTRA, i)
mmap_data[offset:offset + len(data)] = data
offset += len(data)
# 5. Read final block
data = _read_block(radio, CMD_READ, TBL_FINAL, 0x00)
mmap_data[offset:offset + len(data)] = data
offset += len(data)
# 6. Read calibration data (downloaded but not uploaded)
data = _read_block(radio, CMD_READ, TBL_SYSCONFIG, 0x00)
mmap_data[offset:offset + len(data)] = data
offset += len(data)
for i in range(RFCAL_COUNT):
data = _read_block(radio, CMD_READ, TBL_RFCAL, i)
mmap_data[offset:offset + len(data)] = data
offset += len(data)
status.cur = MEMSIZE
radio.status_fn(status)
LOG.debug("Downloaded %d bytes", offset)
radio._metadata['image_version'] = IMAGE_VERSION
return memmap.MemoryMapBytes(bytes(mmap_data[:offset]))
def do_upload(radio):
"""Upload memory image to the radio."""
status = chirp_common.Status()
status.msg = "Uploading to radio"
status.cur = 0
status.max = MEMSIZE
mmap = radio.get_mmap()
# 1. Read ident (handshake)
try:
_read_block(radio, CMD_IDENT, 0x00, 0x00)
except NoData:
raise errors.RadioNoResponse()
except Exception:
raise
# 2. Read current settings (verify communication)
_read_block(radio, CMD_READ, TBL_SETTINGS, 0x00)
# 3. Write settings
settings_data = mmap[SETTINGS_OFFSET:SETTINGS_OFFSET + SETTINGS_SIZE]
_write_block(radio, CMD_WRITE, TBL_SETTINGS, 0x00, settings_data)
_read_block(radio, CMD_VERIFY, TBL_SETTINGS, 0x00)
status.cur = SETTINGS_OFFSET + SETTINGS_SIZE
radio.status_fn(status)
# 4. Write channels
for i in range(NUM_CHANNELS):
ch_offset = CHANNELS_OFFSET + i * CHANNEL_SIZE
ch_data = mmap[ch_offset:ch_offset + CHANNEL_SIZE]
_write_block(radio, CMD_WRITE, TBL_CHANNELS, i, ch_data)
_read_block(radio, CMD_VERIFY, TBL_CHANNELS, i)
status.cur = ch_offset + CHANNEL_SIZE
if i % 16 == 0:
radio.status_fn(status)
# 5. Write extra blocks
for i in range(EXTRA_COUNT):
blk_start = EXTRA_OFFSET + i * EXTRA_SIZE
blk_data = mmap[blk_start:blk_start + EXTRA_SIZE]
_write_block(radio, CMD_WRITE, TBL_EXTRA, i, blk_data)
_read_block(radio, CMD_VERIFY, TBL_EXTRA, i)
# 6. Write final block (448 of 480 bytes, matching CPS protocol)
final_data = mmap[FINAL_OFFSET:FINAL_OFFSET + 448]
_write_block(radio, CMD_WRITE, TBL_FINAL, 0x00, final_data)
_read_block(radio, CMD_VERIFY, TBL_FINAL, 0x00)
# 7. Finalize
_read_block(radio, CMD_FINALIZE, 0x00, 0x00)
status.cur = MEMSIZE
radio.status_fn(status)
# --- Main radio class ---
@directory.register
class RetevisC2(chirp_common.CloneModeRadio):
"""Retevis C2"""
VENDOR = "Retevis"
MODEL = "C2"
BAUD_RATE = 115200
WANTS_RTS = False
_memsize = MEMSIZE
POWER_LEVELS = POWER_LEVELS
VALID_BANDS = [(100000000, 520000000)]
def get_features(self):
rf = chirp_common.RadioFeatures()
rf.has_settings = True
rf.has_bank = False
rf.has_tuning_step = False
rf.has_rx_dtcs = True
rf.has_ctone = True
rf.has_cross = True
rf.has_name = True
rf.valid_name_length = 12
rf.valid_characters = (chirp_common.CHARSET_ALPHANUMERIC +
" !@#$%&*()-+=")
rf.valid_modes = ["FM", "NFM"]
rf.valid_skips = ["", "S"]
rf.valid_tmodes = ["", "Tone", "TSQL", "DTCS", "Cross"]
rf.valid_cross_modes = [
"Tone->Tone",
"DTCS->",
"->DTCS",
"Tone->DTCS",
"DTCS->Tone",
"->Tone",
"DTCS->DTCS",
]
rf.valid_duplexes = ["", "-", "+", "split", "off"]
rf.valid_power_levels = self.POWER_LEVELS
rf.valid_bands = self.VALID_BANDS
rf.valid_tuning_steps = chirp_common.TUNING_STEPS
rf.valid_dtcs_codes = chirp_common.DTCS_CODES
rf.can_odd_split = True
rf.memory_bounds = (1, NUM_CHANNELS)
return rf
def process_mmap(self):
self._memobj = bitwise.parse(MEM_FORMAT, self._mmap)
def sync_in(self):
self._mmap = do_download(self)
self.process_mmap()
def sync_out(self):
do_upload(self)
def get_raw_memory(self, number):
return repr(self._memobj.memory[number - 1])
def get_memory(self, number):
_mem = self._memobj.memory[number - 1]
mem = chirp_common.Memory()
mem.number = number
if int(_mem.rxfreq) == 0:
mem.empty = True
return mem
mem.freq = int(_mem.rxfreq)
# TX freq / duplex
txfreq = int(_mem.txfreq)
if txfreq == 0 or txfreq == 0xFFFFFFFF:
mem.duplex = "off"
mem.offset = 0
else:
offset = txfreq - mem.freq
if offset == 0:
mem.duplex = ""
mem.offset = 0
elif offset > 0:
if chirp_common.is_split(
self.get_features().valid_bands,
mem.freq, txfreq):
mem.duplex = "split"
mem.offset = txfreq
else:
mem.duplex = "+"
mem.offset = offset
elif offset < 0:
mem.duplex = "-"
mem.offset = abs(offset)
# Mode (bandwidth)
bw = int(_mem.bandwidth)
mem.mode = "FM" if bw == 25000 else "NFM"
# Power
if _mem.highpower:
mem.power = self.POWER_LEVELS[0] # High
else:
mem.power = self.POWER_LEVELS[1] # Low
# Tones
txtone = _decode_tone(int(_mem.txtone))
rxtone = _decode_tone(int(_mem.rxtone))
chirp_common.split_tone_decode(mem, txtone, rxtone)
# Name
name = str(_mem.name).rstrip('\x00').rstrip()
mem.name = name
# Skip (scanadd: 1=add, 0=skip)
if not _mem.scanadd:
mem.skip = "S"
# Per-channel extras
mem.extra = RadioSettingGroup("Extra", "extra")
rs = RadioSetting("_compand", "Compand",
RadioSettingValueBoolean(
bool(_mem.compand)))
mem.extra.append(rs)
rs = RadioSetting("_talkaround", "Talk Around",
RadioSettingValueBoolean(
bool(_mem.talkaround)))
mem.extra.append(rs)
rs = RadioSetting("_busylock", "Busy Lock",
RadioSettingValueBoolean(
bool(_mem.busylock)))
mem.extra.append(rs)
rs = RadioSetting("_ani", "ANI",
RadioSettingValueBoolean(
bool(_mem.ani)))
mem.extra.append(rs)
rs = RadioSetting("_pttid", "PTT ID",
RadioSettingValueMap(
PTTID_MAP, int(_mem.pttid)))
mem.extra.append(rs)
dtmf_grp = int(_mem.dtmf_group)
rs = RadioSetting("_dtmf_group", "DTMF Group",
RadioSettingValueInteger(
1, 16, dtmf_grp + 1))
mem.extra.append(rs)
rs = RadioSetting("_scramble", "Scramble",
RadioSettingValueMap(
SCRAMBLE_MAP, int(_mem.scramble)))
mem.extra.append(rs)
# GMRS implied modes
mem.mode, mem.power = self._apply_gmrs_limits(mem)
return mem
def _compute_tx_freq(self, mem):
"""Compute TX frequency from memory duplex settings.
Returns None if TX is disabled (duplex='off').
"""
if mem.duplex == "off":
return None
elif mem.duplex == "split":
return mem.offset
elif mem.duplex == "+":
return mem.freq + mem.offset
elif mem.duplex == "-":
return mem.freq - mem.offset
return mem.freq
def _apply_gmrs_limits(self, mem):
"""Compute GMRS frequency-based restrictions (implied modes).
Returns (mode, power) tuple. Interstitial channels
(467.5625-467.7125 MHz) are forced to NFM and low power.
"""
tx_freq = self._compute_tx_freq(mem)
if tx_freq is not None and tx_freq in bandplan_na.GMRS_HHONLY:
return "NFM", self.POWER_LEVELS[1]
return mem.mode, mem.power
def set_memory(self, mem):
_mem = self._memobj.memory[mem.number - 1]
if mem.empty:
_mem.set_raw(b'\x00' * 56)
_mem.bandwidth = 25000
_mem.highpower = 1
_mem.cfg_unk2 = 5 # CPS default (bits 2,0 set)
_mem.scanadd = 1
_mem.pttid = 1 # Off
_mem.unk11 = 1 # CPS default (bit 11)
_mem.scramble = 0xFFFF
return
_mem.rxfreq = mem.freq
# TX freq
if mem.duplex == "off":
_mem.txfreq = 0
elif mem.duplex == "split":
_mem.txfreq = mem.offset
elif mem.duplex == "+":
_mem.txfreq = mem.freq + mem.offset
elif mem.duplex == "-":
_mem.txfreq = mem.freq - mem.offset
else:
_mem.txfreq = mem.freq
# Enforce GMRS HH-only limits on write
tx_freq = self._compute_tx_freq(mem)
is_hhonly = tx_freq is not None and tx_freq in bandplan_na.GMRS_HHONLY
# Mode
if is_hhonly:
_mem.bandwidth = 12500
else:
_mem.bandwidth = 25000 if mem.mode == "FM" else 12500
# Power
_mem.highpower = (not is_hhonly and
mem.power == self.POWER_LEVELS[0])
# Tones
((txmode, txtone, txpol), (rxmode, rxtone, rxpol)) = \
chirp_common.split_tone_encode(mem)
_mem.txtone = _encode_tone(txmode, txtone, txpol)
_mem.rxtone = _encode_tone(rxmode, rxtone, rxpol)
# Name
name = mem.name.ljust(12, '\x00')[:12]
_mem.name = name
# Skip (scanadd: 1=add, 0=skip)
_mem.scanadd = mem.skip != "S"
_mem.pad0 = 0
# Per-channel extras - reset to defaults if not provided
# (e.g. CSV import or copy where extras are absent)
if not mem.extra:
_mem.busylock = 0
_mem.talkaround = 0
_mem.compand = 0
_mem.dtmf_group = 0
_mem.ani = 0
_mem.pttid = 1 # Off
_mem.scramble = 0xFFFF
return
for setting in mem.extra:
name = setting.get_name()
if name == "_compand":
_mem.compand = int(bool(setting.value))
elif name == "_talkaround":
_mem.talkaround = int(bool(setting.value))
elif name == "_busylock":
_mem.busylock = int(bool(setting.value))
elif name == "_ani":
_mem.ani = int(bool(setting.value))
elif name == "_pttid":
_mem.pttid = int(setting.value)
elif name == "_dtmf_group":
_mem.dtmf_group = int(setting.value) - 1
elif name == "_scramble":
_mem.scramble = int(setting.value)
def get_settings(self):
_mem = self._memobj
basic = RadioSettingGroup("basic", "Basic Settings")
vox = RadioSettingGroup("vox", "VOX Settings")
dtmf = RadioSettingGroup("dtmf", "DTMF Settings")
top = RadioSettings(basic, vox, dtmf)
ABSWITCH_MAP = [("Auto", 0), ("On", 1), ("Off", 2)]
STEP_MAP = [("2.5K", 2500), ("5.0K", 5000), ("6.25K", 6250),
("10.0K", 10000), ("12.5K", 12500), ("20.0K", 20000),
("25.0K", 25000), ("50.0K", 50000)]
TOT_MAP = [("Off", 0)] + [("%d" % x, x) for x in range(15, 615, 15)]
VOXDELAY_MAP = [("0.5", 500), ("1.0", 1000), ("1.5", 1500),
("2.0", 2000), ("2.5", 2500), ("3.0", 3000)]
basic.append(MemSetting("tot", "TOT (seconds)",
RadioSettingValueMap(
TOT_MAP, int(_mem.tot))))
TOTWARN_MAP = [("Off", 0)] + [
("%d" % x, x << 4) for x in range(1, 11)]
basic.append(MemSetting("tot_warning", "TOT Warning Time",
RadioSettingValueMap(
TOTWARN_MAP,
int(_mem.tot_warning))))
basic.append(MemSetting("freq_step", "Frequency Step",
RadioSettingValueMap(
STEP_MAP, int(_mem.freq_step))))
basic.append(MemSetting("abswitch", "A/B Switch",
RadioSettingValueMap(
ABSWITCH_MAP,
int(_mem.abswitch))))
KEY_MAP = [
("Off", 0xFFFFFFFF),
("Monitor", 1),
("TX Power", 4),
("Scan", 5),
("Flashlight", 6),
("Denoise", 12),
("Frequency Detect", 15),
("VOX", 17),
("Local Alarm", 20),
("Remote Alarm", 21),
("Weather", 22),
("Talk Around", 23),
("Reverse Frequency", 24),
]
SKEY_MAP = KEY_MAP + [("SUB-PTT", 25), ("Group PTT", 26)]
for field, label, kmap in [
("pf1_short", "P1 Short Press", KEY_MAP),
("pf1_long", "P1 Long Press", KEY_MAP),
("pf2_short", "P2 Short Press", KEY_MAP),
("pf2_long", "P2 Long Press", KEY_MAP),
("sk_short", "Side Key Short", SKEY_MAP),
("sk_long", "Side Key Long", KEY_MAP)]:
basic.append(MemSetting(
field, label,
RadioSettingValueMap(
kmap, int(getattr(_mem, field)))))
rs = RadioSettingValueInteger(1, 256, int(_mem.group_call_ch) + 1)
basic.append(RadioSetting(
"_group_call_ch", "Group Call Channel", rs))
basic.append(MemSetting(
"squelch", "Squelch Level",
RadioSettingValueInteger(0, 9, min(int(_mem.squelch), 9))))
basic.append(MemSetting("chnameshow",
"Display Channel Names",
RadioSettingValueBoolean(
bool(int(_mem.chnameshow)))))
basic.append(MemSetting("keytone", "Key Tone",
RadioSettingValueBoolean(
bool(int(_mem.keytone)))))
rs = RadioSettingValueBoolean(int(_mem.dwatch) == 2)
basic.append(RadioSetting("_dwatch", "Dual Watch", rs))
basic.append(MemSetting("group_switch", "Group Call",
RadioSettingValueBoolean(
bool(int(_mem.group_switch)))))
noaa_idx = int(_mem.noaa_ch)
if noaa_idx >= 7:
noaa_idx = 0
rs = RadioSettingValueList(LIST_NOAA, current_index=noaa_idx)
basic.append(RadioSetting(
"_noaa_ch", "NOAA Weather Channel", rs))
rs = RadioSettingValueBoolean(int(_mem.noaa_warning) != 0)
basic.append(RadioSetting(
"_noaa_warning", "NOAA Weather Warning", rs))
fm_raw = int(_mem.settings_3d78)
rs = RadioSettingValueBoolean((fm_raw & 0x03) == 1)
basic.append(RadioSetting("_fm_auto", "FM Radio", rs))
rs = RadioSettingValueBoolean(bool(fm_raw & 0x04))
basic.append(RadioSetting(
"_allow_tx_beep", "Allow TX Beep", rs))
# AI Denoise: ul32 = level*96 + 4 + (3 if on else 0)
ai_raw = int(_mem.ai_denoise_packed)
ai_enabled = (ai_raw & 0x03) == 0x03
ai_level = ((ai_raw & ~3) - 4) // 96
if ai_level < 1:
ai_level = 1
if ai_level > 10:
ai_level = 10
rs = RadioSettingValueBoolean(ai_enabled)
basic.append(RadioSetting("_ai_denoise", "AI Denoise", rs))
rs = RadioSettingValueInteger(1, 10, ai_level)
basic.append(RadioSetting(
"_ai_denoise_level", "AI Denoise Level", rs))
basic.append(MemSetting("recorder", "Recording Playback",
RadioSettingValueBoolean(
bool(int(_mem.recorder)))))
basic.append(MemSetting("show_bat", "Show Battery Voltage",
RadioSettingValueBoolean(
bool(int(_mem.show_bat)))))
basic.append(MemSetting("dtmf_show", "DTMF Show",
RadioSettingValueBoolean(
bool(int(_mem.dtmf_show)))))
rs = RadioSettingValueBoolean(bool(int(_mem.css_packed) & 0x01))
basic.append(RadioSetting("_css_vague", "CSS Vague", rs))
rs = RadioSettingValueBoolean(bool(int(_mem.css_packed) & 0x10))
basic.append(RadioSetting(
"_cancel_all_ctdt", "Cancel All CTCSS/DCS", rs))
basic.append(MemSetting("ctcss_tail_tone",
"CTCSS Tail Freq (Hz)",
RadioSettingValueMap(
TAIL_TONE_MAP,
int(_mem.ctcss_tail_tone))))
basic.append(MemSetting("cdcss_tail_tone",
"CDCSS Tail Freq (Hz)",
RadioSettingValueMap(
TAIL_TONE_MAP,
int(_mem.cdcss_tail_tone))))
basic.append(MemSetting("no_ctcss_tail_freq",
"NO CTCSS Tail Freq (Hz)",
RadioSettingValueMap(
NO_CTCSS_TAIL_MAP,
int(_mem.no_ctcss_tail_freq))))
rs = RadioSettingValueBoolean(bool(int(_mem.settings_0020) & 1))
basic.append(RadioSetting("_voice", "Voice Prompt", rs))
POWERSAVE_MAP = [("Off", 0), ("1", 1), ("2", 4),
("3", 6), ("4", 9)]
basic.append(MemSetting("power_save", "Battery Save",
RadioSettingValueMap(
POWERSAVE_MAP,
int(_mem.power_save))))
WORKMODE_MAP = [("Channel", 0), ("Memory", 1)]
basic.append(MemSetting("work_mode", "Work Mode",
RadioSettingValueMap(
WORKMODE_MAP,
int(_mem.work_mode))))
basic.append(MemSetting(
"micgain_analog", "Mic Gain (Analog)",
RadioSettingValueInteger(
-3, 3, max(-3, min(3, int(_mem.micgain_analog))))))
pri_raw = int(_mem.settings_035c)
rs = RadioSettingValueBoolean(bool(pri_raw & 0x01))
basic.append(RadioSetting(
"_tx_start_beep", "TX Start Beep", rs))
rs = RadioSettingValueBoolean(bool(pri_raw & 0x04))
basic.append(RadioSetting(
"_tx_stop_beep", "TX Stop Beep", rs))
pri_ch = pri_raw >> 8
if pri_ch == 0xFFFFFF:
pri_idx = 0
else:
pri_idx = pri_ch + 1
if pri_idx < 1 or pri_idx > 256:
pri_idx = 0
rs = RadioSettingValueList(
LIST_PRIORITY_SCAN, current_index=pri_idx)
basic.append(RadioSetting(
"_priority_scan", "Priority Scan Channel", rs))
SCANMODE_MAP = [("Search", 0), ("Carrier", 1), ("Time", 2)]
basic.append(MemSetting("scan_mode", "Scan Mode",
RadioSettingValueMap(
SCANMODE_MAP,
int(_mem.scan_mode))))
basic.append(MemSetting(
"lcd_brightness", "LCD Brightness",
RadioSettingValueInteger(
1, 5, min(max(int(_mem.lcd_brightness), 1), 5))))
KEYLOCK_MAP = [("Off", 0), ("5s", 5000), ("10s", 10000),
("15s", 15000), ("20s", 20000), ("25s", 25000),
("30s", 30000)]
basic.append(MemSetting("key_lock", "Key Lock",
RadioSettingValueMap(
KEYLOCK_MAP, int(_mem.key_lock))))
ABLTIME_MAP = [("Always", 0), ("5s", 5000), ("10s", 10000),
("15s", 15000), ("20s", 20000)]
basic.append(MemSetting("auto_bl_time", "Auto Backlight Time",
RadioSettingValueMap(
ABLTIME_MAP, int(_mem.auto_bl_time))))
APO_MAP = [("Off", 0), ("10min", 600000), ("30min", 1800000)] + \
[("%dhr" % h, h * 3600000) for h in range(1, 13)]
basic.append(MemSetting("apo_time", "Auto Power Off",
RadioSettingValueMap(
APO_MAP, int(_mem.apo_time))))
# Password: packed as pwd_int * 4 + flag (1=on, 0=off)
pwd_raw = int(_mem.password)
pwd_enabled = (pwd_raw % 4) == 1
pwd_num = pwd_raw // 4
pwd_str = "%06d" % pwd_num
rs = RadioSettingValueBoolean(pwd_enabled)
basic.append(RadioSetting(
"_pwd_enable", "Password Enable", rs))
rs = RadioSettingValueString(6, 6, pwd_str)
rs.set_charset("0123456789")
basic.append(RadioSetting("_pwd_code", "Password", rs))
TONE_MAP = [("1000", 1000), ("1450", 1450),
("1750", 1750), ("2100", 2100)]
basic.append(MemSetting("tone_burst", "Tone Burst (Hz)",
RadioSettingValueMap(
TONE_MAP, int(_mem.tone_burst))))
# VOX is packed: vox_level*4 + (1=on, 2=off)
vox_packed = int(_mem.vox_packed)
vox_on = (vox_packed % 4) == 1
vox_level = vox_packed // 4
if vox_level < 1:
vox_level = 1
if vox_level > 9:
vox_level = 5
rs = RadioSettingValueBoolean(vox_on)
vox.append(RadioSetting("_vox_enabled", "VOX Function", rs))
vox.append(RadioSetting(
"_vox_level", "VOX Level",
RadioSettingValueInteger(1, 9, vox_level)))
vox.append(MemSetting("vox_delay", "VOX Delay Time",
RadioSettingValueMap(
VOXDELAY_MAP, int(_mem.vox_delay))))
# DTMF settings
dtmf_id = str(_mem.dtmf_local_id).rstrip('\x00')
rs = RadioSettingValueString(0, 8, dtmf_id,
mem_pad_char='\x00')
rs.set_charset(DTMF_CHARS)
dtmf.append(MemSetting("dtmf_local_id", "DTMF Local ID", rs))
DTMF_FIRST_MAP = [("%dms" % ms, ms * 2)
for ms in range(150, 1010, 10)]
dtmf.append(MemSetting("dtmf_first_time",
"DTMF First Code Time",
RadioSettingValueMap(
DTMF_FIRST_MAP,
int(_mem.dtmf_first_time))))
LIST_SEP = ['*', '#', 'A', 'B', 'C', 'D']
sep = str(_mem.dtmf_sep_code)
sep_idx = LIST_SEP.index(sep) if sep in LIST_SEP else 0
rs = RadioSettingValueList(LIST_SEP, current_index=sep_idx)
dtmf.append(RadioSetting(
"dtmf_sep_code", "Separate Code", rs))
LIST_GRP = ['A', 'B', 'C', 'D', '*', '#']
grp = str(_mem.dtmf_group_code)
grp_idx = LIST_GRP.index(grp) if grp in LIST_GRP else 0
rs = RadioSettingValueList(LIST_GRP, current_index=grp_idx)
dtmf.append(RadioSetting(
"dtmf_group_code", "Group Code", rs))
return top
def set_settings(self, settings):
_mem = self._memobj
for element in settings:
if isinstance(element, MemSetting):
element.apply_to_memobj(_mem)
elif isinstance(element, RadioSetting):
# Packed/composite settings need manual encoding
name = element.get_name()
if name == '_group_call_ch':
_mem.group_call_ch = int(element.value) - 1
elif name == '_fm_auto':
cur = int(_mem.settings_3d78) & ~0x03
_mem.settings_3d78 = \
cur | (1 if bool(element.value) else 2)
elif name == '_allow_tx_beep':
cur = int(_mem.settings_3d78)
if bool(element.value):
_mem.settings_3d78 = cur | 0x04
else:
_mem.settings_3d78 = cur & ~0x04
elif name == '_dwatch':
_mem.dwatch = 2 if bool(element.value) else 1
elif name == '_voice':
cur = int(_mem.settings_0020)
if bool(element.value):
_mem.settings_0020 = cur | 1
else:
_mem.settings_0020 = cur & ~1
elif name == '_vox_enabled':
# Repack VOX: vox_level*4 + (1=on, 2=off)
cur = int(_mem.vox_packed)
level = cur // 4
flag = 1 if bool(element.value) else 2
_mem.vox_packed = level * 4 + flag
elif name == '_vox_level':
cur = int(_mem.vox_packed)
flag = cur % 4
_mem.vox_packed = int(element.value) * 4 + flag
elif name == '_pwd_enable':
cur = int(_mem.password)
pwd = cur // 4
if bool(element.value):
_mem.password = pwd * 4 + 1
else:
_mem.password = pwd * 4
elif name == '_pwd_code':
cur = int(_mem.password)
flag = cur % 4
_mem.password = \
int(str(element.value)) * 4 + flag
elif name == '_ai_denoise':
cur = int(_mem.ai_denoise_packed)
if bool(element.value):
_mem.ai_denoise_packed = (cur & ~3) | 3
else:
_mem.ai_denoise_packed = cur & ~3
elif name == '_ai_denoise_level':
cur = int(_mem.ai_denoise_packed)
enable = cur & 3
_mem.ai_denoise_packed = \
int(element.value) * 96 + 4 + enable
elif name == '_tx_start_beep':
cur = int(_mem.settings_035c)
if bool(element.value):
_mem.settings_035c = cur | 0x01
else:
_mem.settings_035c = cur & ~0x01
elif name == '_tx_stop_beep':
cur = int(_mem.settings_035c)
if bool(element.value):
_mem.settings_035c = cur | 0x04
else:
_mem.settings_035c = cur & ~0x04
elif name == '_priority_scan':
cur = int(_mem.settings_035c) & 0xFF
idx = int(element.value)
if idx == 0:
_mem.settings_035c = \
(0xFFFFFF << 8) | cur
else:
_mem.settings_035c = \
((idx - 1) << 8) | cur
elif name == '_css_vague':
cur = int(_mem.css_packed)
if bool(element.value):
_mem.css_packed = cur | 0x01
else:
_mem.css_packed = cur & ~0x01
elif name == '_cancel_all_ctdt':
cur = int(_mem.css_packed)
if bool(element.value):
_mem.css_packed = cur | 0x10
else:
_mem.css_packed = cur & ~0x10
elif name == '_noaa_ch':
idx = int(element.value)
_mem.noaa_ch = idx
_mem.noaa_freq = NOAA_FREQS[idx]
elif name == '_noaa_warning':
cur = int(_mem.noaa_warning)
if bool(element.value):
_mem.noaa_warning = cur | 0x40000
else:
_mem.noaa_warning = cur & ~0x40000
elif name in ('dtmf_sep_code', 'dtmf_group_code'):
setattr(_mem, name, str(element.value))
else:
LOG.warning("Unhandled setting: %s", name)
else:
self.set_settings(element)
def validate_memory(self, mem):
msgs = super().validate_memory(mem)
if mem.empty:
return msgs
tx_freq = self._compute_tx_freq(mem)
if tx_freq is not None:
if tx_freq not in GMRS_TX_FREQS:
msgs.append(chirp_common.ValidationWarning(
"TX frequency %s is not a GMRS frequency"
% chirp_common.format_freq(tx_freq)))
if tx_freq in bandplan_na.GMRS_HHONLY:
if mem.mode != "NFM":
msgs.append(chirp_common.ValidationWarning(
"GMRS 467 MHz channels require NFM"))
if mem.power and mem.power != self.POWER_LEVELS[1]:
msgs.append(chirp_common.ValidationWarning(
"GMRS 467 MHz channels require low power"))
return msgs
chirp-20261030/chirp/drivers/retevis_h777v4.py 0000644 0001750 0001750 00000061142 15175067036 017714 0 ustar hibby hibby # Copyright 2025 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 .
import logging
import struct
from chirp import (
bitwise,
chirp_common,
directory,
errors,
memmap,
util,
)
from chirp.drivers import h777
from chirp.settings import (
MemSetting,
RadioSetting,
RadioSettingGroup,
RadioSettings,
RadioSettingValueBoolean,
RadioSettingValueInvertedBoolean,
RadioSettingValueInteger,
RadioSettingValueList,
)
LOG = logging.getLogger(__name__)
MEM_FORMAT = """
struct {
ul32 rxfreq; // 0-3
ul32 txfreq; // 4-7
lbcd rx_tone[2]; // 8-9
lbcd tx_tone[2]; // A-B
u8 unknown0:2, // C
scramb:1, // Scramble 0 = Off, 1 = On
scanadd:1, // Scan Add 0 = Scan, 1 = Skip
ishighpower:1, // Power Level 0 = Low, 1 = High
narrow:1, // Bandwidth 0 = Wide, 1 = Narrow
unknown1:1, //
bcl:1; // Busy Channel Lockout 0 = On, 1 = Off
} memory[16];
struct {
u8 codesw:1, // 00D0 Code Switch
scanm:1, // Scan Mode
voxs:1, // VOX Switch
roger:1, // Roger
voice:1, // Voice Annunciation
unknown_0:1,
save:1, // Battery Save
beep:1; // Beep Tone
u8 squelch; // 00D1 Squelch Level
u8 tot; // 00D2 Time-out Timer
u8 vox; // 00D3 VOX Level
u8 voxd; // 00D4 Vox Delay
u8 unknown_1;
u8 unknown_2;
u8 skey2; // 00D7 Side Key 2 (long)
u8 unknown_3[5]; // 00D8 - 00DC
u8 password[6]; // 00DD - 00E2 Password
} settings;
"""
MEM_FORMAT_RT21H = """
struct {
ul24 rxfreq; // 0-2
ul24 txfreq; // 3-5
lbcd rx_tone[2]; // 6-7
lbcd tx_tone[2]; // 8-9
u8 speccode:1, // A Spec Code 0 = On, 1 = Off
compand:1, // Compander 0 = On, 1 = Off
scramb:1, // Scramble 0 = Off, 1 = On
scanadd:1, // Scan Add 0 = Scan, 1 = Skip
ishighpower:1, // Power Level 0 = Low, 1 = High
narrow:1, // Bandwidth 0 = Wide, 1 = Narrow
unknown1:1, //
bcl:1; // Busy Channel Lockout 0 = On, 1 = Off
u8 scrambopt; // B Scramble Options
} memory[16];
struct {
u8 unknown_4:1, // 00C0
scanm:1, // Scan Mode
roger:1, // Roger
lowbattwarn:1, // Low Battery Warning
unknown_0:1,
voice:1, // Voice Annunciation
save:1, // Battery Save
voxs:1; // VOX Switch
u8 squelch; // 00C1 Squelch Level
u8 tot; // 00C2 Time-out Timer
u8 voxd; // 00C3 Vox Delay
u8 vox_level; // 00C4 VOX Level
u8 unknown_1;
u8 unknown_2;
u8 skey1L:4, // 00C7 Side Key 1 (long)
skey1S:4; // Side Key 1 (short)
u8 unknown_3:5, // 00C8
removectdcs:1, // Remove CT/DCS
beep:1, // Beep Tone
specmode:1; // Spec Mode
} settings;
"""
CMD_ACK = b"\x06"
DTCS = tuple(sorted(chirp_common.DTCS_CODES + (645,)))
OFF1TO9_LIST = ["Off"] + ["%s" % x for x in range(1, 10)]
ONE_TO_NINE_LIST = OFF1TO9_LIST[1:] # ["1", "2", ..., "9"]
SCANM_LIST = ["Carrier", "Time"]
SCRAMBOPT_LIST = ["1", "2", "3", "4", "5", "6", "7", "8"]
SPECMODE_LIST = ["Spec Code 1", "Spec Code 2"]
SKEY1_LIST = ["Off", "Monitor", "Scan", "Channel Lock", "VOX", "Power",
"Alarm", "Roger"]
SKEY2_LIST = ["Off", "VOX", "Power", "Scan"]
TIMEOUTTIMER_LIST = ["Off"] + ["%s seconds" % x for x in range(30, 210, 30)]
VOICE_LIST = ["Off", "English"]
MODEL_CONFIG = {
"RT21H": {
"base_freq_offset": 0x2000000,
"freq_storage_bits": 24,
},
}
DEFAULT_CONFIG = {
"base_freq_offset": 0,
"freq_storage_bits": 32,
}
def _read_block(radio, block_addr, block_size):
serial = radio.pipe
cmd = struct.pack(">cHb", b'R', block_addr, block_size)
expectedresponse = b"W" + cmd[1:]
LOG.debug("Reading block %04x..." % (block_addr))
try:
serial.write(cmd)
response = serial.read(4 + block_size)
if response[:4] != expectedresponse:
raise Exception("Error reading block %04x." % (block_addr))
block_data = response[4:]
except Exception:
raise errors.RadioError("Failed to read block at %04x" % block_addr)
return block_data
def _write_block(radio, block_addr, block_size):
serial = radio.pipe
cmd = struct.pack(">cHb", b'W', block_addr, block_size)
data = radio.get_mmap()[block_addr:block_addr + block_size]
LOG.debug("Writing Data:")
LOG.debug(util.hexprint(cmd + data))
try:
serial.write(cmd + data)
if serial.read(1) != CMD_ACK:
raise Exception("No ACK")
except Exception:
raise errors.RadioError("Failed to send block "
"to radio at %04x" % block_addr)
def do_download(radio):
LOG.debug("download")
if not radio.is_detected() and not radio.detected_models():
# If we are a detect-ee or detect-or, don't re-initialize programming
# mode. But if we're one of the other subclasses of this work that
# would not have run through detection, we need to do so.
h777._h777_enter_single_programming_mode(radio)
data = b""
status = chirp_common.Status()
status.msg = "Cloning from radio"
status.cur = 0
status.max = radio._memsize
for addr in range(0, radio._memsize, radio.BLOCK_SIZE):
status.cur = addr + radio.BLOCK_SIZE
radio.status_fn(status)
block = _read_block(radio, addr, radio.BLOCK_SIZE)
data += block
LOG.debug("Address: %04x" % addr)
LOG.debug(util.hexprint(block))
return memmap.MemoryMapBytes(data)
def do_upload(radio):
status = chirp_common.Status()
status.msg = "Uploading to radio"
h777._h777_enter_single_programming_mode(radio)
status.cur = 0
status.max = radio._memsize
for start_addr, end_addr in radio._ranges:
for addr in range(start_addr, end_addr, radio.BLOCK_SIZE_UP):
status.cur = addr + radio.BLOCK_SIZE_UP
radio.status_fn(status)
_write_block(radio, addr, radio.BLOCK_SIZE_UP)
class H777V4BaseRadio(chirp_common.CloneModeRadio):
"""RETEVIS H777 V4 Base"""
VENDOR = "Retevis"
MODEL = "H777 V4 Base"
BAUD_RATE = 9600
BLOCK_SIZE = 0x0D
BLOCK_SIZE_UP = 0x0D
VOXD_LIST = ["0.5", "1.0", "1.5", "2.0", "2.5", "3.0"]
VOXD_DOC = "VOX Delay: 0.5, 1.0, 1.5, 2.0, 2.5, 3.0 seconds"
VOXL_DOC = "VOX Level: Off, 1, 2, 3, 4, 5, 6, 7, 8, 9"
_has_codeswitch = True
_has_compander = False
_has_low_battery_warning = False
_has_remove_ctdcs = False
_has_scramble_options = False
_has_spec_code = False
_has_sidekey1 = False
_has_sidekey2 = True
_has_spec_mode = False
_has_vox_level_off = True
POWER_LEVELS = [chirp_common.PowerLevel("Low", watts=0.50),
chirp_common.PowerLevel("High", watts=2.00)
]
VALID_BANDS = [(400000000, 520000000)]
PROGRAM_CMD = b"C777HAM"
_ranges = [
(0x0000, 0x00EA),
]
_memsize = 0x00EA
def get_features(self):
rf = chirp_common.RadioFeatures()
rf.has_settings = True
rf.has_bank = False
rf.has_ctone = True
rf.has_cross = True
rf.has_rx_dtcs = True
rf.has_tuning_step = False
rf.can_odd_split = True
rf.has_name = False
rf.valid_skips = ["", "S"]
rf.valid_tmodes = ["", "Tone", "TSQL", "DTCS", "Cross"]
rf.valid_cross_modes = ["Tone->Tone", "Tone->DTCS", "DTCS->Tone",
"->Tone", "->DTCS", "DTCS->", "DTCS->DTCS"]
rf.valid_power_levels = self.POWER_LEVELS
rf.valid_duplexes = ["", "-", "+", "split", "off"]
rf.valid_modes = ["FM", "NFM"] # 25 kHz, 12.5 kHz.
rf.valid_dtcs_codes = DTCS
rf.memory_bounds = (1, 16)
rf.valid_tuning_steps = [2.5, 5., 6.25, 10., 12.5, 20., 25., 50.]
rf.valid_bands = self.VALID_BANDS
return rf
def process_mmap(self):
self._memobj = bitwise.parse(MEM_FORMAT, self._mmap)
def sync_in(self):
"""Download from radio"""
try:
data = do_download(self)
except errors.RadioError:
# Pass through any real errors we raise
raise
except Exception:
# If anything unexpected happens, make sure we raise
# a RadioError and log the problem
LOG.exception('Unexpected error during download')
raise errors.RadioError('Unexpected error communicating '
'with the radio')
self._mmap = data
self.process_mmap()
def sync_out(self):
"""Upload to radio"""
try:
do_upload(self)
except errors.RadioError:
raise
except Exception:
# If anything unexpected happens, make sure we raise
# a RadioError and log the problem
LOG.exception('Unexpected error during upload')
raise errors.RadioError('Unexpected error communicating '
'with the radio')
def _decode_tone(self, toneval, txrx, chnum):
pol = "N"
rawval = (toneval[1].get_bits(0xFF) << 8) | toneval[0].get_bits(0xFF)
if toneval[0].get_bits(0xFF) == 0xFF:
mode = ""
val = 0
elif toneval[1].get_bits(0xC0) == 0xC0:
val = int(f'{rawval & 0x1FF:o}')
if val in DTCS:
mode = "DTCS"
pol = "R"
else:
LOG.error('unknown value: CH# %i DTCS %fR %s' % (
chnum, val, txrx))
mode = ""
val = 0
elif toneval[1].get_bits(0x80):
val = int(f'{rawval & 0x1FF:o}')
if val in DTCS:
mode = "DTCS"
else:
LOG.error('unknown value: CH# %i DTCS %fN %s' % (
chnum, val, txrx))
mode = ""
val = 0
else:
val = int(toneval) / 10.0
if val in chirp_common.TONES:
mode = "Tone"
else:
LOG.error('unknown value: CH# %i CTCSS %fHZ %s' % (
chnum, val, txrx))
mode = ""
val = 0
return mode, val, pol
def _encode_tone(self, _toneval, mode, val, pol):
toneval = 0
if mode == "Tone":
v = int(round(val * 10))
thousands = (v // 1000) % 10
hundreds = (v // 100) % 10
tens = (v // 10) % 10
ones = v % 10
toneval = (
(thousands << 12)
| (hundreds << 8)
| (tens << 4)
| ones
)
elif mode == "DTCS":
toneval = int('%i' % val, 8)
toneval |= 0x8000
if pol == "R":
toneval |= 0x4000
else:
toneval = 0xFFFF
_toneval[0].set_raw(toneval & 0xFF)
_toneval[1].set_raw((toneval >> 8) & 0xFF)
def _model_cfg(self):
cfg = MODEL_CONFIG.get(self.MODEL, DEFAULT_CONFIG).copy()
cfg["tx_unused_marker"] = (1 << cfg["freq_storage_bits"]) - 1
return cfg
def _decode_freq(self, raw):
cfg = self._model_cfg()
return (int(raw) + cfg["base_freq_offset"]) * 10
def get_raw_memory(self, number):
return repr(self._memobj.memory[number - 1])
def get_memory(self, number):
_mem = self._memobj.memory[number - 1]
mem = chirp_common.Memory()
mem.number = number
if _mem.get_raw()[:1] == b"\xFF":
mem.empty = True
return mem
cfg = self._model_cfg()
rx_hz = self._decode_freq(_mem.rxfreq)
mem.freq = rx_hz
if _mem.txfreq == cfg["tx_unused_marker"]:
mem.duplex = "off"
mem.offset = 0
else:
tx_hz = self._decode_freq(_mem.txfreq)
diff = rx_hz - tx_hz
if abs(diff) > 25_000_000:
mem.duplex = "split"
mem.offset = tx_hz
elif diff == 0:
mem.duplex = ""
mem.offset = 0
else:
mem.duplex = "-" if diff > 0 else "+"
mem.offset = abs(diff)
txmode, txval, txpol = self._decode_tone(_mem.tx_tone, 'TX', number)
rxmode, rxval, rxpol = self._decode_tone(_mem.rx_tone, 'RX', number)
chirp_common.split_tone_decode(mem,
(txmode, txval, txpol),
(rxmode, rxval, rxpol))
if _mem.scanadd:
mem.skip = "S"
mem.power = self.POWER_LEVELS[_mem.ishighpower]
mem.mode = _mem.narrow and "NFM" or "FM"
mem.extra = RadioSettingGroup("Extra", "extra")
# BCL (Busy Channel Lockout)
rs = RadioSettingValueInvertedBoolean(not bool(_mem.bcl))
rset = MemSetting("bcl", "BCL", rs)
rset.set_doc("Busy Channel Lockout")
mem.extra.append(rset)
# Scramble
rs = RadioSettingValueInvertedBoolean(not bool(_mem.scramb))
rset = MemSetting("scramb", "Scramble", rs)
rset.set_doc("Frequency inversion Scramble")
mem.extra.append(rset)
# Compander
if self._has_compander:
rs = RadioSettingValueInvertedBoolean(not bool(_mem.compand))
rset = MemSetting("compand", "Compander", rs)
rset.set_doc("Voice Compander")
mem.extra.append(rset)
# Spec Code
if self._has_spec_code:
rs = RadioSettingValueInvertedBoolean(not bool(_mem.speccode))
rset = MemSetting("speccode", "Spec Code", rs)
rset.set_doc("Spec Code")
mem.extra.append(rset)
# Scramble Options
if self._has_scramble_options:
if _mem.scrambopt > 0x07:
val = 0x00
else:
val = _mem.scrambopt
rs = RadioSettingValueList(SCRAMBOPT_LIST, current_index=val)
rset = MemSetting("scrambopt", "Scramble Options", rs)
rset.set_doc("Scramble Options: 1, 2, 3, 4, 5, 6, 7, 8")
mem.extra.append(rset)
return mem
def _encode_freq(self, hz):
cfg = self._model_cfg()
raw = int(hz / 10) - cfg["base_freq_offset"]
if not (0 <= raw <= cfg["tx_unused_marker"]):
raise ValueError(f"Frequency {hz} out of 24-bit range")
return raw
def set_memory(self, mem):
_mem = self._memobj.memory[mem.number - 1]
cfg = self._model_cfg()
if mem.empty:
_mem.set_raw(b"\xFF" * (_mem.size() // 8))
return
rx = self._encode_freq(mem.freq)
_mem.rxfreq = rx
if mem.duplex == "off":
tx = cfg["tx_unused_marker"]
elif mem.duplex == "split":
tx = self._encode_freq(mem.offset)
elif mem.duplex == "+":
tx = self._encode_freq(mem.freq + mem.offset)
elif mem.duplex == "-":
tx = self._encode_freq(mem.freq - mem.offset)
else:
tx = rx
_mem.txfreq = tx
(txmode, txval, txpol), (rxmode, rxval, rxpol) = \
chirp_common.split_tone_encode(mem)
self._encode_tone(_mem.tx_tone, txmode, txval, txpol)
self._encode_tone(_mem.rx_tone, rxmode, rxval, rxpol)
_mem.scanadd = mem.skip == "S"
_mem.narrow = mem.mode == "NFM"
_mem.ishighpower = mem.power == self.POWER_LEVELS[1]
for setting in mem.extra:
setting.apply_to_memobj(_mem)
def get_settings(self):
_settings = self._memobj.settings
basic = RadioSettingGroup("basic", "Basic Settings")
group = RadioSettings(basic)
# Squelch
rs = RadioSettingValueInteger(0, 9, _settings.squelch)
rset = MemSetting("squelch", "Squelch", rs)
rset.set_doc("Squelch Level: 0, 1, 2, 3, 4, 5, 6, 7, 8, 9")
basic.append(rset)
# Time Out Timer
rs = RadioSettingValueList(TIMEOUTTIMER_LIST,
current_index=_settings.tot)
rset = MemSetting("tot", "Time-out Timer", rs)
rset.set_doc("TX Time-out Timer: Off, 30, 60, 90, 120, 150," +
" 180 seconds")
basic.append(rset)
# Vox Level
if self._has_vox_level_off:
# H777 behavior: 0-9 maps directly
rs = RadioSettingValueList(
OFF1TO9_LIST,
current_index=_settings.vox
)
rset = MemSetting("vox", "VOX Level", rs)
else:
# RT21H behavior: hide "Off"
# Firmware: 1-9
# UI index: 0-8
index = min(8, max(0, _settings.vox_level - 1))
rs = RadioSettingValueList(
ONE_TO_NINE_LIST,
current_index=index
)
rset = MemSetting("vox_level", "VOX Level", rs)
rset.set_doc(self.VOXL_DOC)
basic.append(rset)
# Vox Delay
rs = RadioSettingValueList(self.VOXD_LIST,
current_index=_settings.voxd)
rset = MemSetting("voxd", "Vox Delay", rs)
rset.set_doc(self.VOXD_DOC)
basic.append(rset)
# Spec Mode
if self._has_spec_mode:
rs = RadioSettingValueList(SPECMODE_LIST,
current_index=_settings.specmode)
rset = MemSetting("specmode", "Spec Mode", rs)
rset.set_doc("Spec Mode: Spec Code 1, Spec Code 2")
basic.append(rset)
# Scan Mode
rs = RadioSettingValueList(SCANM_LIST, current_index=_settings.scanm)
rset = MemSetting("scanm", "Scan Mode", rs)
rset.set_doc("Scan Mode: Carrier, Time")
basic.append(rset)
# Voice Annunciation
rs = RadioSettingValueList(VOICE_LIST,
current_index=_settings.voice)
rset = MemSetting("voice", "Voice", rs)
rset.set_doc("Voice Prompts: Off, English")
basic.append(rset)
# Side Key 1
if self._has_sidekey1:
SKEY1_DOC = ("Off, Monitor, Scan, Channel Lock, VOX, Power," +
" Alarm, Roger")
# Side Key 1 (short)
rs = RadioSettingValueList(SKEY1_LIST,
current_index=_settings.skey1S)
rset = MemSetting("skey1S", "Side Key 1 (short)", rs)
rset.set_doc("Side Key 1 (short press): " + SKEY1_DOC)
basic.append(rset)
# Side Key 1 (long)
rs = RadioSettingValueList(SKEY1_LIST,
current_index=_settings.skey1L)
rset = MemSetting("skey1L", "Side Key 1 (long)", rs)
rset.set_doc("Side Key 1 (long press): " + SKEY1_DOC)
basic.append(rset)
# Side key 2
if self._has_sidekey2:
# Side Key 2 (long)
rs = RadioSettingValueList(SKEY2_LIST,
current_index=_settings.skey2)
rset = MemSetting("skey2", "Side Key 2", rs)
rset.set_doc("Side Key 2 (long press): Off, VOX, Power, Scan")
basic.append(rset)
# Code Switch
if self._has_codeswitch:
rs = RadioSettingValueBoolean(_settings.codesw)
rset = MemSetting("codesw", "Code Switch", rs)
rset.set_doc("Code Switch: Off, Enabled")
basic.append(rset)
# Battery Save
rs = RadioSettingValueBoolean(_settings.save)
rset = MemSetting("save", "Battery Save", rs)
rset.set_doc("Battery Save: Off, Enabled")
basic.append(rset)
# Beep Tone
rs = RadioSettingValueBoolean(_settings.beep)
rset = MemSetting("beep", "Beep", rs)
rset.set_doc("Beep Prompt: Off, Enabled")
basic.append(rset)
# VOX Switch
rs = RadioSettingValueBoolean(_settings.voxs)
rset = MemSetting("voxs", "VOX Switch", rs)
rset.set_doc("VOX Switch: Off, Enabled")
basic.append(rset)
# Low Battery Warning
if self._has_low_battery_warning:
rs = RadioSettingValueBoolean(_settings.lowbattwarn)
rset = MemSetting("lowbattwarn", "Low Battery Warning", rs)
rset.set_doc("Low Battery Warning: Off, Enabled")
basic.append(rset)
# Roger
rs = RadioSettingValueBoolean(_settings.roger)
rset = MemSetting("roger", "Roger", rs)
rset.set_doc("Roger: Off, Enabled")
basic.append(rset)
# Remove CT/DCS
if self._has_remove_ctdcs:
rs = RadioSettingValueBoolean(_settings.removectdcs)
rset = MemSetting("removectdcs", "Remove CT/DCS", rs)
rset.set_doc("Remove CT/DCS: Off, Enabled")
basic.append(rset)
return group
def set_settings(self, settings):
others = settings.apply_to(self._memobj.settings)
if others:
LOG.error('Did not apply %s' % others)
@classmethod
def match_model(cls, filedata, filename):
# This radio has always been post-metadata, so never do
# old-school detection
return False
@directory.register
@directory.detected_by(h777.RetevisH777)
class H777V4(H777V4BaseRadio):
"""RETEVIS H777V4"""
VENDOR = "Retevis"
MODEL = "H777"
VARIANT = 'V4'
IDENT = [b'\x00' * 6, b'\xFF' * 6]
# SKU #: A9294A (sold as FRS radio but supports full band TX/RX)
# Serial #: 2412R777XXXXXXX
# SKU #: A9294C (same as SKU #: A9294A (2 pack))
# Serial #: 2406R777XXXXXXX
# SKU #: A9294B (sold as PMR radio but supports full band TX/RX)
# Serial #: 2412R777XXXXXXX
# SKU #: A9294D (same as SKU #: A9294B (2 pack))
# Serial #: 24XXR777XXXXXXX
@directory.register
class RT21HRadio(H777V4BaseRadio):
"""RETEVIS RT21H"""
VENDOR = "Retevis"
MODEL = "RT21H"
# SKU #: A9118R (sold as FRS radio but supports full band TX/RX)
# SKU #: A9118S (sold as PMR radio but supports full band TX/RX)
IDENT = [b'SMP558\x02\xFF', b'SMP558\x02\x00']
BLOCK_SIZE = 0x0C
BLOCK_SIZE_UP = 0x0C
VOXD_LIST = ["0.5", "1.0", "1.5", "2.0", "2.5", "3.0", "3.5"]
VOXD_DOC = "VOX Delay: 0.5, 1.0, 1.5, 2.0, 2.5, 3.0, 3.5 seconds"
VOXL_DOC = "VOX Level: 1, 2, 3, 4, 5, 6, 7, 8, 9"
_has_codeswitch = False
_has_compander = True
_has_low_battery_warning = True
_has_remove_ctdcs = True
_has_scramble_options = True
_has_sidekey1 = True
_has_sidekey2 = False
_has_spec_code = True
_has_spec_mode = True
_has_vox_level_off = False
POWER_LEVELS = [chirp_common.PowerLevel("Low", watts=0.50),
chirp_common.PowerLevel("High", watts=2.00)
]
VALID_BANDS = [(400000000, 502000000)]
PROGRAM_CMD = b"T210GAM"
_ranges = [
(0x0000, 0x00D8),
]
_memsize = 0x00D8
def process_mmap(self):
self._memobj = bitwise.parse(MEM_FORMAT_RT21H, self._mmap)
def set_settings(self, settings):
for element in settings:
if not isinstance(element, RadioSetting):
self.set_settings(element)
continue
obj = self._memobj.settings
setting = element.get_name()
try:
if setting == "vox_level":
# UI index 0-8 ? firmware value 1-9
value = min(9, max(1, int(element.value) + 1))
else:
value = element.value
LOG.debug("Setting %s = %s", setting, value)
setattr(obj, setting, value)
except Exception:
LOG.debug("Error applying setting %s", setting)
raise
chirp-20261030/chirp/drivers/retevis_ha1g.py 0000644 0001750 0001750 00000166236 15210511266 017565 0 ustar hibby hibby # Copyright 2024 Tommy
#
# 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