├── .gitignore ├── resources └── short.png ├── chirp ├── requirements-dev.txt ├── Makefile └── chirp.patch ├── .github └── workflows │ └── main.yml ├── README.md ├── LICENSE ├── uvk5_egzumer_115200_baud.py └── uvk5_egzumer.py /.gitignore: -------------------------------------------------------------------------------- 1 | __pycache__/ 2 | -------------------------------------------------------------------------------- /resources/short.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kamilsss655/uvk5-chirp-driver/HEAD/resources/short.png -------------------------------------------------------------------------------- /chirp/requirements-dev.txt: -------------------------------------------------------------------------------- 1 | future 2 | six 3 | wxPython 4 | pypiwin32 5 | pyserial 6 | requests 7 | yattag 8 | wheel 9 | importlib-resources -------------------------------------------------------------------------------- /chirp/Makefile: -------------------------------------------------------------------------------- 1 | POFILES = $(shell ls *.po) 2 | MOFILES = $(patsubst %.po,%/LC_MESSAGES/CHIRP.mo,$(POFILES)) 3 | SHELL = bash 4 | 5 | COPY="Dan Smith " 6 | PKG=CHIRP 7 | XGT_OPTS=--copyright-holder=$(COPY) --package-name=$(PKG) 8 | 9 | .PHONY: all 10 | 11 | all: chirpui.pot $(MOFILES) 12 | 13 | clean: 14 | rm -f $(MOFILES) *~ *.orig chirpui.pot 15 | find . -name '*.mo' -exec rm -f "{}" \; 16 | find * -depth -type d -exec rmdir "{}" \; 17 | 18 | chirpui.pot: 19 | find .. -name '*.py' > file_list.txt 20 | xgettext -s -L Python -k_ -o chirpui.pot --from-code=iso-8859-15 -f file_list.txt $(XGT_OPTS) 21 | rm file_list.txt 22 | 23 | %.po: chirpui.pot 24 | if [ -f $@ ]; then \ 25 | msgmerge -sU $@ chirpui.pot; \ 26 | else \ 27 | msginit --input=chirpui.pot --locale=$(@:%.po=%); \ 28 | fi 29 | 30 | %/LC_MESSAGES/CHIRP.mo: %.po 31 | mkdir -p $(shell dirname $@) 32 | msgfmt --output-file=$@ $^ 33 | -------------------------------------------------------------------------------- /chirp/chirp.patch: -------------------------------------------------------------------------------- 1 | diff --git a/chirp/wxui/main.py b/chirp/wxui/main.py 2 | index bc599144..a1381603 100644 3 | --- a/chirp/wxui/main.py 4 | +++ b/chirp/wxui/main.py 5 | @@ -506,7 +506,7 @@ class ChirpMain(wx.Frame): 6 | return 7 | 8 | def _detach(event): 9 | - new = ChirpMain(None, title='CHIRP') 10 | + new = ChirpMain(None, title='CHIRP egzumer') 11 | self._editors.RemovePage(selected) 12 | eset.Reparent(new._editors) 13 | new.add_editorset(eset) 14 | @@ -1094,9 +1094,9 @@ class ChirpMain(wx.Frame): 15 | is_memedit = isinstance(eset.current_editor, memedit.ChirpMemEdit) 16 | is_bank = isinstance(eset.current_editor, bankedit.ChirpBankEdit) 17 | can_edit = not is_network 18 | - self.SetTitle('CHIRP (%s)' % os.path.basename(eset.filename)) 19 | + self.SetTitle('CHIRP egzumer (%s)' % os.path.basename(eset.filename)) 20 | else: 21 | - self.SetTitle('CHIRP') 22 | + self.SetTitle('CHIRP egzumer') 23 | 24 | if self.current_editorset: 25 | radio = self.current_editorset.radio 26 | @@ -1640,7 +1640,7 @@ class ChirpMain(wx.Frame): 27 | 28 | def _menu_about(self, event): 29 | pyver = sys.version_info 30 | - aboutinfo = 'CHIRP %s on Python %s wxPython %s' % ( 31 | + aboutinfo = 'CHIRP egzumer %s\non Python %s\nwxPython %s' % ( 32 | CHIRP_VERSION, 33 | '%s.%s.%s' % (pyver.major, pyver.minor, pyver.micro), 34 | wx.version()) 35 | @@ -1768,8 +1768,8 @@ def display_update_notice(version): 36 | 37 | CONF.set_int("last_update_check", int(time.time()), "state") 38 | 39 | - url = 'https://chirp.danplanet.com/projects/chirp/wiki/ChirpNextBuild' 40 | - msg = _('A new CHIRP version is available. Please visit the ' 41 | + url = 'https://github.com/egzumer/uvk5-chirp-driver/releases' 42 | + msg = _('A new CHIRP egzumer version is available. Please visit the ' 43 | 'website as soon as possible to download it!') 44 | d = wx.MessageDialog(None, msg, _('New version available'), 45 | style=wx.OK | wx.CANCEL | wx.ICON_INFORMATION) 46 | diff --git a/chirp/wxui/report.py b/chirp/wxui/report.py 47 | index d2ca7711..d0e4dfc0 100644 48 | --- a/chirp/wxui/report.py 49 | +++ b/chirp/wxui/report.py 50 | @@ -102,8 +102,8 @@ def with_session(fn): 51 | 52 | @with_session 53 | def check_for_updates(session, callback): 54 | - r = session.get('%s/latest' % BASE) 55 | - callback(r.json()['latest']) 56 | + r = session.get("https://api.github.com/repos/egzumer/uvk5-chirp-driver/releases/latest") 57 | + callback(r.json()['tag_name']) 58 | 59 | 60 | @with_session 61 | -------------------------------------------------------------------------------- /.github/workflows/main.yml: -------------------------------------------------------------------------------- 1 | on: 2 | push: 3 | tags: 4 | - 'v*' 5 | env: 6 | CHIRP_DIR: ${{github.workspace}}\temp\chirp 7 | BASH: C:\tools\cygwin\bin\bash 8 | 9 | jobs: 10 | build: 11 | runs-on: windows-latest 12 | 13 | steps: 14 | 15 | - name: "Checkout repository" 16 | uses: actions/checkout@v3 17 | 18 | - name: "Install MSVC" 19 | uses: ilammy/msvc-dev-cmd@v1 20 | 21 | - name: "Install Python" 22 | uses: actions/setup-python@v5 23 | with: 24 | python-version: '3.9' 25 | cache: 'pip' 26 | cache-dependency-path: 'chirp\requirements-dev.txt' 27 | 28 | - name: "Instal Python packages" 29 | run: pip install -r chirp\requirements-dev.txt 30 | 31 | - name: "Download PyInstaller" 32 | uses: robinraju/release-downloader@v1.8 33 | with: 34 | repository: "pyinstaller/pyinstaller" 35 | latest: true 36 | zipBall: true 37 | out-file-path: "temp" 38 | extract: true 39 | 40 | - name: "Build PyInstaller" 41 | run: > 42 | cd temp\pyinstaller-pyinstaller-*\bootloader && 43 | python.exe ./waf all --target-arch=64bit && 44 | cd .. 45 | && pip install . 46 | 47 | - name: "Install Cygwin" 48 | uses: egor-tensin/setup-cygwin@v4 49 | with: 50 | packages: make gettext 51 | 52 | - name: "Prepare CHIRP repository" 53 | run: > 54 | New-Item -ItemType Directory -Force -Path temp && 55 | cd temp && 56 | git clone https://github.com/kk7ds/chirp.git && 57 | Copy-Item -Path "${{github.workspace}}\chirp\Makefile" -Destination "${{env.CHIRP_DIR}}\chirp\locale\" -Force && 58 | Copy-Item -Path "${{github.workspace}}\chirp\chirp.patch" -Destination "${{env.CHIRP_DIR}}\" -Force && 59 | Copy-Item -Path "${{github.workspace}}\uvk5_egzumer.py" -Destination "${{env.CHIRP_DIR}}\chirp\drivers\" -Force && 60 | cd chirp && 61 | git apply chirp.patch; 62 | if ("${{github.ref}}" -like "refs/tags/v*") { 63 | $content = Get-Content -Path "chirp\__init__.py" -Raw; 64 | $content = $content -replace 'CHIRP_VERSION = "py3dev"', "CHIRP_VERSION = `"${{github.ref_name}}`""; 65 | Set-Content -Path "chirp\__init__.py" -Value $content; 66 | } 67 | 68 | - name: "Build CHIRP locale" 69 | run: > 70 | ${{env.BASH}} --login -c "cd '${{env.CHIRP_DIR}}\chirp\locale'; make" && 71 | ${{env.BASH}} --login -c "cd '${{env.CHIRP_DIR}}'; find chirp/locale/ -maxdepth 1 -type f -delete" 72 | 73 | - name: "Build CHIRP" 74 | run: > 75 | cd "${{env.CHIRP_DIR}}" && 76 | pyinstaller 77 | --noconfirm 78 | --onefile 79 | --windowed 80 | --icon "chirp/share/chirp.ico" 81 | --name "CHIRP_egzumer" 82 | --hidden-import "wx" 83 | --hidden-import "wx._xml" 84 | --hidden-import "importlib-resources" 85 | --add-data "chirp/share;chirp/share/" 86 | --add-data "chirp/stock_configs;chirp/stock_configs/" 87 | --add-data "chirp/locale;chirp/locale/" 88 | --add-data "chirp/drivers;chirp/drivers/" 89 | "chirpwx.py"; 90 | 91 | - name: Upload onefile to release 92 | if: ${{ startsWith(github.ref, 'refs/tags/v') }} 93 | uses: svenstaro/upload-release-action@v2 94 | with: 95 | repo_token: ${{ secrets.GITHUB_TOKEN }} 96 | file: ${{env.CHIRP_DIR}}\dist\CHIRP_egzumer.exe 97 | asset_name: CHIRP_egzumer.exe 98 | tag: ${{ github.ref }} 99 | overwrite: true 100 | release_name: release ${{ github.ref_name }} 101 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Introduction 2 | 3 | CHIRP driver for UV-K5/K6/5R radios running [Egzumer firmware](https://github.com/egzumer/uv-k5-firmware-custom) 4 | 5 | This is a modification of a driver created by:
6 | (c) 2023 Jacek Lipkowski 7 | 8 | Licensed cc-by-sa-4.0 9 | 10 | # How to use 11 | 12 | At some point the driver will hopefully be added to official release of CHIRP. 13 | 14 | Currently you can use modified version of CHIRP from [releases](https://github.com/egzumer/uvk5-chirp-driver/releases). The modified CHIRP is built from [source code](https://github.com/egzumer/chirp/tree/egzumer) and contains the driver. It is packed using pyinstaller into one file for convinience, however some antiviruses don't like that and [report it as a threat](https://stackoverflow.com/questions/43777106/program-made-with-pyinstaller-now-seen-as-a-trojan-horse-by-avg). If you don't trust this file, then use the other method. 15 | 16 | You can also use development version of driver [uvk5_EGZUMER.py](uvk5_egzumer.py?raw=1) (right click, "Save link as...") and load it to CHIRP manually. 17 | 18 | > [!WARNING] 19 | > Set the `UV-K5 (egzumer)` radio model in the CHIRP before you upload/download a configuration. 20 | 21 | ## Loading with menu 22 | 1. go to menu `Help` and turn on `Developer mode` 23 | 1. Restart CHIRP 24 | 1. Go to menu `File`, `Load module...` 25 | 1. Choose downloaded `uvk5_EGZUMER.py`, new radio will appear in Quansheng section in download/upload function. 26 | 27 | ## Loading with CHIRP input argument 28 | 1. Create a shortcut to CHIRP program 29 | 1. Edit shortcut settings, in target field add at the end `--module PATH_TO_DRIVER` (replace `PATH_TO_DRIVER` with a real path) example : "C:\Program Files (x86)\CHIRP\chirpwx.exe" --module C:\chirp_egzumer\uvk5_EGZUMER.py![add to start shorcut](resources/short.png) 30 | 31 | 1. Run CHIRP with the shortcut, it will automatically load the driver. 32 | ![when chirp start](https://github.com/egzumer/uvk5-chirp-driver/assets/56229329/5fa94f0f-a540-4bc0-bd27-633a04e67b41) 33 | 34 | ## Custom channel settings 35 | 36 | By default CHIRP shows only default channel options, that are universal for all types radios. You can see and change custom channel settings by going to menu `View` and turning on `Show extra fields`, this will show more options in the `Memories` tab. 37 | 38 | # Custom firmware build options 39 | 40 | This driver supports custom Egzumer firmware builds and detects which [options](https://github.com/egzumer/uv-k5-firmware-custom?tab=readme-ov-file#user-customization) have been used. 41 | Disabled options will be hidden in CHIRP. This only works if the configuration was read from a specific radio. You can use configuration files from other radios with different build options, but unsupported settings will be reset to defaults on the target radio. 42 | 43 | # Calibration settings 44 | 45 | Those setting are not uploaded to the radio by default. If you make some changes in the calibration and you want to save it, you have to enable the `Upload calibration` option before uploading. If this is enabled only the calibration part of the configuration data is sent to the radio, without channels and other settings. 46 | 47 | Use this option at your own risk. Make a backup of the calibration first! Some settings are calibrated at the factory and each radio has different and unique calibration data. You will not be able to restore those setting using some other radios settings. Be carefull not to use CHIRP config file that was downloaded from other radio. Each CHIRP config contains full EEPROM dump, it always did, even the original UV-K5 driver did this, so if you have some old config saved it also contains calibration section and can be used to restore the calibration, but the best way to make a backup is to use software that doesn't depend on CHIRP driver, like [k5prog-win](https://github.com/OneOfEleven/k5prog-win/raw/main/k5prog_win.exe). 48 | 49 | Calibration settings are raw values read from the EEPROM, not recalculated to dBm, dB or any particular units. All the settings are presented as they were in the stock Quansheng firmware. Not all calibration settings are used the same way by the egzumer firmware: 50 | - Squelch - sensitivity is doubled if ENABLE_SQUELCH_MORE_SENSITIVE is enabled (enabled by default) 51 | - Microphone sensitivity - not used at all 52 | - RSSI levels - only used for small RSSI bar indicator if the firmware is built with the custom S-meter disabled (ENABLE_RSSI_BAR = 0) 53 | - TX power - if built with ENABLE_REDUCE_LOW_MID_TX_POWER then medium power is further divided by 3, low power is divided by 5 (not enabled by default) 54 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Attribution-ShareAlike 4.0 International 2 | 3 | ======================================================================= 4 | 5 | Creative Commons Corporation ("Creative Commons") is not a law firm and 6 | does not provide legal services or legal advice. Distribution of 7 | Creative Commons public licenses does not create a lawyer-client or 8 | other relationship. Creative Commons makes its licenses and related 9 | information available on an "as-is" basis. Creative Commons gives no 10 | warranties regarding its licenses, any material licensed under their 11 | terms and conditions, or any related information. Creative Commons 12 | disclaims all liability for damages resulting from their use to the 13 | fullest extent possible. 14 | 15 | Using Creative Commons Public Licenses 16 | 17 | Creative Commons public licenses provide a standard set of terms and 18 | conditions that creators and other rights holders may use to share 19 | original works of authorship and other material subject to copyright 20 | and certain other rights specified in the public license below. The 21 | following considerations are for informational purposes only, are not 22 | exhaustive, and do not form part of our licenses. 23 | 24 | Considerations for licensors: Our public licenses are 25 | intended for use by those authorized to give the public 26 | permission to use material in ways otherwise restricted by 27 | copyright and certain other rights. Our licenses are 28 | irrevocable. Licensors should read and understand the terms 29 | and conditions of the license they choose before applying it. 30 | Licensors should also secure all rights necessary before 31 | applying our licenses so that the public can reuse the 32 | material as expected. Licensors should clearly mark any 33 | material not subject to the license. This includes other CC- 34 | licensed material, or material used under an exception or 35 | limitation to copyright. More considerations for licensors: 36 | wiki.creativecommons.org/Considerations_for_licensors 37 | 38 | Considerations for the public: By using one of our public 39 | licenses, a licensor grants the public permission to use the 40 | licensed material under specified terms and conditions. If 41 | the licensor's permission is not necessary for any reason--for 42 | example, because of any applicable exception or limitation to 43 | copyright--then that use is not regulated by the license. Our 44 | licenses grant only permissions under copyright and certain 45 | other rights that a licensor has authority to grant. Use of 46 | the licensed material may still be restricted for other 47 | reasons, including because others have copyright or other 48 | rights in the material. A licensor may make special requests, 49 | such as asking that all changes be marked or described. 50 | Although not required by our licenses, you are encouraged to 51 | respect those requests where reasonable. More considerations 52 | for the public: 53 | wiki.creativecommons.org/Considerations_for_licensees 54 | 55 | ======================================================================= 56 | 57 | Creative Commons Attribution-ShareAlike 4.0 International Public 58 | License 59 | 60 | By exercising the Licensed Rights (defined below), You accept and agree 61 | to be bound by the terms and conditions of this Creative Commons 62 | Attribution-ShareAlike 4.0 International Public License ("Public 63 | License"). To the extent this Public License may be interpreted as a 64 | contract, You are granted the Licensed Rights in consideration of Your 65 | acceptance of these terms and conditions, and the Licensor grants You 66 | such rights in consideration of benefits the Licensor receives from 67 | making the Licensed Material available under these terms and 68 | conditions. 69 | 70 | 71 | Section 1 -- Definitions. 72 | 73 | a. Adapted Material means material subject to Copyright and Similar 74 | Rights that is derived from or based upon the Licensed Material 75 | and in which the Licensed Material is translated, altered, 76 | arranged, transformed, or otherwise modified in a manner requiring 77 | permission under the Copyright and Similar Rights held by the 78 | Licensor. For purposes of this Public License, where the Licensed 79 | Material is a musical work, performance, or sound recording, 80 | Adapted Material is always produced where the Licensed Material is 81 | synched in timed relation with a moving image. 82 | 83 | b. Adapter's License means the license You apply to Your Copyright 84 | and Similar Rights in Your contributions to Adapted Material in 85 | accordance with the terms and conditions of this Public License. 86 | 87 | c. BY-SA Compatible License means a license listed at 88 | creativecommons.org/compatiblelicenses, approved by Creative 89 | Commons as essentially the equivalent of this Public License. 90 | 91 | d. Copyright and Similar Rights means copyright and/or similar rights 92 | closely related to copyright including, without limitation, 93 | performance, broadcast, sound recording, and Sui Generis Database 94 | Rights, without regard to how the rights are labeled or 95 | categorized. For purposes of this Public License, the rights 96 | specified in Section 2(b)(1)-(2) are not Copyright and Similar 97 | Rights. 98 | 99 | e. Effective Technological Measures means those measures that, in the 100 | absence of proper authority, may not be circumvented under laws 101 | fulfilling obligations under Article 11 of the WIPO Copyright 102 | Treaty adopted on December 20, 1996, and/or similar international 103 | agreements. 104 | 105 | f. Exceptions and Limitations means fair use, fair dealing, and/or 106 | any other exception or limitation to Copyright and Similar Rights 107 | that applies to Your use of the Licensed Material. 108 | 109 | g. License Elements means the license attributes listed in the name 110 | of a Creative Commons Public License. The License Elements of this 111 | Public License are Attribution and ShareAlike. 112 | 113 | h. Licensed Material means the artistic or literary work, database, 114 | or other material to which the Licensor applied this Public 115 | License. 116 | 117 | i. Licensed Rights means the rights granted to You subject to the 118 | terms and conditions of this Public License, which are limited to 119 | all Copyright and Similar Rights that apply to Your use of the 120 | Licensed Material and that the Licensor has authority to license. 121 | 122 | j. Licensor means the individual(s) or entity(ies) granting rights 123 | under this Public License. 124 | 125 | k. Share means to provide material to the public by any means or 126 | process that requires permission under the Licensed Rights, such 127 | as reproduction, public display, public performance, distribution, 128 | dissemination, communication, or importation, and to make material 129 | available to the public including in ways that members of the 130 | public may access the material from a place and at a time 131 | individually chosen by them. 132 | 133 | l. Sui Generis Database Rights means rights other than copyright 134 | resulting from Directive 96/9/EC of the European Parliament and of 135 | the Council of 11 March 1996 on the legal protection of databases, 136 | as amended and/or succeeded, as well as other essentially 137 | equivalent rights anywhere in the world. 138 | 139 | m. You means the individual or entity exercising the Licensed Rights 140 | under this Public License. Your has a corresponding meaning. 141 | 142 | 143 | Section 2 -- Scope. 144 | 145 | a. License grant. 146 | 147 | 1. Subject to the terms and conditions of this Public License, 148 | the Licensor hereby grants You a worldwide, royalty-free, 149 | non-sublicensable, non-exclusive, irrevocable license to 150 | exercise the Licensed Rights in the Licensed Material to: 151 | 152 | a. reproduce and Share the Licensed Material, in whole or 153 | in part; and 154 | 155 | b. produce, reproduce, and Share Adapted Material. 156 | 157 | 2. Exceptions and Limitations. For the avoidance of doubt, where 158 | Exceptions and Limitations apply to Your use, this Public 159 | License does not apply, and You do not need to comply with 160 | its terms and conditions. 161 | 162 | 3. Term. The term of this Public License is specified in Section 163 | 6(a). 164 | 165 | 4. Media and formats; technical modifications allowed. The 166 | Licensor authorizes You to exercise the Licensed Rights in 167 | all media and formats whether now known or hereafter created, 168 | and to make technical modifications necessary to do so. The 169 | Licensor waives and/or agrees not to assert any right or 170 | authority to forbid You from making technical modifications 171 | necessary to exercise the Licensed Rights, including 172 | technical modifications necessary to circumvent Effective 173 | Technological Measures. For purposes of this Public License, 174 | simply making modifications authorized by this Section 2(a) 175 | (4) never produces Adapted Material. 176 | 177 | 5. Downstream recipients. 178 | 179 | a. Offer from the Licensor -- Licensed Material. Every 180 | recipient of the Licensed Material automatically 181 | receives an offer from the Licensor to exercise the 182 | Licensed Rights under the terms and conditions of this 183 | Public License. 184 | 185 | b. Additional offer from the Licensor -- Adapted Material. 186 | Every recipient of Adapted Material from You 187 | automatically receives an offer from the Licensor to 188 | exercise the Licensed Rights in the Adapted Material 189 | under the conditions of the Adapter's License You apply. 190 | 191 | c. No downstream restrictions. You may not offer or impose 192 | any additional or different terms or conditions on, or 193 | apply any Effective Technological Measures to, the 194 | Licensed Material if doing so restricts exercise of the 195 | Licensed Rights by any recipient of the Licensed 196 | Material. 197 | 198 | 6. No endorsement. Nothing in this Public License constitutes or 199 | may be construed as permission to assert or imply that You 200 | are, or that Your use of the Licensed Material is, connected 201 | with, or sponsored, endorsed, or granted official status by, 202 | the Licensor or others designated to receive attribution as 203 | provided in Section 3(a)(1)(A)(i). 204 | 205 | b. Other rights. 206 | 207 | 1. Moral rights, such as the right of integrity, are not 208 | licensed under this Public License, nor are publicity, 209 | privacy, and/or other similar personality rights; however, to 210 | the extent possible, the Licensor waives and/or agrees not to 211 | assert any such rights held by the Licensor to the limited 212 | extent necessary to allow You to exercise the Licensed 213 | Rights, but not otherwise. 214 | 215 | 2. Patent and trademark rights are not licensed under this 216 | Public License. 217 | 218 | 3. To the extent possible, the Licensor waives any right to 219 | collect royalties from You for the exercise of the Licensed 220 | Rights, whether directly or through a collecting society 221 | under any voluntary or waivable statutory or compulsory 222 | licensing scheme. In all other cases the Licensor expressly 223 | reserves any right to collect such royalties. 224 | 225 | 226 | Section 3 -- License Conditions. 227 | 228 | Your exercise of the Licensed Rights is expressly made subject to the 229 | following conditions. 230 | 231 | a. Attribution. 232 | 233 | 1. If You Share the Licensed Material (including in modified 234 | form), You must: 235 | 236 | a. retain the following if it is supplied by the Licensor 237 | with the Licensed Material: 238 | 239 | i. identification of the creator(s) of the Licensed 240 | Material and any others designated to receive 241 | attribution, in any reasonable manner requested by 242 | the Licensor (including by pseudonym if 243 | designated); 244 | 245 | ii. a copyright notice; 246 | 247 | iii. a notice that refers to this Public License; 248 | 249 | iv. a notice that refers to the disclaimer of 250 | warranties; 251 | 252 | v. a URI or hyperlink to the Licensed Material to the 253 | extent reasonably practicable; 254 | 255 | b. indicate if You modified the Licensed Material and 256 | retain an indication of any previous modifications; and 257 | 258 | c. indicate the Licensed Material is licensed under this 259 | Public License, and include the text of, or the URI or 260 | hyperlink to, this Public License. 261 | 262 | 2. You may satisfy the conditions in Section 3(a)(1) in any 263 | reasonable manner based on the medium, means, and context in 264 | which You Share the Licensed Material. For example, it may be 265 | reasonable to satisfy the conditions by providing a URI or 266 | hyperlink to a resource that includes the required 267 | information. 268 | 269 | 3. If requested by the Licensor, You must remove any of the 270 | information required by Section 3(a)(1)(A) to the extent 271 | reasonably practicable. 272 | 273 | b. ShareAlike. 274 | 275 | In addition to the conditions in Section 3(a), if You Share 276 | Adapted Material You produce, the following conditions also apply. 277 | 278 | 1. The Adapter's License You apply must be a Creative Commons 279 | license with the same License Elements, this version or 280 | later, or a BY-SA Compatible License. 281 | 282 | 2. You must include the text of, or the URI or hyperlink to, the 283 | Adapter's License You apply. You may satisfy this condition 284 | in any reasonable manner based on the medium, means, and 285 | context in which You Share Adapted Material. 286 | 287 | 3. You may not offer or impose any additional or different terms 288 | or conditions on, or apply any Effective Technological 289 | Measures to, Adapted Material that restrict exercise of the 290 | rights granted under the Adapter's License You apply. 291 | 292 | 293 | Section 4 -- Sui Generis Database Rights. 294 | 295 | Where the Licensed Rights include Sui Generis Database Rights that 296 | apply to Your use of the Licensed Material: 297 | 298 | a. for the avoidance of doubt, Section 2(a)(1) grants You the right 299 | to extract, reuse, reproduce, and Share all or a substantial 300 | portion of the contents of the database; 301 | 302 | b. if You include all or a substantial portion of the database 303 | contents in a database in which You have Sui Generis Database 304 | Rights, then the database in which You have Sui Generis Database 305 | Rights (but not its individual contents) is Adapted Material, 306 | including for purposes of Section 3(b); and 307 | 308 | c. You must comply with the conditions in Section 3(a) if You Share 309 | all or a substantial portion of the contents of the database. 310 | 311 | For the avoidance of doubt, this Section 4 supplements and does not 312 | replace Your obligations under this Public License where the Licensed 313 | Rights include other Copyright and Similar Rights. 314 | 315 | 316 | Section 5 -- Disclaimer of Warranties and Limitation of Liability. 317 | 318 | a. UNLESS OTHERWISE SEPARATELY UNDERTAKEN BY THE LICENSOR, TO THE 319 | EXTENT POSSIBLE, THE LICENSOR OFFERS THE LICENSED MATERIAL AS-IS 320 | AND AS-AVAILABLE, AND MAKES NO REPRESENTATIONS OR WARRANTIES OF 321 | ANY KIND CONCERNING THE LICENSED MATERIAL, WHETHER EXPRESS, 322 | IMPLIED, STATUTORY, OR OTHER. THIS INCLUDES, WITHOUT LIMITATION, 323 | WARRANTIES OF TITLE, MERCHANTABILITY, FITNESS FOR A PARTICULAR 324 | PURPOSE, NON-INFRINGEMENT, ABSENCE OF LATENT OR OTHER DEFECTS, 325 | ACCURACY, OR THE PRESENCE OR ABSENCE OF ERRORS, WHETHER OR NOT 326 | KNOWN OR DISCOVERABLE. WHERE DISCLAIMERS OF WARRANTIES ARE NOT 327 | ALLOWED IN FULL OR IN PART, THIS DISCLAIMER MAY NOT APPLY TO YOU. 328 | 329 | b. TO THE EXTENT POSSIBLE, IN NO EVENT WILL THE LICENSOR BE LIABLE 330 | TO YOU ON ANY LEGAL THEORY (INCLUDING, WITHOUT LIMITATION, 331 | NEGLIGENCE) OR OTHERWISE FOR ANY DIRECT, SPECIAL, INDIRECT, 332 | INCIDENTAL, CONSEQUENTIAL, PUNITIVE, EXEMPLARY, OR OTHER LOSSES, 333 | COSTS, EXPENSES, OR DAMAGES ARISING OUT OF THIS PUBLIC LICENSE OR 334 | USE OF THE LICENSED MATERIAL, EVEN IF THE LICENSOR HAS BEEN 335 | ADVISED OF THE POSSIBILITY OF SUCH LOSSES, COSTS, EXPENSES, OR 336 | DAMAGES. WHERE A LIMITATION OF LIABILITY IS NOT ALLOWED IN FULL OR 337 | IN PART, THIS LIMITATION MAY NOT APPLY TO YOU. 338 | 339 | c. The disclaimer of warranties and limitation of liability provided 340 | above shall be interpreted in a manner that, to the extent 341 | possible, most closely approximates an absolute disclaimer and 342 | waiver of all liability. 343 | 344 | 345 | Section 6 -- Term and Termination. 346 | 347 | a. This Public License applies for the term of the Copyright and 348 | Similar Rights licensed here. However, if You fail to comply with 349 | this Public License, then Your rights under this Public License 350 | terminate automatically. 351 | 352 | b. Where Your right to use the Licensed Material has terminated under 353 | Section 6(a), it reinstates: 354 | 355 | 1. automatically as of the date the violation is cured, provided 356 | it is cured within 30 days of Your discovery of the 357 | violation; or 358 | 359 | 2. upon express reinstatement by the Licensor. 360 | 361 | For the avoidance of doubt, this Section 6(b) does not affect any 362 | right the Licensor may have to seek remedies for Your violations 363 | of this Public License. 364 | 365 | c. For the avoidance of doubt, the Licensor may also offer the 366 | Licensed Material under separate terms or conditions or stop 367 | distributing the Licensed Material at any time; however, doing so 368 | will not terminate this Public License. 369 | 370 | d. Sections 1, 5, 6, 7, and 8 survive termination of this Public 371 | License. 372 | 373 | 374 | Section 7 -- Other Terms and Conditions. 375 | 376 | a. The Licensor shall not be bound by any additional or different 377 | terms or conditions communicated by You unless expressly agreed. 378 | 379 | b. Any arrangements, understandings, or agreements regarding the 380 | Licensed Material not stated herein are separate from and 381 | independent of the terms and conditions of this Public License. 382 | 383 | 384 | Section 8 -- Interpretation. 385 | 386 | a. For the avoidance of doubt, this Public License does not, and 387 | shall not be interpreted to, reduce, limit, restrict, or impose 388 | conditions on any use of the Licensed Material that could lawfully 389 | be made without permission under this Public License. 390 | 391 | b. To the extent possible, if any provision of this Public License is 392 | deemed unenforceable, it shall be automatically reformed to the 393 | minimum extent necessary to make it enforceable. If the provision 394 | cannot be reformed, it shall be severed from this Public License 395 | without affecting the enforceability of the remaining terms and 396 | conditions. 397 | 398 | c. No term or condition of this Public License will be waived and no 399 | failure to comply consented to unless expressly agreed to by the 400 | Licensor. 401 | 402 | d. Nothing in this Public License constitutes or may be interpreted 403 | as a limitation upon, or waiver of, any privileges and immunities 404 | that apply to the Licensor or You, including from the legal 405 | processes of any jurisdiction or authority. 406 | 407 | 408 | ======================================================================= 409 | 410 | Creative Commons is not a party to its public licenses. 411 | Notwithstanding, Creative Commons may elect to apply one of its public 412 | licenses to material it publishes and in those instances will be 413 | considered the "Licensor." The text of the Creative Commons public 414 | licenses is dedicated to the public domain under the CC0 Public Domain 415 | Dedication. Except for the limited purpose of indicating that material 416 | is shared under a Creative Commons public license or as otherwise 417 | permitted by the Creative Commons policies published at 418 | creativecommons.org/policies, Creative Commons does not authorize the 419 | use of the trademark "Creative Commons" or any other trademark or logo 420 | of Creative Commons without its prior written consent including, 421 | without limitation, in connection with any unauthorized modifications 422 | to any of its public licenses or any other arrangements, 423 | understandings, or agreements concerning use of licensed material. For 424 | the avoidance of doubt, this paragraph does not form part of the public 425 | licenses. 426 | 427 | Creative Commons may be contacted at creativecommons.org. 428 | 429 | -------------------------------------------------------------------------------- /uvk5_egzumer_115200_baud.py: -------------------------------------------------------------------------------- 1 | # Quansheng UV-K5 driver (c) 2023 Jacek Lipkowski 2 | # Adapted For UV-K5 EGZUMER custom software By EGZUMER, JOC2 3 | # 4 | # based on template.py Copyright 2012 Dan Smith 5 | # 6 | # 7 | # This is a preliminary version of a driver for the UV-K5 8 | # It is based on my reverse engineering effort described here: 9 | # https://github.com/sq5bpf/uvk5-reverse-engineering 10 | # 11 | # Warning: this driver is experimental, it may brick your radio, 12 | # eat your lunch and mess up your configuration. 13 | # 14 | # 15 | # This program is free software: you can redistribute it and/or modify 16 | # it under the terms of the GNU General Public License as published by 17 | # the Free Software Foundation, either version 2 of the License, or 18 | # (at your option) any later version. 19 | # 20 | # This program is distributed in the hope that it will be useful, 21 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 22 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 23 | # GNU General Public License for more details. 24 | # 25 | # You should have received a copy of the GNU General Public License 26 | # along with this program. If not, see . 27 | 28 | 29 | import struct 30 | import logging 31 | import wx 32 | 33 | from chirp import chirp_common, directory, bitwise, memmap, errors, util 34 | from chirp.settings import RadioSetting, RadioSettingGroup, \ 35 | RadioSettingValueBoolean, RadioSettingValueList, \ 36 | RadioSettingValueInteger, RadioSettingValueString, \ 37 | RadioSettings, InvalidValueError 38 | 39 | LOG = logging.getLogger(__name__) 40 | 41 | # Show the obfuscated version of commands. Not needed normally, but 42 | # might be useful for someone who is debugging a similar radio 43 | DEBUG_SHOW_OBFUSCATED_COMMANDS = False 44 | 45 | # Show the memory being written/received. Not needed normally, because 46 | # this is the same information as in the packet hexdumps, but 47 | # might be useful for someone debugging some obscure memory issue 48 | DEBUG_SHOW_MEMORY_ACTIONS = False 49 | 50 | # TODO: remove the driver version when it's in mainline chirp 51 | DRIVER_VERSION = "Quansheng UV-K5/K6/5R driver (c) egzumer" 52 | VALEUR_COMPILER = "ENABLE" 53 | 54 | MEM_FORMAT = """ 55 | //#seekto 0x0000; 56 | struct { 57 | ul32 freq; 58 | ul32 offset; 59 | 60 | // 0x08 61 | u8 rxcode; 62 | u8 txcode; 63 | 64 | // 0x0A 65 | u8 txcodeflag:4, 66 | rxcodeflag:4; 67 | 68 | // 0x0B 69 | u8 modulation:4, 70 | offsetDir:4; 71 | 72 | // 0x0C 73 | u8 __UNUSED1:3, 74 | busyChLockout:1, 75 | txpower:2, 76 | bandwidth:1, 77 | freq_reverse:1; 78 | 79 | // 0x0D 80 | u8 __UNUSED2:4, 81 | dtmf_pttid:3, 82 | dtmf_decode:1; 83 | 84 | // 0x0E 85 | u8 step; 86 | u8 scrambler; 87 | 88 | } channel[214]; 89 | 90 | //#seekto 0xd60; 91 | struct { 92 | u8 is_scanlist1:1, 93 | is_scanlist2:1, 94 | compander:2, 95 | is_free:1, 96 | band:3; 97 | } ch_attr[207]; 98 | 99 | #seekto 0xe40; 100 | ul16 fmfreq[20]; 101 | 102 | #seekto 0xe70; 103 | u8 call_channel; 104 | u8 squelch; 105 | u8 max_talk_time; 106 | u8 noaa_autoscan; 107 | u8 key_lock; 108 | u8 vox_switch; 109 | u8 vox_level; 110 | u8 mic_gain; 111 | 112 | 113 | u8 backlight_min:4, 114 | backlight_max:4; 115 | 116 | u8 channel_display_mode; 117 | u8 crossband; 118 | u8 battery_save; 119 | u8 dual_watch; 120 | u8 backlight_time; 121 | u8 ste; 122 | u8 freq_mode_allowed; 123 | 124 | #seekto 0xe80; 125 | u8 ScreenChannel_A; 126 | u8 MrChannel_A; 127 | u8 FreqChannel_A; 128 | u8 ScreenChannel_B; 129 | u8 MrChannel_B; 130 | u8 FreqChannel_B; 131 | u8 NoaaChannel_A; 132 | u8 NoaaChannel_B; 133 | 134 | #seekto 0xe90; 135 | 136 | u8 keyM_longpress_action:7, 137 | button_beep:1; 138 | 139 | u8 key1_shortpress_action; 140 | u8 key1_longpress_action; 141 | u8 key2_shortpress_action; 142 | u8 key2_longpress_action; 143 | u8 scan_resume_mode; 144 | u8 auto_keypad_lock; 145 | u8 power_on_dispmode; 146 | ul32 password; 147 | 148 | #seekto 0xea0; 149 | u8 voice; 150 | u8 s0_level; 151 | u8 s9_level; 152 | 153 | #seekto 0xea8; 154 | u8 alarm_mode; 155 | u8 roger_beep; 156 | u8 rp_ste; 157 | u8 TX_VFO; 158 | u8 Battery_type; 159 | 160 | #seekto 0xeb0; 161 | char logo_line1[16]; 162 | char logo_line2[16]; 163 | 164 | //#seekto 0xed0; 165 | struct { 166 | u8 side_tone; 167 | char separate_code; 168 | char group_call_code; 169 | u8 decode_response; 170 | u8 auto_reset_time; 171 | u8 preload_time; 172 | u8 first_code_persist_time; 173 | u8 hash_persist_time; 174 | u8 code_persist_time; 175 | u8 code_interval_time; 176 | u8 permit_remote_kill; 177 | 178 | #seekto 0xee0; 179 | char local_code[3]; 180 | #seek 5; 181 | char kill_code[5]; 182 | #seek 3; 183 | char revive_code[5]; 184 | #seek 3; 185 | char up_code[16]; 186 | char down_code[16]; 187 | } dtmf; 188 | 189 | //#seekto 0xf18; 190 | u8 slDef; 191 | u8 sl1PriorEnab; 192 | u8 sl1PriorCh1; 193 | u8 sl1PriorCh2; 194 | u8 sl2PriorEnab; 195 | u8 sl2PriorCh1; 196 | u8 sl2PriorCh2; 197 | 198 | #seekto 0xf40; 199 | u8 int_flock; 200 | u8 int_350tx; 201 | u8 int_KILLED; 202 | u8 int_200tx; 203 | u8 int_500tx; 204 | u8 int_350en; 205 | u8 int_scren; 206 | 207 | 208 | u8 backlight_on_TX_RX:2, 209 | AM_fix:1, 210 | mic_bar:1, 211 | battery_text:2, 212 | live_DTMF_decoder:1, 213 | unknown:1; 214 | 215 | 216 | #seekto 0xf50; 217 | struct { 218 | char name[16]; 219 | } channelname[200]; 220 | 221 | #seekto 0x1c00; 222 | struct { 223 | char name[8]; 224 | char number[3]; 225 | #seek 5; 226 | } dtmfcontact[16]; 227 | 228 | struct { 229 | struct { 230 | #seekto 0x1E00; 231 | u8 openRssiThr[10]; 232 | #seekto 0x1E10; 233 | u8 closeRssiThr[10]; 234 | #seekto 0x1E20; 235 | u8 openNoiseThr[10]; 236 | #seekto 0x1E30; 237 | u8 closeNoiseThr[10]; 238 | #seekto 0x1E40; 239 | u8 closeGlitchThr[10]; 240 | #seekto 0x1E50; 241 | u8 openGlitchThr[10]; 242 | } sqlBand4_7; 243 | 244 | struct { 245 | #seekto 0x1E60; 246 | u8 openRssiThr[10]; 247 | #seekto 0x1E70; 248 | u8 closeRssiThr[10]; 249 | #seekto 0x1E80; 250 | u8 openNoiseThr[10]; 251 | #seekto 0x1E90; 252 | u8 closeNoiseThr[10]; 253 | #seekto 0x1EA0; 254 | u8 closeGlitchThr[10]; 255 | #seekto 0x1EB0; 256 | u8 openGlitchThr[10]; 257 | } sqlBand1_3; 258 | 259 | #seekto 0x1EC0; 260 | struct { 261 | ul16 level1; 262 | ul16 level2; 263 | ul16 level4; 264 | ul16 level6; 265 | } rssiLevelsBands3_7; 266 | 267 | struct { 268 | ul16 level1; 269 | ul16 level2; 270 | ul16 level4; 271 | ul16 level6; 272 | } rssiLevelsBands1_2; 273 | 274 | struct { 275 | struct { 276 | u8 lower; 277 | u8 center; 278 | u8 upper; 279 | } low; 280 | struct { 281 | u8 lower; 282 | u8 center; 283 | u8 upper; 284 | } mid; 285 | struct { 286 | u8 lower; 287 | u8 center; 288 | u8 upper; 289 | } hi; 290 | #seek 7; 291 | } txp[7]; 292 | 293 | #seekto 0x1F40; 294 | ul16 batLvl[6]; 295 | 296 | #seekto 0x1F50; 297 | ul16 vox1Thr[10]; 298 | 299 | #seekto 0x1F68; 300 | ul16 vox0Thr[10]; 301 | 302 | #seekto 0x1F80; 303 | u8 micLevel[5]; 304 | 305 | #seekto 0x1F88; 306 | il16 xtalFreqLow; 307 | 308 | #seekto 0x1F8E; 309 | u8 volumeGain; 310 | u8 dacGain; 311 | } cal; 312 | 313 | 314 | #seekto 0x1FF0; 315 | struct { 316 | u8 ENABLE_DTMF_CALLING:1, 317 | ENABLE_PWRON_PASSWORD:1, 318 | ENABLE_TX1750:1, 319 | ENABLE_ALARM:1, 320 | ENABLE_VOX:1, 321 | ENABLE_VOICE:1, 322 | ENABLE_NOAA:1, 323 | ENABLE_FMRADIO:1; 324 | u8 __UNUSED:3, 325 | ENABLE_AM_FIX:1, 326 | ENABLE_BLMIN_TMP_OFF:1, 327 | ENABLE_RAW_DEMODULATORS:1, 328 | ENABLE_WIDE_RX:1, 329 | ENABLE_FLASHLIGHT:1; 330 | } BUILD_OPTIONS; 331 | 332 | """ 333 | 334 | 335 | # flags1 336 | FLAGS1_OFFSET_NONE = 0b00 337 | FLAGS1_OFFSET_MINUS = 0b10 338 | FLAGS1_OFFSET_PLUS = 0b01 339 | 340 | POWER_HIGH = 0b10 341 | POWER_MEDIUM = 0b01 342 | POWER_LOW = 0b00 343 | 344 | # dtmf_flags 345 | PTTID_LIST = ["Off", "Up code", "Down code", "Up+Down code", "Apollo Quindar"] 346 | 347 | # power 348 | UVK5_POWER_LEVELS = [chirp_common.PowerLevel("Low", watts=1.50), 349 | chirp_common.PowerLevel("Med", watts=3.00), 350 | chirp_common.PowerLevel("High", watts=5.00), 351 | ] 352 | 353 | # scrambler 354 | SCRAMBLER_LIST = ["Off", "2600Hz", "2700Hz", "2800Hz", "2900Hz", "3000Hz", 355 | "3100Hz", "3200Hz", "3300Hz", "3400Hz", "3500Hz"] 356 | # compander 357 | COMPANDER_LIST = ["Off", "TX", "RX", "TX/RX"] 358 | # rx mode 359 | RXMODE_LIST = ["Main only", "Dual RX, respond", "Crossband", 360 | "Dual RX, TX on main"] 361 | # channel display mode 362 | CHANNELDISP_LIST = ["Frequency", "Channel Number", "Name", "Name + Frequency"] 363 | 364 | # TalkTime 365 | TALK_TIME_LIST = ["30 sec", "1 min", "2 min", "3 min", "4 min", "5 min", 366 | "6 min", "7 min", "8 min", "9 min", "15 min"] 367 | 368 | # battery save 369 | BATSAVE_LIST = ["Off", "1:1", "1:2", "1:3", "1:4"] 370 | 371 | # battery type 372 | BATTYPE_LIST = ["1600 mAh", "2200 mAh"] 373 | # bat txt 374 | BAT_TXT_LIST = ["None", "Voltage", "Percentage"] 375 | # Backlight auto mode 376 | BACKLIGHT_LIST = ["Off", "5s", "10s", "20s", "1min", "2min", "4min", 377 | "Always On"] 378 | 379 | # Backlight LVL 380 | BACKLIGHT_LVL_LIST = ["0", "1", "2", "3", "4", "5", "6", "7", "8", "9", "10"] 381 | 382 | # Backlight _TX_RX_LIST 383 | BACKLIGHT_TX_RX_LIST = ["Off", "TX", "RX", "TX/RX"] 384 | 385 | # steps TODO: change order 386 | STEPS = [2.5, 5, 6.25, 10, 12.5, 25, 8.33, 0.01, 0.05, 0.1, 0.25, 0.5, 1, 1.25, 387 | 9, 15, 20, 30, 50, 100, 125, 200, 250, 500] 388 | 389 | # ctcss/dcs codes 390 | TMODES = ["", "Tone", "DTCS", "DTCS"] 391 | TONE_NONE = 0 392 | TONE_CTCSS = 1 393 | TONE_DCS = 2 394 | TONE_RDCS = 3 395 | 396 | 397 | CTCSS_TONES = [ 398 | 67.0, 69.3, 71.9, 74.4, 77.0, 79.7, 82.5, 85.4, 399 | 88.5, 91.5, 94.8, 97.4, 100.0, 103.5, 107.2, 110.9, 400 | 114.8, 118.8, 123.0, 127.3, 131.8, 136.5, 141.3, 146.2, 401 | 151.4, 156.7, 159.8, 162.2, 165.5, 167.9, 171.3, 173.8, 402 | 177.3, 179.9, 183.5, 186.2, 189.9, 192.8, 196.6, 199.5, 403 | 203.5, 206.5, 210.7, 218.1, 225.7, 229.1, 233.6, 241.8, 404 | 250.3, 254.1 405 | ] 406 | 407 | # lifted from ft4.py 408 | DTCS_CODES = [ # TODO: add negative codes 409 | 23, 25, 26, 31, 32, 36, 43, 47, 51, 53, 54, 410 | 65, 71, 72, 73, 74, 114, 115, 116, 122, 125, 131, 411 | 132, 134, 143, 145, 152, 155, 156, 162, 165, 172, 174, 412 | 205, 212, 223, 225, 226, 243, 244, 245, 246, 251, 252, 413 | 255, 261, 263, 265, 266, 271, 274, 306, 311, 315, 325, 414 | 331, 332, 343, 346, 351, 356, 364, 365, 371, 411, 412, 415 | 413, 423, 431, 432, 445, 446, 452, 454, 455, 462, 464, 416 | 465, 466, 503, 506, 516, 523, 526, 532, 546, 565, 606, 417 | 612, 624, 627, 631, 632, 654, 662, 664, 703, 712, 723, 418 | 731, 732, 734, 743, 754 419 | ] 420 | 421 | # flock list extended 422 | FLOCK_LIST = ["Default+ (137-174, 400-470 + Tx200, Tx350, Tx500)", 423 | "FCC HAM (144-148, 420-450)", 424 | "CE HAM (144-146, 430-440)", 425 | "GB HAM (144-148, 430-440)", 426 | "137-174, 400-430", 427 | "137-174, 400-438", 428 | "Disable All", 429 | "Unlock All"] 430 | 431 | SCANRESUME_LIST = ["Listen 5 seconds and resume", 432 | "Listen until carrier dissapears", 433 | "Stop scanning after receiving a signal"] 434 | WELCOME_LIST = ["Full screen test", "User message", "Battery voltage", "None"] 435 | VOICE_LIST = ["Off", "Chinese", "English"] 436 | 437 | # ACTIVE CHANNEL 438 | TX_VFO_LIST = ["A", "B"] 439 | ALARMMODE_LIST = ["Site", "Tone"] 440 | ROGER_LIST = ["Off", "Roger beep", "MDC data burst"] 441 | RTE_LIST = ["Off", "100ms", "200ms", "300ms", "400ms", 442 | "500ms", "600ms", "700ms", "800ms", "900ms", "1000ms"] 443 | VOX_LIST = ["Off", "1", "2", "3", "4", "5", "6", "7", "8", "9", "10"] 444 | 445 | MEM_SIZE = 0x2000 # size of all memory 446 | PROG_SIZE = 0x1d00 # size of the memory that we will write 447 | MEM_BLOCK = 0x80 # largest block of memory that we can reliably write 448 | CAL_START = 0x1E00 # calibration memory start address 449 | 450 | # fm radio supported frequencies 451 | FMMIN = 76.0 452 | FMMAX = 108.0 453 | 454 | # bands supported by the UV-K5 455 | BANDS_STANDARD = { 456 | 0: [50.0, 76.0], 457 | 1: [108.0, 136.9999], 458 | 2: [137.0, 173.9999], 459 | 3: [174.0, 349.9999], 460 | 4: [350.0, 399.9999], 461 | 5: [400.0, 469.9999], 462 | 6: [470.0, 600.0] 463 | } 464 | 465 | BANDS_WIDE = { 466 | 0: [18.0, 108.0], 467 | 1: [108.0, 136.9999], 468 | 2: [137.0, 173.9999], 469 | 3: [174.0, 349.9999], 470 | 4: [350.0, 399.9999], 471 | 5: [400.0, 469.9999], 472 | 6: [470.0, 1300.0] 473 | } 474 | 475 | SCANLIST_LIST = ["None", "List1", "List2", "Both"] 476 | SCANLIST_SELECT_LIST = ["List 1", "List 2", "All channels"] 477 | 478 | DTMF_CHARS = "0123456789ABCD*# " 479 | DTMF_CHARS_ID = "0123456789ABCDabcd" 480 | DTMF_CHARS_KILL = "0123456789ABCDabcd" 481 | DTMF_CHARS_UPDOWN = "0123456789ABCDabcd#* " 482 | DTMF_CODE_CHARS = "ABCD*# " 483 | DTMF_DECODE_RESPONSE_LIST = ["Do nothing", "Local ringing", "Replay response", 484 | "Local ringing + reply response"] 485 | 486 | KEYACTIONS_LIST = ["None", 487 | "Flashlight", 488 | "TX power", 489 | "Monitor", 490 | "Scan", 491 | "VOX", 492 | "Alarm", 493 | "FM broadcast radio", 494 | "1750Hz tone", 495 | "Lock keypad", 496 | "Switch main VFO", 497 | "Switch frequency/memory mode", 498 | "Switch demodulation" 499 | ] 500 | 501 | MIC_GAIN_LIST = ["+1.1dB", "+4.0dB", "+8.0dB", "+12.0dB", "+15.1dB"] 502 | 503 | 504 | def xorarr(data: bytes): 505 | """the communication is obfuscated using this fine mechanism""" 506 | tbl = [22, 108, 20, 230, 46, 145, 13, 64, 33, 53, 213, 64, 19, 3, 233, 128] 507 | ret = b"" 508 | idx = 0 509 | for byte in data: 510 | ret += bytes([byte ^ tbl[idx]]) 511 | idx = (idx+1) % len(tbl) 512 | return ret 513 | 514 | 515 | def calculate_crc16_xmodem(data: bytes): 516 | """ 517 | if this crc was used for communication to AND from the radio, then it 518 | would be a measure to increase reliability. 519 | but it's only used towards the radio, so it's for further obfuscation 520 | """ 521 | poly = 0x1021 522 | crc = 0x0 523 | for byte in data: 524 | crc = crc ^ (byte << 8) 525 | for _ in range(8): 526 | crc = crc << 1 527 | if crc & 0x10000: 528 | crc = (crc ^ poly) & 0xFFFF 529 | return crc & 0xFFFF 530 | 531 | 532 | def _send_command(serport, data: bytes): 533 | """Send a command to UV-K5 radio""" 534 | LOG.debug("Sending command (unobfuscated) len=0x%4.4x:\n%s", 535 | len(data), util.hexprint(data)) 536 | 537 | crc = calculate_crc16_xmodem(data) 538 | data2 = data + struct.pack("HBB", 0xabcd, len(data), 0) + \ 541 | xorarr(data2) + \ 542 | struct.pack(">H", 0xdcba) 543 | if DEBUG_SHOW_OBFUSCATED_COMMANDS: 544 | LOG.debug("Sending command (obfuscated):\n%s", util.hexprint(command)) 545 | try: 546 | result = serport.write(command) 547 | except Exception as e: 548 | raise errors.RadioError("Error writing data to radio") from e 549 | return result 550 | 551 | 552 | def _receive_reply(serport): 553 | header = serport.read(4) 554 | if len(header) != 4: 555 | LOG.warning("Header short read: [%s] len=%i", 556 | util.hexprint(header), len(header)) 557 | raise errors.RadioError("Header short read") 558 | if header[0] != 0xAB or header[1] != 0xCD or header[3] != 0x00: 559 | LOG.warning("Bad response header: %s len=%i", 560 | util.hexprint(header), len(header)) 561 | raise errors.RadioError("Bad response header") 562 | 563 | cmd = serport.read(int(header[2])) 564 | if len(cmd) != int(header[2]): 565 | LOG.warning("Body short read: [%s] len=%i", 566 | util.hexprint(cmd), len(cmd)) 567 | raise errors.RadioError("Command body short read") 568 | 569 | footer = serport.read(4) 570 | 571 | if len(footer) != 4: 572 | LOG.warning("Footer short read: [%s] len=%i", 573 | util.hexprint(footer), len(footer)) 574 | raise errors.RadioError("Footer short read") 575 | 576 | if footer[2] != 0xDC or footer[3] != 0xBA: 577 | LOG.debug("Reply before bad response footer (obfuscated)" 578 | "len=0x%4.4x:\n%s", len(cmd), util.hexprint(cmd)) 579 | LOG.warning("Bad response footer: %s len=%i", 580 | util.hexprint(footer), len(footer)) 581 | raise errors.RadioError("Bad response footer") 582 | 583 | if DEBUG_SHOW_OBFUSCATED_COMMANDS: 584 | LOG.debug("Received reply (obfuscated) len=0x%4.4x:\n%s", 585 | len(cmd), util.hexprint(cmd)) 586 | 587 | cmd2 = xorarr(cmd) 588 | 589 | LOG.debug("Received reply (unobfuscated) len=0x%4.4x:\n%s", 590 | len(cmd2), util.hexprint(cmd2)) 591 | 592 | return cmd2 593 | 594 | 595 | def _getstring(data: bytes, begin, maxlen): 596 | tmplen = min(maxlen+1, len(data)) 597 | ss = [data[i] for i in range(begin, tmplen)] 598 | key = 0 599 | for key, val in enumerate(ss): 600 | if val < ord(' ') or val > ord('~'): 601 | return ''.join(chr(x) for x in ss[0:key]) 602 | return '' 603 | 604 | 605 | def _sayhello(serport): 606 | hellopacket = b"\x14\x05\x04\x00\x6a\x39\x57\x64" 607 | 608 | tries = 5 609 | while True: 610 | LOG.debug("Sending hello packet") 611 | _send_command(serport, hellopacket) 612 | rep = _receive_reply(serport) 613 | if rep: 614 | break 615 | tries -= 1 616 | if tries == 0: 617 | LOG.warning("Failed to initialise radio") 618 | raise errors.RadioError("Failed to initialize radio") 619 | if rep.startswith(b'\x18\x05'): 620 | raise errors.RadioError("Radio is in programming mode, " 621 | "restart radio into normal mode") 622 | firmware = _getstring(rep, 4, 24) 623 | 624 | LOG.info("Found firmware: %s", firmware) 625 | return firmware 626 | 627 | 628 | def _readmem(serport, offset, length): 629 | LOG.debug("Sending readmem offset=0x%4.4x len=0x%4.4x", offset, length) 630 | 631 | readmem = b"\x1b\x05\x08\x00" + \ 632 | struct.pack("> 8) & 0xff): 664 | return True 665 | 666 | LOG.warning("Bad data from writemem") 667 | raise errors.RadioError("Bad response to writemem") 668 | 669 | 670 | def _resetradio(serport): 671 | resetpacket = b"\xdd\x05\x00\x00" 672 | _send_command(serport, resetpacket) 673 | 674 | 675 | def do_download(radio): 676 | """download eeprom from radio""" 677 | serport = radio.pipe 678 | serport.timeout = 0.5 679 | status = chirp_common.Status() 680 | status.cur = 0 681 | status.max = MEM_SIZE 682 | status.msg = "Downloading from radio" 683 | radio.status_fn(status) 684 | 685 | eeprom = b"" 686 | f = _sayhello(serport) 687 | if f: 688 | radio.FIRMWARE_VERSION = f 689 | else: 690 | raise errors.RadioError("Failed to initialize radio") 691 | 692 | addr = 0 693 | while addr < MEM_SIZE: 694 | data = _readmem(serport, addr, MEM_BLOCK) 695 | status.cur = addr 696 | radio.status_fn(status) 697 | 698 | if data and len(data) == MEM_BLOCK: 699 | eeprom += data 700 | addr += MEM_BLOCK 701 | else: 702 | raise errors.RadioError("Memory download incomplete") 703 | 704 | return memmap.MemoryMapBytes(eeprom) 705 | 706 | 707 | def do_upload(radio): 708 | """upload configuration to radio eeprom""" 709 | serport = radio.pipe 710 | serport.timeout = 0.5 711 | status = chirp_common.Status() 712 | status.cur = 0 713 | status.msg = "Uploading to radio" 714 | 715 | if radio.upload_calibration: 716 | status.max = MEM_SIZE-CAL_START 717 | start_addr = CAL_START 718 | stop_addr = MEM_SIZE 719 | else: 720 | status.max = PROG_SIZE 721 | start_addr = 0 722 | stop_addr = PROG_SIZE 723 | 724 | radio.status_fn(status) 725 | 726 | f = _sayhello(serport) 727 | if f: 728 | radio.FIRMWARE_VERSION = f 729 | else: 730 | return False 731 | 732 | addr = start_addr 733 | while addr < stop_addr: 734 | dat = radio.get_mmap()[addr:addr+MEM_BLOCK] 735 | _writemem(serport, dat, addr) 736 | status.cur = addr - start_addr 737 | radio.status_fn(status) 738 | if dat: 739 | addr += MEM_BLOCK 740 | else: 741 | raise errors.RadioError("Memory upload incomplete") 742 | status.msg = "Uploaded OK" 743 | 744 | _resetradio(serport) 745 | 746 | return True 747 | 748 | 749 | def min_max_def(value, min_val, max_val, default): 750 | """returns value if in bounds or default otherwise""" 751 | if min_val is not None and value < min_val: 752 | return default 753 | if max_val is not None and value > max_val: 754 | return default 755 | return value 756 | 757 | 758 | def list_def(value, lst, default): 759 | """return value if is in the list, default otherwise""" 760 | if isinstance(default, str): 761 | default = lst.index(default) 762 | if value < 0 or value >= len(lst): 763 | return default 764 | return value 765 | 766 | 767 | @directory.register 768 | class UVK5RadioEgzumer(chirp_common.CloneModeRadio): 769 | """Quansheng UV-K5 (egzumer)""" 770 | VENDOR = "Quansheng" 771 | MODEL = "UV-K5 (egzumer)" 772 | BAUD_RATE = 115200 773 | NEEDS_COMPAT_SERIAL = False 774 | FIRMWARE_VERSION = "" 775 | 776 | upload_calibration = False 777 | 778 | def _get_bands(self): 779 | is_wide = self._memobj.BUILD_OPTIONS.ENABLE_WIDE_RX \ 780 | if self._memobj is not None else True 781 | bands = BANDS_WIDE if is_wide else BANDS_STANDARD 782 | return bands 783 | 784 | def _find_band(self, hz): 785 | mhz = hz/1000000.0 786 | bands = self._get_bands() 787 | for bnd, rng in bands.items(): 788 | if rng[0] <= mhz <= rng[1]: 789 | return bnd 790 | return False 791 | 792 | def _get_vfo_channel_names(self): 793 | """generates VFO_CHANNEL_NAMES""" 794 | bands = self._get_bands() 795 | names = [] 796 | for bnd, rng in bands.items(): 797 | name = f"F{bnd + 1}({round(rng[0])}M-{round(rng[1])}M)" 798 | names.append(name + "A") 799 | names.append(name + "B") 800 | return names 801 | 802 | def _get_specials(self): 803 | """generates SPECIALS""" 804 | specials = {} 805 | for idx, name in enumerate(self._get_vfo_channel_names()): 806 | specials[name] = 200 + idx 807 | return specials 808 | 809 | @classmethod 810 | def get_prompts(cls): 811 | rp = chirp_common.RadioPrompts() 812 | rp.experimental = \ 813 | 'This is an experimental driver for the Quansheng UV-K5. ' \ 814 | 'It may harm your radio, or worse. Use at your own risk.\n\n' \ 815 | 'Before attempting to do any changes please download' \ 816 | 'the memory image from the radio with chirp ' \ 817 | 'and keep it. This can be later used to recover the ' \ 818 | 'original settings. \n\n' \ 819 | 'some details are not yet implemented' 820 | rp.pre_download = \ 821 | "1. Turn radio on.\n" \ 822 | "2. Connect cable to mic/spkr connector.\n" \ 823 | "3. Make sure connector is firmly connected.\n" \ 824 | "4. Click OK to download image from device.\n\n" \ 825 | "It may not work if you turn on the radio " \ 826 | "with the cable already attached\n" 827 | rp.pre_upload = \ 828 | "1. Turn radio on.\n" \ 829 | "2. Connect cable to mic/spkr connector.\n" \ 830 | "3. Make sure connector is firmly connected.\n" \ 831 | "4. Click OK to upload the image to device.\n\n" \ 832 | "It may not work if you turn on the radio " \ 833 | "with the cable already attached" 834 | return rp 835 | 836 | # Return information about this radio's features, including 837 | # how many memories it has, what bands it supports, etc 838 | def get_features(self): 839 | rf = chirp_common.RadioFeatures() 840 | rf.has_bank = False 841 | rf.valid_dtcs_codes = DTCS_CODES 842 | rf.has_rx_dtcs = True 843 | rf.has_ctone = True 844 | rf.has_settings = True 845 | rf.has_comment = False 846 | rf.valid_name_length = 10 847 | rf.valid_power_levels = UVK5_POWER_LEVELS 848 | rf.valid_special_chans = self._get_vfo_channel_names() 849 | rf.valid_duplexes = ["", "-", "+", "off"] 850 | 851 | steps = STEPS.copy() 852 | steps.sort() 853 | rf.valid_tuning_steps = steps 854 | 855 | rf.valid_tmodes = ["", "Tone", "TSQL", "DTCS", "Cross"] 856 | rf.valid_cross_modes = ["Tone->Tone", "Tone->DTCS", "DTCS->Tone", 857 | "->Tone", "->DTCS", "DTCS->", "DTCS->DTCS"] 858 | 859 | rf.valid_characters = chirp_common.CHARSET_ASCII 860 | rf.valid_modes = ["FM", "NFM", "AM", "NAM", "USB"] 861 | 862 | rf.valid_skips = [""] 863 | 864 | # This radio supports memories 1-200, 201-214 are the VFO memories 865 | rf.memory_bounds = (1, 200) 866 | 867 | # This is what the BK4819 chip supports 868 | # Will leave it in a comment, might be useful someday 869 | # rf.valid_bands = [(18000000, 620000000), 870 | # (840000000, 1300000000) 871 | # ] 872 | rf.valid_bands = [] 873 | bands = self._get_bands() 874 | for _, rng in bands.items(): 875 | rf.valid_bands.append( 876 | (int(rng[0]*1000000), int(rng[1]*1000000))) 877 | return rf 878 | 879 | # Do a download of the radio from the serial port 880 | def sync_in(self): 881 | self._mmap = do_download(self) 882 | self.process_mmap() 883 | 884 | # Do an upload of the radio to the serial port 885 | def sync_out(self): 886 | do_upload(self) 887 | 888 | # Convert the raw byte array into a memory object structure 889 | def process_mmap(self): 890 | self._memobj = bitwise.parse(MEM_FORMAT, self._mmap) 891 | 892 | # Return a raw representation of the memory object, which 893 | # is very helpful for development 894 | def get_raw_memory(self, number): 895 | return repr(self._memobj.channel[number-1]) 896 | 897 | def validate_memory(self, mem): 898 | msgs = super().validate_memory(mem) 899 | 900 | if mem.duplex == 'off': 901 | return msgs 902 | 903 | # find tx frequency 904 | if mem.duplex == '-': 905 | txfreq = mem.freq - mem.offset 906 | elif mem.duplex == '+': 907 | txfreq = mem.freq + mem.offset 908 | else: 909 | txfreq = mem.freq 910 | 911 | # find band 912 | band = self._find_band(txfreq) 913 | if band is False: 914 | msg = f"Transmit frequency {txfreq/1000000.0:.4f}MHz " \ 915 | "is not supported by this radio" 916 | msgs.append(chirp_common.ValidationWarning(msg)) 917 | 918 | band = self._find_band(mem.freq) 919 | if band is False: 920 | msg = f"The frequency {mem.freq/1000000.0:%.4f}MHz " \ 921 | "is not supported by this radio" 922 | msgs.append(chirp_common.ValidationWarning(msg)) 923 | 924 | return msgs 925 | 926 | def _set_tone(self, mem, _mem): 927 | ((txmode, txtone, txpol), 928 | (rxmode, rxtone, rxpol)) = chirp_common.split_tone_encode(mem) 929 | 930 | if txmode == "Tone": 931 | txtoval = CTCSS_TONES.index(txtone) 932 | txmoval = 0b01 933 | elif txmode == "DTCS": 934 | txmoval = txpol == "R" and 0b11 or 0b10 935 | txtoval = DTCS_CODES.index(txtone) 936 | else: 937 | txmoval = 0 938 | txtoval = 0 939 | 940 | if rxmode == "Tone": 941 | rxtoval = CTCSS_TONES.index(rxtone) 942 | rxmoval = 0b01 943 | elif rxmode == "DTCS": 944 | rxmoval = rxpol == "R" and 0b11 or 0b10 945 | rxtoval = DTCS_CODES.index(rxtone) 946 | else: 947 | rxmoval = 0 948 | rxtoval = 0 949 | 950 | _mem.rxcodeflag = rxmoval 951 | _mem.txcodeflag = txmoval 952 | _mem.rxcode = rxtoval 953 | _mem.txcode = txtoval 954 | 955 | def _get_tone(self, mem, _mem): 956 | rxtype = _mem.rxcodeflag 957 | txtype = _mem.txcodeflag 958 | rx_tmode = TMODES[rxtype] 959 | tx_tmode = TMODES[txtype] 960 | 961 | rx_tone = tx_tone = None 962 | 963 | if tx_tmode == "Tone": 964 | if _mem.txcode < len(CTCSS_TONES): 965 | tx_tone = CTCSS_TONES[_mem.txcode] 966 | else: 967 | tx_tone = 0 968 | tx_tmode = "" 969 | elif tx_tmode == "DTCS": 970 | if _mem.txcode < len(DTCS_CODES): 971 | tx_tone = DTCS_CODES[_mem.txcode] 972 | else: 973 | tx_tone = 0 974 | tx_tmode = "" 975 | 976 | if rx_tmode == "Tone": 977 | if _mem.rxcode < len(CTCSS_TONES): 978 | rx_tone = CTCSS_TONES[_mem.rxcode] 979 | else: 980 | rx_tone = 0 981 | rx_tmode = "" 982 | elif rx_tmode == "DTCS": 983 | if _mem.rxcode < len(DTCS_CODES): 984 | rx_tone = DTCS_CODES[_mem.rxcode] 985 | else: 986 | rx_tone = 0 987 | rx_tmode = "" 988 | 989 | tx_pol = txtype == 0x03 and "R" or "N" 990 | rx_pol = rxtype == 0x03 and "R" or "N" 991 | 992 | chirp_common.split_tone_decode(mem, (tx_tmode, tx_tone, tx_pol), 993 | (rx_tmode, rx_tone, rx_pol)) 994 | 995 | # Extract a high-level memory object from the low-level memory map 996 | # This is called to populate a memory in the UI 997 | def get_memory(self, number): 998 | 999 | mem = chirp_common.Memory() 1000 | 1001 | if isinstance(number, str): 1002 | ch_num = self._get_specials()[number] 1003 | mem.extd_number = number 1004 | else: 1005 | ch_num = number - 1 1006 | 1007 | mem.number = ch_num + 1 1008 | 1009 | _mem = self._memobj.channel[ch_num] 1010 | 1011 | is_empty = False 1012 | # We'll consider any blank (i.e. 0MHz frequency) to be empty 1013 | if (_mem.freq == 0xffffffff) or (_mem.freq == 0): 1014 | is_empty = True 1015 | 1016 | # We'll also look at the channel attributes if a memory has them 1017 | tmpscn = 0 1018 | tmp_comp = 0 1019 | if ch_num < 200: 1020 | _mem3 = self._memobj.ch_attr[ch_num] 1021 | # free memory bit 1022 | is_empty |= _mem3.is_free 1023 | # scanlists 1024 | tmpscn = _mem3.is_scanlist1 + _mem3.is_scanlist2 * 2 1025 | tmp_comp = list_def(_mem3.compander, COMPANDER_LIST, 0) 1026 | elif ch_num < 214: 1027 | att_num = 200 + int((ch_num - 200) / 2) 1028 | _mem3 = self._memobj.ch_attr[att_num] 1029 | is_empty |= _mem3.is_free 1030 | tmp_comp = list_def(_mem3.compander, COMPANDER_LIST, 0) 1031 | 1032 | if is_empty: 1033 | mem.empty = True 1034 | # set some sane defaults: 1035 | mem.power = UVK5_POWER_LEVELS[2] 1036 | mem.extra = RadioSettingGroup("Extra", "extra") 1037 | 1038 | val = RadioSettingValueBoolean(False) 1039 | rs = RadioSetting("busyChLockout", "BusyCL", val) 1040 | mem.extra.append(rs) 1041 | 1042 | val = RadioSettingValueBoolean(False) 1043 | rs = RadioSetting("frev", "FreqRev", val) 1044 | mem.extra.append(rs) 1045 | 1046 | val = RadioSettingValueList(PTTID_LIST) 1047 | rs = RadioSetting("pttid", "PTTID", val) 1048 | mem.extra.append(rs) 1049 | 1050 | val = RadioSettingValueBoolean(False) 1051 | rs = RadioSetting("dtmfdecode", "DTMF decode", val) 1052 | if self._memobj.BUILD_OPTIONS.ENABLE_DTMF_CALLING: 1053 | mem.extra.append(rs) 1054 | 1055 | val = RadioSettingValueList(SCRAMBLER_LIST) 1056 | rs = RadioSetting("scrambler", "Scrambler", val) 1057 | mem.extra.append(rs) 1058 | 1059 | val = RadioSettingValueList(COMPANDER_LIST) 1060 | rs = RadioSetting("compander", "Compander", val) 1061 | mem.extra.append(rs) 1062 | 1063 | val = RadioSettingValueList(SCANLIST_LIST) 1064 | rs = RadioSetting("scanlists", "Scanlists", val) 1065 | mem.extra.append(rs) 1066 | 1067 | # actually the step and duplex are overwritten by chirp based on 1068 | # bandplan. they are here to document sane defaults for IARU r1 1069 | # mem.tuning_step = 25.0 1070 | # mem.duplex = "off" 1071 | 1072 | return mem 1073 | 1074 | if ch_num > 199: 1075 | mem.name = self._get_vfo_channel_names()[ch_num-200] 1076 | mem.immutable = ["name", "scanlists"] 1077 | else: 1078 | _mem2 = self._memobj.channelname[ch_num] 1079 | for char in _mem2.name: 1080 | if str(char) == "\xFF" or str(char) == "\x00": 1081 | break 1082 | mem.name += str(char) 1083 | mem.name = mem.name.rstrip() 1084 | 1085 | # Convert your low-level frequency to Hertz 1086 | mem.freq = int(_mem.freq)*10 1087 | mem.offset = int(_mem.offset)*10 1088 | 1089 | if mem.offset == 0: 1090 | mem.duplex = '' 1091 | else: 1092 | if _mem.offsetDir == FLAGS1_OFFSET_MINUS: 1093 | if _mem.freq == _mem.offset: 1094 | # fake tx disable by setting tx to 0 MHz 1095 | mem.duplex = 'off' 1096 | mem.offset = 0 1097 | else: 1098 | mem.duplex = '-' 1099 | elif _mem.offsetDir == FLAGS1_OFFSET_PLUS: 1100 | mem.duplex = '+' 1101 | else: 1102 | mem.duplex = '' 1103 | 1104 | # tone data 1105 | self._get_tone(mem, _mem) 1106 | 1107 | # mode 1108 | temp_modes = self.get_features().valid_modes 1109 | temp_modul = _mem.modulation*2 + _mem.bandwidth 1110 | if temp_modul < len(temp_modes): 1111 | mem.mode = temp_modes[temp_modul] 1112 | elif temp_modul == 5: # USB with narrow setting 1113 | mem.mode = temp_modes[4] 1114 | elif temp_modul >= len(temp_modes): 1115 | mem.mode = "UNSUPPORTED BY CHIRP" 1116 | 1117 | # tuning step 1118 | tstep = _mem.step 1119 | if tstep < len(STEPS): 1120 | mem.tuning_step = STEPS[tstep] 1121 | else: 1122 | mem.tuning_step = 2.5 1123 | 1124 | # power 1125 | if _mem.txpower == POWER_HIGH: 1126 | mem.power = UVK5_POWER_LEVELS[2] 1127 | elif _mem.txpower == POWER_MEDIUM: 1128 | mem.power = UVK5_POWER_LEVELS[1] 1129 | else: 1130 | mem.power = UVK5_POWER_LEVELS[0] 1131 | 1132 | # We'll consider any blank (i.e. 0MHz frequency) to be empty 1133 | if (_mem.freq == 0xffffffff) or (_mem.freq == 0): 1134 | mem.empty = True 1135 | else: 1136 | mem.empty = False 1137 | 1138 | mem.extra = RadioSettingGroup("Extra", "extra") 1139 | 1140 | # BusyCL 1141 | val = RadioSettingValueBoolean(_mem.busyChLockout) 1142 | rs = RadioSetting("busyChLockout", "Busy Ch Lockout (BusyCL)", val) 1143 | mem.extra.append(rs) 1144 | 1145 | # Frequency reverse 1146 | val = RadioSettingValueBoolean(_mem.freq_reverse) 1147 | rs = RadioSetting("frev", "Reverse Frequencies (R)", val) 1148 | mem.extra.append(rs) 1149 | 1150 | # PTTID 1151 | pttid = list_def(_mem.dtmf_pttid, PTTID_LIST, 0) 1152 | val = RadioSettingValueList(PTTID_LIST, None, pttid) 1153 | rs = RadioSetting("pttid", "PTT ID (PTT ID)", val) 1154 | mem.extra.append(rs) 1155 | 1156 | # DTMF DECODE 1157 | val = RadioSettingValueBoolean(_mem.dtmf_decode) 1158 | rs = RadioSetting("dtmfdecode", "DTMF decode (D Decd)", val) 1159 | if self._memobj.BUILD_OPTIONS.ENABLE_DTMF_CALLING: 1160 | mem.extra.append(rs) 1161 | 1162 | # Scrambler 1163 | enc = list_def(_mem.scrambler, SCRAMBLER_LIST, 0) 1164 | val = RadioSettingValueList(SCRAMBLER_LIST, None, enc) 1165 | rs = RadioSetting("scrambler", "Scrambler (Scramb)", val) 1166 | mem.extra.append(rs) 1167 | 1168 | # Compander 1169 | val = RadioSettingValueList(COMPANDER_LIST, None, tmp_comp) 1170 | rs = RadioSetting("compander", "Compander (Compnd)", val) 1171 | mem.extra.append(rs) 1172 | 1173 | val = RadioSettingValueList(SCANLIST_LIST, None, tmpscn) 1174 | rs = RadioSetting("scanlists", "Scanlists (SList)", val) 1175 | mem.extra.append(rs) 1176 | 1177 | return mem 1178 | 1179 | def set_settings(self, settings): 1180 | _mem = self._memobj 1181 | for element in settings: 1182 | if not isinstance(element, RadioSetting): 1183 | self.set_settings(element) 1184 | continue 1185 | 1186 | elname = element.get_name() 1187 | 1188 | # basic settings 1189 | 1190 | # VFO_A e80 ScreenChannel_A 1191 | if elname == "VFO_A_chn": 1192 | _mem.ScreenChannel_A = int(element.value) 1193 | if _mem.ScreenChannel_A < 200: 1194 | _mem.MrChannel_A = _mem.ScreenChannel_A 1195 | elif _mem.ScreenChannel_A < 207: 1196 | _mem.FreqChannel_A = _mem.ScreenChannel_A 1197 | else: 1198 | _mem.NoaaChannel_A = _mem.ScreenChannel_A 1199 | 1200 | # VFO_B e83 1201 | elif elname == "VFO_B_chn": 1202 | _mem.ScreenChannel_B = int(element.value) 1203 | if _mem.ScreenChannel_B < 200: 1204 | _mem.MrChannel_B = _mem.ScreenChannel_B 1205 | elif _mem.ScreenChannel_B < 207: 1206 | _mem.FreqChannel_B = _mem.ScreenChannel_B 1207 | else: 1208 | _mem.NoaaChannel_B = _mem.ScreenChannel_B 1209 | 1210 | # TX_VFO channel selected A,B 1211 | elif elname == "TX_VFO": 1212 | _mem.TX_VFO = int(element.value) 1213 | 1214 | # call channel 1215 | elif elname == "call_channel": 1216 | _mem.call_channel = int(element.value) 1217 | 1218 | # squelch 1219 | elif elname == "squelch": 1220 | _mem.squelch = int(element.value) 1221 | 1222 | # TOT 1223 | elif elname == "tot": 1224 | _mem.max_talk_time = int(element.value) 1225 | 1226 | # NOAA autoscan 1227 | elif elname == "noaa_autoscan": 1228 | _mem.noaa_autoscan = int(element.value) 1229 | 1230 | # VOX 1231 | elif elname == "vox": 1232 | voxvalue = int(element.value) 1233 | _mem.vox_switch = voxvalue > 0 1234 | _mem.vox_level = (voxvalue - 1) if _mem.vox_switch else 0 1235 | 1236 | # mic gain 1237 | elif elname == "mic_gain": 1238 | _mem.mic_gain = int(element.value) 1239 | 1240 | # Channel display mode 1241 | elif elname == "channel_display_mode": 1242 | _mem.channel_display_mode = int(element.value) 1243 | 1244 | # RX Mode 1245 | elif elname == "rx_mode": 1246 | tmptxmode = int(element.value) 1247 | tmpmainvfo = _mem.TX_VFO + 1 1248 | _mem.crossband = tmpmainvfo * bool(tmptxmode & 0b10) 1249 | _mem.dual_watch = tmpmainvfo * bool(tmptxmode & 0b01) 1250 | 1251 | # Battery Save 1252 | elif elname == "battery_save": 1253 | _mem.battery_save = int(element.value) 1254 | 1255 | # Backlight auto mode 1256 | elif elname == "backlight_time": 1257 | _mem.backlight_time = int(element.value) 1258 | 1259 | # Backlight min 1260 | elif elname == "backlight_min": 1261 | _mem.backlight_min = int(element.value) 1262 | 1263 | # Backlight max 1264 | elif elname == "backlight_max": 1265 | _mem.backlight_max = int(element.value) 1266 | 1267 | # Backlight TX_RX 1268 | elif elname == "backlight_on_TX_RX": 1269 | _mem.backlight_on_TX_RX = int(element.value) 1270 | # AM_fix 1271 | elif elname == "AM_fix": 1272 | _mem.AM_fix = int(element.value) 1273 | 1274 | # mic_bar 1275 | elif elname == "mem.mic_bar": 1276 | _mem.mic_bar = int(element.value) 1277 | 1278 | # Batterie txt 1279 | elif elname == "_mem.battery_text": 1280 | _mem.battery_text = int(element.value) 1281 | 1282 | # Tail tone elimination 1283 | elif elname == "ste": 1284 | _mem.ste = int(element.value) 1285 | 1286 | # VFO Open 1287 | elif elname == "freq_mode_allowed": 1288 | _mem.freq_mode_allowed = int(element.value) 1289 | 1290 | # Beep control 1291 | elif elname == "button_beep": 1292 | _mem.button_beep = int(element.value) 1293 | 1294 | # Scan resume mode 1295 | elif elname == "scan_resume_mode": 1296 | _mem.scan_resume_mode = int(element.value) 1297 | 1298 | # Keypad lock 1299 | elif elname == "key_lock": 1300 | _mem.key_lock = int(element.value) 1301 | 1302 | # Auto keypad lock 1303 | elif elname == "auto_keypad_lock": 1304 | _mem.auto_keypad_lock = int(element.value) 1305 | 1306 | # Power on display mode 1307 | elif elname == "welcome_mode": 1308 | _mem.power_on_dispmode = int(element.value) 1309 | 1310 | # Keypad Tone 1311 | elif elname == "voice": 1312 | _mem.voice = int(element.value) 1313 | 1314 | elif elname == "s0_level": 1315 | _mem.s0_level = -int(element.value) 1316 | 1317 | elif elname == "s9_level": 1318 | _mem.s9_level = -int(element.value) 1319 | 1320 | elif elname == "password": 1321 | if element.value.get_value() is None or element.value == "": 1322 | _mem.password = 0xFFFFFFFF 1323 | else: 1324 | _mem.password = int(element.value) 1325 | 1326 | # Alarm mode 1327 | elif elname == "alarm_mode": 1328 | _mem.alarm_mode = int(element.value) 1329 | 1330 | # Reminding of end of talk 1331 | elif elname == "roger_beep": 1332 | _mem.roger_beep = int(element.value) 1333 | 1334 | # Repeater tail tone elimination 1335 | elif elname == "rp_ste": 1336 | _mem.rp_ste = int(element.value) 1337 | 1338 | # Logo string 1 1339 | elif elname == "logo1": 1340 | bts = str(element.value).rstrip("\x20\xff\x00")+"\x00"*12 1341 | _mem.logo_line1 = bts[0:12]+"\x00\xff\xff\xff" 1342 | 1343 | # Logo string 2 1344 | elif elname == "logo2": 1345 | bts = str(element.value).rstrip("\x20\xff\x00")+"\x00"*12 1346 | _mem.logo_line2 = bts[0:12]+"\x00\xff\xff\xff" 1347 | 1348 | # unlock settings 1349 | 1350 | # FLOCK 1351 | elif elname == "int_flock": 1352 | _mem.int_flock = int(element.value) 1353 | 1354 | # 350TX 1355 | elif elname == "int_350tx": 1356 | _mem.int_350tx = int(element.value) 1357 | 1358 | # KILLED 1359 | elif elname == "int_KILLED": 1360 | _mem.int_KILLED = int(element.value) 1361 | 1362 | # 200TX 1363 | elif elname == "int_200tx": 1364 | _mem.int_200tx = int(element.value) 1365 | 1366 | # 500TX 1367 | elif elname == "int_500tx": 1368 | _mem.int_500tx = int(element.value) 1369 | 1370 | # 350EN 1371 | elif elname == "int_350en": 1372 | _mem.int_350en = int(element.value) 1373 | 1374 | # SCREN 1375 | elif elname == "int_scren": 1376 | _mem.int_scren = int(element.value) 1377 | 1378 | # battery type 1379 | elif elname == "Battery_type": 1380 | _mem.Battery_type = int(element.value) 1381 | # fm radio 1382 | for i in range(1, 21): 1383 | freqname = "FM_" + str(i) 1384 | if elname == freqname: 1385 | val = str(element.value).strip() 1386 | try: 1387 | val2 = int(float(val)*10) 1388 | except Exception: 1389 | val2 = 0xffff 1390 | 1391 | if val2 < FMMIN*10 or val2 > FMMAX*10: 1392 | val2 = 0xffff 1393 | # raise errors.InvalidValueError( 1394 | # "FM radio frequency should be a value " 1395 | # "in the range %.1f - %.1f" % (FMMIN , FMMAX)) 1396 | _mem.fmfreq[i-1] = val2 1397 | 1398 | # dtmf settings 1399 | if elname == "dtmf_side_tone": 1400 | _mem.dtmf.side_tone = int(element.value) 1401 | 1402 | elif elname == "dtmf_separate_code": 1403 | _mem.dtmf.separate_code = str(element.value) 1404 | 1405 | elif elname == "dtmf_group_call_code": 1406 | _mem.dtmf.group_call_code = element.value 1407 | 1408 | elif elname == "dtmf_decode_response": 1409 | _mem.dtmf.decode_response = int(element.value) 1410 | 1411 | elif elname == "dtmf_auto_reset_time": 1412 | _mem.dtmf.auto_reset_time = int(int(element.value)/10) 1413 | 1414 | elif elname == "dtmf_preload_time": 1415 | _mem.dtmf.preload_time = int(int(element.value)/10) 1416 | 1417 | elif elname == "dtmf_first_code_persist_time": 1418 | _mem.dtmf.first_code_persist_time = int(int(element.value)/10) 1419 | 1420 | elif elname == "dtmf_hash_persist_time": 1421 | _mem.dtmf.hash_persist_time = int(int(element.value)/10) 1422 | 1423 | elif elname == "dtmf_code_persist_time": 1424 | _mem.dtmf.code_persist_time = \ 1425 | int(int(element.value)/10) 1426 | 1427 | elif elname == "dtmf_code_interval_time": 1428 | _mem.dtmf.code_interval_time = \ 1429 | int(int(element.value)/10) 1430 | 1431 | elif elname == "dtmf_permit_remote_kill": 1432 | _mem.dtmf.permit_remote_kill = \ 1433 | int(element.value) 1434 | 1435 | elif elname == "dtmf_dtmf_local_code": 1436 | k = str(element.value).rstrip("\x20\xff\x00") + "\x00"*3 1437 | _mem.dtmf.local_code = k[0:3] 1438 | 1439 | elif elname == "dtmf_dtmf_up_code": 1440 | k = str(element.value).strip("\x20\xff\x00") + "\x00"*16 1441 | _mem.dtmf.up_code = k[0:16] 1442 | 1443 | elif elname == "dtmf_dtmf_down_code": 1444 | k = str(element.value).rstrip("\x20\xff\x00") + "\x00"*16 1445 | _mem.dtmf.down_code = k[0:16] 1446 | 1447 | elif elname == "dtmf_kill_code": 1448 | k = str(element.value).strip("\x20\xff\x00") + "\x00"*5 1449 | _mem.dtmf.kill_code = k[0:5] 1450 | 1451 | elif elname == "dtmf_revive_code": 1452 | k = str(element.value).strip("\x20\xff\x00") + "\x00"*5 1453 | _mem.dtmf.revive_code = k[0:5] 1454 | 1455 | elif elname == "live_DTMF_decoder": 1456 | _mem.live_DTMF_decoder = int(element.value) 1457 | 1458 | # dtmf contacts 1459 | for i in range(1, 17): 1460 | varname = "DTMF_" + str(i) 1461 | if elname == varname: 1462 | k = str(element.value).rstrip("\x20\xff\x00") + "\x00"*8 1463 | _mem.dtmfcontact[i-1].name = k[0:8] 1464 | 1465 | varnumname = "DTMFNUM_" + str(i) 1466 | if elname == varnumname: 1467 | k = str(element.value).rstrip("\x20\xff\x00") + "\xff"*3 1468 | _mem.dtmfcontact[i-1].number = k[0:3] 1469 | 1470 | # scanlist stuff 1471 | if elname == "slDef": 1472 | _mem.slDef = int(element.value) 1473 | 1474 | elif elname == "sl1PriorEnab": 1475 | _mem.sl1PriorEnab = int(element.value) 1476 | 1477 | elif elname == "sl2PriorEnab": 1478 | _mem.sl2PriorEnab = int(element.value) 1479 | 1480 | elif elname in ["sl1PriorCh1", "sl1PriorCh2", "sl2PriorCh1", 1481 | "sl2PriorCh2"]: 1482 | val = int(element.value) 1483 | 1484 | if val > 200 or val < 1: 1485 | val = 0xff 1486 | else: 1487 | val -= 1 1488 | 1489 | _mem[elname] = val 1490 | 1491 | if elname == "key1_shortpress_action": 1492 | _mem.key1_shortpress_action = int(element.value) 1493 | 1494 | elif elname == "key1_longpress_action": 1495 | _mem.key1_longpress_action = int(element.value) 1496 | 1497 | elif elname == "key2_shortpress_action": 1498 | _mem.key2_shortpress_action = int(element.value) 1499 | 1500 | elif elname == "key2_longpress_action": 1501 | _mem.key2_longpress_action = int(element.value) 1502 | 1503 | elif elname == "keyM_longpress_action": 1504 | _mem.keyM_longpress_action = int(element.value) 1505 | 1506 | elif element.changed() and elname.startswith("_mem.cal."): 1507 | exec(elname + " = element.value.get_value()") 1508 | 1509 | def get_settings(self): 1510 | _mem = self._memobj 1511 | basic = RadioSettingGroup("basic", "Basic Settings") 1512 | advanced = RadioSettingGroup("advanced", "Advanced Settings") 1513 | keya = RadioSettingGroup("keya", "Programmable Keys") 1514 | dtmf = RadioSettingGroup("dtmf", "DTMF Settings") 1515 | dtmfc = RadioSettingGroup("dtmfc", "DTMF Contacts") 1516 | scanl = RadioSettingGroup("scn", "Scan Lists") 1517 | unlock = RadioSettingGroup("unlock", "Unlock Settings") 1518 | fmradio = RadioSettingGroup("fmradio", "FM Radio") 1519 | calibration = RadioSettingGroup("calibration", "Calibration") 1520 | 1521 | roinfo = RadioSettingGroup("roinfo", "Driver Information") 1522 | top = RadioSettings() 1523 | top.append(basic) 1524 | top.append(advanced) 1525 | top.append(keya) 1526 | top.append(dtmf) 1527 | if _mem.BUILD_OPTIONS.ENABLE_DTMF_CALLING: 1528 | top.append(dtmfc) 1529 | top.append(scanl) 1530 | top.append(unlock) 1531 | if _mem.BUILD_OPTIONS.ENABLE_FMRADIO: 1532 | top.append(fmradio) 1533 | top.append(roinfo) 1534 | top.append(calibration) 1535 | 1536 | # helper function 1537 | def append_label(radio_setting, label, descr=""): 1538 | if not hasattr(append_label, 'idx'): 1539 | append_label.idx = 0 1540 | 1541 | val = RadioSettingValueString(len(descr), len(descr), descr) 1542 | val.set_mutable(False) 1543 | rs = RadioSetting("label" + str(append_label.idx), label, val) 1544 | append_label.idx += 1 1545 | radio_setting.append(rs) 1546 | 1547 | # Programmable keys 1548 | def get_action(action_num): 1549 | """"get actual key action""" 1550 | has_alarm = self._memobj.BUILD_OPTIONS.ENABLE_ALARM 1551 | has1750 = self._memobj.BUILD_OPTIONS.ENABLE_TX1750 1552 | has_flashlight = self._memobj.BUILD_OPTIONS.ENABLE_FLASHLIGHT 1553 | lst = KEYACTIONS_LIST.copy() 1554 | if not has_alarm: 1555 | lst.remove("Alarm") 1556 | if not has1750: 1557 | lst.remove("1750Hz tone") 1558 | if not has_flashlight: 1559 | lst.remove("Flashlight") 1560 | 1561 | action_num = int(action_num) 1562 | if action_num >= len(KEYACTIONS_LIST) or \ 1563 | KEYACTIONS_LIST[action_num] not in lst: 1564 | action_num = 0 1565 | return lst, KEYACTIONS_LIST[action_num] 1566 | 1567 | val = RadioSettingValueList(*get_action(_mem.key1_shortpress_action)) 1568 | rs = RadioSetting("key1_shortpress_action", 1569 | "Side key 1 short press (F1Shrt)", val) 1570 | keya.append(rs) 1571 | 1572 | val = RadioSettingValueList(*get_action(_mem.key1_longpress_action)) 1573 | rs = RadioSetting("key1_longpress_action", 1574 | "Side key 1 long press (F1Long)", val) 1575 | keya.append(rs) 1576 | 1577 | val = RadioSettingValueList(*get_action(_mem.key2_shortpress_action)) 1578 | rs = RadioSetting("key2_shortpress_action", 1579 | "Side key 2 short press (F2Shrt)", val) 1580 | keya.append(rs) 1581 | 1582 | val = RadioSettingValueList(*get_action(_mem.key2_longpress_action)) 1583 | rs = RadioSetting("key2_longpress_action", 1584 | "Side key 2 long press (F2Long)", val) 1585 | keya.append(rs) 1586 | 1587 | val = RadioSettingValueList(*get_action(_mem.keyM_longpress_action)) 1588 | rs = RadioSetting("keyM_longpress_action", 1589 | "Menu key long press (M Long)", val) 1590 | keya.append(rs) 1591 | 1592 | # ----------------- DTMF settings 1593 | 1594 | tmpval = str(_mem.dtmf.separate_code) 1595 | if tmpval not in DTMF_CODE_CHARS: 1596 | tmpval = '*' 1597 | val = RadioSettingValueString(1, 1, tmpval) 1598 | val.set_charset(DTMF_CODE_CHARS) 1599 | sep_code_setting = RadioSetting("dtmf_separate_code", 1600 | "Separate Code", val) 1601 | 1602 | tmpval = str(_mem.dtmf.group_call_code) 1603 | if tmpval not in DTMF_CODE_CHARS: 1604 | tmpval = '#' 1605 | val = RadioSettingValueString(1, 1, tmpval) 1606 | val.set_charset(DTMF_CODE_CHARS) 1607 | group_code_setting = RadioSetting("dtmf_group_call_code", 1608 | "Group Call Code", val) 1609 | 1610 | tmpval = min_max_def(_mem.dtmf.first_code_persist_time * 10, 1611 | 30, 1000, 300) 1612 | val = RadioSettingValueInteger(30, 1000, tmpval, 10) 1613 | first_code_per_setting = \ 1614 | RadioSetting("dtmf_first_code_persist_time", 1615 | "First code persist time (ms)", val) 1616 | 1617 | tmpval = min_max_def(_mem.dtmf.hash_persist_time * 10, 30, 1000, 300) 1618 | val = RadioSettingValueInteger(30, 1000, tmpval, 10) 1619 | spec_per_setting = RadioSetting("dtmf_hash_persist_time", 1620 | "#/* persist time (ms)", val) 1621 | 1622 | tmpval = min_max_def(_mem.dtmf.code_persist_time * 10, 30, 1000, 300) 1623 | val = RadioSettingValueInteger(30, 1000, tmpval, 10) 1624 | code_per_setting = RadioSetting("dtmf_code_persist_time", 1625 | "Code persist time (ms)", val) 1626 | 1627 | tmpval = min_max_def(_mem.dtmf.code_interval_time * 10, 30, 1000, 300) 1628 | val = RadioSettingValueInteger(30, 1000, tmpval, 10) 1629 | code_int_setting = RadioSetting("dtmf_code_interval_time", 1630 | "Code interval time (ms)", val) 1631 | 1632 | tmpval = str(_mem.dtmf.local_code).upper().strip( 1633 | "\x00\xff\x20") 1634 | for i in tmpval: 1635 | if i in DTMF_CHARS_ID: 1636 | continue 1637 | tmpval = "103" 1638 | break 1639 | val = RadioSettingValueString(3, 3, tmpval) 1640 | val.set_charset(DTMF_CHARS_ID) 1641 | ani_id_setting = \ 1642 | RadioSetting("dtmf_dtmf_local_code", 1643 | "Local code (3 chars 0-9 ABCD) (ANI ID)", val) 1644 | 1645 | tmpval = str(_mem.dtmf.up_code).upper().strip( 1646 | "\x00\xff\x20") 1647 | for i in tmpval: 1648 | if i in DTMF_CHARS_UPDOWN or i == "": 1649 | continue 1650 | else: 1651 | tmpval = "123" 1652 | break 1653 | val = RadioSettingValueString(1, 16, tmpval) 1654 | val.set_charset(DTMF_CHARS_UPDOWN) 1655 | up_code_setting = \ 1656 | RadioSetting("dtmf_dtmf_up_code", 1657 | "Up code (1-16 chars 0-9 ABCD*#) (UPCode)", val) 1658 | 1659 | tmpval = str(_mem.dtmf.down_code).upper().strip( 1660 | "\x00\xff\x20") 1661 | for i in tmpval: 1662 | if i in DTMF_CHARS_UPDOWN: 1663 | continue 1664 | else: 1665 | tmpval = "456" 1666 | break 1667 | val = RadioSettingValueString(1, 16, tmpval) 1668 | val.set_charset(DTMF_CHARS_UPDOWN) 1669 | dw_code_setting = \ 1670 | RadioSetting("dtmf_dtmf_down_code", 1671 | "Down code (1-16 chars 0-9 ABCD*#) (DWCode)", val) 1672 | 1673 | val = RadioSettingValueBoolean(_mem.dtmf.side_tone) 1674 | dtmf_side_tone_setting = \ 1675 | RadioSetting("dtmf_side_tone", 1676 | "DTMF Sidetone on speaker when sent (D ST)", val) 1677 | 1678 | tmpval = list_def(_mem.dtmf.decode_response, 1679 | DTMF_DECODE_RESPONSE_LIST, 0) 1680 | val = RadioSettingValueList(DTMF_DECODE_RESPONSE_LIST, None, tmpval) 1681 | dtmf_resp_setting = RadioSetting("dtmf_decode_response", 1682 | "Decode Response (D Resp)", val) 1683 | 1684 | tmpval = min_max_def(_mem.dtmf.auto_reset_time, 5, 60, 5) 1685 | val = RadioSettingValueInteger(5, 60, tmpval) 1686 | d_hold_setting = RadioSetting("dtmf_auto_reset_time", 1687 | "Auto reset time (s) (D Hold)", val) 1688 | 1689 | # D Prel 1690 | tmpval = min_max_def(_mem.dtmf.preload_time * 10, 30, 990, 300) 1691 | val = RadioSettingValueInteger(30, 990, tmpval, 10) 1692 | d_prel_setting = RadioSetting("dtmf_preload_time", 1693 | "Pre-load time (ms) (D Prel)", val) 1694 | 1695 | # D LIVE 1696 | val = RadioSettingValueBoolean(_mem.live_DTMF_decoder) 1697 | d_live_setting = \ 1698 | RadioSetting("live_DTMF_decoder", "Displays DTMF codes" 1699 | " received in the middle of the screen (D Live)", val) 1700 | 1701 | val = RadioSettingValueBoolean(_mem.dtmf.permit_remote_kill) 1702 | perm_kill_setting = RadioSetting("dtmf_permit_remote_kill", 1703 | "Permit remote kill", val) 1704 | 1705 | tmpval = str(_mem.dtmf.kill_code).upper().strip( 1706 | "\x00\xff\x20") 1707 | for i in tmpval: 1708 | if i in DTMF_CHARS_KILL: 1709 | continue 1710 | else: 1711 | tmpval = "77777" 1712 | break 1713 | if not len(tmpval) == 5: 1714 | tmpval = "77777" 1715 | val = RadioSettingValueString(5, 5, tmpval) 1716 | val.set_charset(DTMF_CHARS_KILL) 1717 | kill_code_setting = RadioSetting("dtmf_kill_code", 1718 | "Kill code (5 chars 0-9 ABCD)", val) 1719 | 1720 | tmpval = str(_mem.dtmf.revive_code).upper().strip( 1721 | "\x00\xff\x20") 1722 | for i in tmpval: 1723 | if i in DTMF_CHARS_KILL: 1724 | continue 1725 | else: 1726 | tmpval = "88888" 1727 | break 1728 | if not len(tmpval) == 5: 1729 | tmpval = "88888" 1730 | val = RadioSettingValueString(5, 5, tmpval) 1731 | val.set_charset(DTMF_CHARS_KILL) 1732 | rev_code_setting = RadioSetting("dtmf_revive_code", 1733 | "Revive code (5 chars 0-9 ABCD)", val) 1734 | 1735 | val = RadioSettingValueBoolean(_mem.int_KILLED) 1736 | killed_setting = RadioSetting("int_KILLED", "DTMF kill lock", val) 1737 | 1738 | # ----------------- DTMF Contacts 1739 | 1740 | append_label(dtmfc, "DTMF Contacts (D List)", 1741 | "All DTMF Contacts are 3 codes " 1742 | "(valid: 0-9 * # ABCD), " 1743 | "or an empty string") 1744 | 1745 | for i in range(1, 17): 1746 | varname = "DTMF_"+str(i) 1747 | varnumname = "DTMFNUM_"+str(i) 1748 | vardescr = "DTMF Contact "+str(i)+" name" 1749 | varinumdescr = "DTMF Contact "+str(i)+" number" 1750 | 1751 | cntn = str(_mem.dtmfcontact[i-1].name).strip("\x20\x00\xff") 1752 | cntnum = str(_mem.dtmfcontact[i-1].number).strip("\x20\x00\xff") 1753 | 1754 | val = RadioSettingValueString(0, 8, cntn) 1755 | rs = RadioSetting(varname, vardescr, val) 1756 | dtmfc.append(rs) 1757 | 1758 | val = RadioSettingValueString(0, 3, cntnum) 1759 | val.set_charset(DTMF_CHARS) 1760 | rs = RadioSetting(varnumname, varinumdescr, val) 1761 | dtmfc.append(rs) 1762 | 1763 | # ----------------- Scan Lists 1764 | 1765 | tmpscanl = list_def(_mem.slDef, SCANLIST_SELECT_LIST, 0) 1766 | val = RadioSettingValueList(SCANLIST_SELECT_LIST, None, tmpscanl) 1767 | rs = RadioSetting("slDef", "Default scanlist (SList)", val) 1768 | scanl.append(rs) 1769 | 1770 | val = RadioSettingValueBoolean(_mem.sl1PriorEnab) 1771 | rs = RadioSetting("sl1PriorEnab", "List 1 priority channel scan", val) 1772 | scanl.append(rs) 1773 | 1774 | ch_list = ["None"] 1775 | for ch in range(1, 201): 1776 | ch_list.append("Channel M" + str(ch)) 1777 | 1778 | tmpch = list_def(_mem.sl1PriorCh1 + 1, ch_list, 0) 1779 | val = RadioSettingValueList(ch_list, None, tmpch) 1780 | rs = RadioSetting("sl1PriorCh1", "List 1 priority channel 1", val) 1781 | scanl.append(rs) 1782 | 1783 | tmpch = list_def(_mem.sl1PriorCh2 + 1, ch_list, 0) 1784 | val = RadioSettingValueList(ch_list, None, tmpch) 1785 | rs = RadioSetting("sl1PriorCh2", "List 1 priority channel 2", val) 1786 | scanl.append(rs) 1787 | 1788 | val = RadioSettingValueBoolean(_mem.sl2PriorEnab) 1789 | rs = RadioSetting("sl2PriorEnab", "List 2 priority channel scan", val) 1790 | scanl.append(rs) 1791 | 1792 | tmpch = list_def(_mem.sl2PriorCh1 + 1, ch_list, 0) 1793 | val = RadioSettingValueList(ch_list, None, tmpch) 1794 | rs = RadioSetting("sl2PriorCh1", "List 2 priority channel 1", val) 1795 | scanl.append(rs) 1796 | 1797 | tmpch = list_def(_mem.sl2PriorCh2 + 1, ch_list, 0) 1798 | val = RadioSettingValueList(ch_list, None, tmpch) 1799 | rs = RadioSetting("sl2PriorCh2", "List 2 priority channel 2", val) 1800 | scanl.append(rs) 1801 | 1802 | # ----------------- Basic settings 1803 | 1804 | ch_list = [] 1805 | for ch in range(1, 201): 1806 | ch_list.append("Channel M" + str(ch)) 1807 | for bnd in range(1, 8): 1808 | ch_list.append("Band F" + str(bnd)) 1809 | if _mem.BUILD_OPTIONS.ENABLE_NOAA: 1810 | for bnd in range(1, 11): 1811 | ch_list.append("NOAA N" + str(bnd)) 1812 | 1813 | tmpfreq0 = list_def(_mem.ScreenChannel_A, ch_list, 0) 1814 | val = RadioSettingValueList(ch_list, None, tmpfreq0) 1815 | freq0_setting = RadioSetting("VFO_A_chn", 1816 | "VFO A current channel/band", val) 1817 | 1818 | tmpfreq1 = list_def(_mem.ScreenChannel_B, ch_list, 0) 1819 | val = RadioSettingValueList(ch_list, None, tmpfreq1) 1820 | freq1_setting = RadioSetting("VFO_B_chn", 1821 | "VFO B current channel/band", val) 1822 | 1823 | tmptxvfo = list_def(_mem.TX_VFO, TX_VFO_LIST, 0) 1824 | val = RadioSettingValueList(TX_VFO_LIST, None, tmptxvfo) 1825 | tx_vfo_setting = RadioSetting("TX_VFO", "Main VFO", val) 1826 | 1827 | tmpsq = min_max_def(_mem.squelch, 0, 9, 1) 1828 | val = RadioSettingValueInteger(0, 9, tmpsq) 1829 | squelch_setting = RadioSetting("squelch", "Squelch (Sql)", val) 1830 | 1831 | ch_list = [] 1832 | for ch in range(1, 201): 1833 | ch_list.append("Channel M" + str(ch)) 1834 | 1835 | tmpc = list_def(_mem.call_channel, ch_list, 0) 1836 | val = RadioSettingValueList(ch_list, None, tmpc) 1837 | call_channel_setting = RadioSetting("call_channel", 1838 | "One key call channel (1 Call)", 1839 | val) 1840 | 1841 | val = RadioSettingValueBoolean(_mem.key_lock) 1842 | keypad_cock_setting = RadioSetting("key_lock", "Keypad locked", val) 1843 | 1844 | val = RadioSettingValueBoolean(_mem.auto_keypad_lock) 1845 | auto_keypad_lock_setting = \ 1846 | RadioSetting("auto_keypad_lock", 1847 | "Auto keypad lock (KeyLck)", val) 1848 | 1849 | tmptot = list_def(_mem.max_talk_time, TALK_TIME_LIST, 1) 1850 | val = RadioSettingValueList(TALK_TIME_LIST, None, tmptot) 1851 | tx_t_out_setting = RadioSetting("tot", 1852 | "Max talk, TX Time Out (TxTOut)", val) 1853 | 1854 | tmpbatsave = list_def(_mem.battery_save, BATSAVE_LIST, 4) 1855 | val = RadioSettingValueList(BATSAVE_LIST, None, tmpbatsave) 1856 | bat_save_setting = RadioSetting("battery_save", 1857 | "Battery save (BatSav)", val) 1858 | 1859 | val = RadioSettingValueBoolean(_mem.noaa_autoscan) 1860 | noaa_auto_scan_setting = RadioSetting("noaa_autoscan", 1861 | "NOAA Autoscan (NOAA-S)", val) 1862 | 1863 | tmpmicgain = list_def(_mem.mic_gain, MIC_GAIN_LIST, 2) 1864 | val = RadioSettingValueList(MIC_GAIN_LIST, None, tmpmicgain) 1865 | mic_gain_setting = RadioSetting("mic_gain", "Mic Gain (Mic)", val) 1866 | 1867 | val = RadioSettingValueBoolean(_mem.mic_bar) 1868 | mic_bar_setting = RadioSetting("mic_bar", 1869 | "Microphone Bar display (MicBar)", val) 1870 | 1871 | tmpchdispmode = list_def(_mem.channel_display_mode, 1872 | CHANNELDISP_LIST, 0) 1873 | val = RadioSettingValueList(CHANNELDISP_LIST, None, tmpchdispmode) 1874 | ch_disp_setting = RadioSetting("channel_display_mode", 1875 | "Channel display mode (ChDisp)", val) 1876 | 1877 | tmpdispmode = list_def(_mem.power_on_dispmode, WELCOME_LIST, 0) 1878 | val = RadioSettingValueList(WELCOME_LIST, None, tmpdispmode) 1879 | p_on_msg_setting = RadioSetting("welcome_mode", 1880 | "Power ON display message (POnMsg)", 1881 | val) 1882 | 1883 | logo1 = str(_mem.logo_line1).strip("\x20\x00\xff") + "\x00" 1884 | logo1 = _getstring(logo1.encode('ascii', errors='ignore'), 0, 12) 1885 | val = RadioSettingValueString(0, 12, logo1) 1886 | logo1_setting = RadioSetting("logo1", 1887 | "Message line 1", 1888 | val) 1889 | 1890 | logo2 = str(_mem.logo_line2).strip("\x20\x00\xff") + "\x00" 1891 | logo2 = _getstring(logo2.encode('ascii', errors='ignore'), 0, 12) 1892 | val = RadioSettingValueString(0, 12, logo2) 1893 | logo2_setting = RadioSetting("logo2", 1894 | "Message line 2", 1895 | val) 1896 | 1897 | tmpbattxt = list_def(_mem.battery_text, BAT_TXT_LIST, 2) 1898 | val = RadioSettingValueList(BAT_TXT_LIST, None, tmpbattxt) 1899 | bat_txt_setting = RadioSetting("battery_text", 1900 | "Battery Level Display (BatTXT)", val) 1901 | 1902 | tmpback = list_def(_mem.backlight_time, BACKLIGHT_LIST, 0) 1903 | val = RadioSettingValueList(BACKLIGHT_LIST, None, tmpback) 1904 | back_lt_setting = RadioSetting("backlight_time", 1905 | "Backlight time (BackLt)", val) 1906 | 1907 | tmpback = list_def(_mem.backlight_min, BACKLIGHT_LVL_LIST, 0) 1908 | val = RadioSettingValueList(BACKLIGHT_LVL_LIST, None, tmpback) 1909 | bl_min_setting = RadioSetting("backlight_min", 1910 | "Backlight level min (BLMin)", val) 1911 | 1912 | tmpback = list_def(_mem.backlight_max, BACKLIGHT_LVL_LIST, 10) 1913 | val = RadioSettingValueList(BACKLIGHT_LVL_LIST, None, tmpback) 1914 | bl_max_setting = RadioSetting("backlight_max", 1915 | "Backlight level max (BLMax)", val) 1916 | 1917 | tmpback = list_def(_mem.backlight_on_TX_RX, BACKLIGHT_TX_RX_LIST, 0) 1918 | val = RadioSettingValueList(BACKLIGHT_TX_RX_LIST, None, tmpback) 1919 | blt_trx_setting = RadioSetting("backlight_on_TX_RX", 1920 | "Backlight on TX/RX (BltTRX)", val) 1921 | 1922 | val = RadioSettingValueBoolean(_mem.button_beep) 1923 | beep_setting = RadioSetting("button_beep", 1924 | "Key press beep sound (Beep)", val) 1925 | 1926 | tmpalarmmode = list_def(_mem.roger_beep, ROGER_LIST, 0) 1927 | val = RadioSettingValueList(ROGER_LIST, None, tmpalarmmode) 1928 | roger_setting = RadioSetting("roger_beep", 1929 | "End of transmission beep (Roger)", val) 1930 | 1931 | val = RadioSettingValueBoolean(_mem.ste) 1932 | ste_setting = RadioSetting("ste", "Squelch tail elimination (STE)", 1933 | val) 1934 | 1935 | tmprte = list_def(_mem.rp_ste, RTE_LIST, 0) 1936 | val = RadioSettingValueList(RTE_LIST, None, tmprte) 1937 | rp_ste_setting = \ 1938 | RadioSetting("rp_ste", 1939 | "Repeater squelch tail elimination (RP STE)", val) 1940 | 1941 | val = RadioSettingValueBoolean(_mem.AM_fix) 1942 | am_fix_setting = RadioSetting("AM_fix", 1943 | "AM reception fix (AM Fix)", val) 1944 | 1945 | tmpvox = min_max_def((_mem.vox_level + 1) * _mem.vox_switch, 0, 10, 0) 1946 | val = RadioSettingValueList(VOX_LIST, None, tmpvox) 1947 | vox_setting = RadioSetting("vox", "Voice-operated switch (VOX)", val) 1948 | 1949 | tmprxmode = list_def((bool(_mem.crossband) << 1) 1950 | + bool(_mem.dual_watch), 1951 | RXMODE_LIST, 0) 1952 | val = RadioSettingValueList(RXMODE_LIST, None, tmprxmode) 1953 | rx_mode_setting = RadioSetting("rx_mode", "RX Mode (RxMode)", val) 1954 | 1955 | val = RadioSettingValueBoolean(_mem.freq_mode_allowed) 1956 | freq_mode_allowed_setting = RadioSetting("freq_mode_allowed", 1957 | "Frequency mode allowed", val) 1958 | 1959 | tmpscanres = list_def(_mem.scan_resume_mode, SCANRESUME_LIST, 0) 1960 | val = RadioSettingValueList(SCANRESUME_LIST, None, tmpscanres) 1961 | scn_rev_setting = RadioSetting("scan_resume_mode", 1962 | "Scan resume mode (ScnRev)", val) 1963 | 1964 | tmpvoice = list_def(_mem.voice, VOICE_LIST, 0) 1965 | val = RadioSettingValueList(VOICE_LIST, None, tmpvoice) 1966 | voice_setting = RadioSetting("voice", "Voice", val) 1967 | 1968 | tmpalarmmode = list_def(_mem.alarm_mode, ALARMMODE_LIST, 0) 1969 | val = RadioSettingValueList(ALARMMODE_LIST, None, tmpalarmmode) 1970 | alarm_setting = RadioSetting("alarm_mode", "Alarm mode", val) 1971 | 1972 | # ----------------- Extra settings 1973 | 1974 | # S-meter 1975 | tmp_s0 = -int(_mem.s0_level) 1976 | tmp_s9 = -int(_mem.s9_level) 1977 | 1978 | if tmp_s0 not in range(-200, -91) or tmp_s9 not in range(-160, -51) \ 1979 | or tmp_s9 < tmp_s0+9: 1980 | 1981 | tmp_s0 = -130 1982 | tmp_s9 = -76 1983 | val = RadioSettingValueInteger(-200, -90, tmp_s0) 1984 | s0_level_setting = RadioSetting("s0_level", 1985 | "S-meter S0 level [dBm]", val) 1986 | 1987 | val = RadioSettingValueInteger(-160, -50, tmp_s9) 1988 | s9_level_setting = RadioSetting("s9_level", 1989 | "S-meter S9 level [dBm]", val) 1990 | 1991 | # Battery Type 1992 | tmpbtype = list_def(_mem.Battery_type, BATTYPE_LIST, 0) 1993 | val = RadioSettingValueList(BATTYPE_LIST, BATTYPE_LIST[tmpbtype]) 1994 | bat_type_setting = RadioSetting("Battery_type", 1995 | "Battery Type (BatTyp)", val) 1996 | 1997 | # Power on password 1998 | def validate_password(value): 1999 | value = value.strip(" ") 2000 | if value.isdigit(): 2001 | return value.zfill(6) 2002 | if value != "": 2003 | raise InvalidValueError("Power on password " 2004 | "can only have digits") 2005 | return "" 2006 | 2007 | pswd_str = str(int(_mem.password)).zfill(6) \ 2008 | if _mem.password < 1000000 else "" 2009 | val = RadioSettingValueString(0, 6, pswd_str) 2010 | val.set_validate_callback(validate_password) 2011 | pswd_setting = RadioSetting("password", "Power on password", val) 2012 | 2013 | # ----------------- FM radio 2014 | 2015 | append_label(fmradio, "Channel", "Frequency [MHz]") 2016 | 2017 | for i in range(1, 21): 2018 | fmfreq = _mem.fmfreq[i-1]/10.0 2019 | freq_name = str(fmfreq) 2020 | if fmfreq < FMMIN or fmfreq > FMMAX: 2021 | freq_name = "" 2022 | rs = RadioSetting("FM_" + str(i), "Ch " + str(i), 2023 | RadioSettingValueString(0, 5, freq_name)) 2024 | fmradio.append(rs) 2025 | 2026 | # ----------------- Unlock settings 2027 | 2028 | # F-LOCK 2029 | def validate_int_flock(value): 2030 | mem_val = self._memobj.int_flock 2031 | if mem_val != 7 and value == FLOCK_LIST[7]: 2032 | msg = "\"" + value + "\" can only be enabled from radio menu" 2033 | raise InvalidValueError(msg) 2034 | return value 2035 | 2036 | tmpflock = list_def(_mem.int_flock, FLOCK_LIST, 0) 2037 | val = RadioSettingValueList(FLOCK_LIST, None, tmpflock) 2038 | val.set_validate_callback(validate_int_flock) 2039 | f_lock_setting = RadioSetting("int_flock", 2040 | "TX Frequency Lock (F Lock)", val) 2041 | 2042 | val = RadioSettingValueBoolean(_mem.int_200tx) 2043 | tx200_setting = RadioSetting("int_200tx", 2044 | "Unlock 174-350MHz TX (Tx 200)", val) 2045 | 2046 | val = RadioSettingValueBoolean(_mem.int_350tx) 2047 | tx350_setting = RadioSetting("int_350tx", 2048 | "Unlock 350-400MHz TX (Tx 350)", val) 2049 | 2050 | val = RadioSettingValueBoolean(_mem.int_500tx) 2051 | tx500_setting = RadioSetting("int_500tx", 2052 | "Unlock 500-600MHz TX (Tx 500)", val) 2053 | 2054 | val = RadioSettingValueBoolean(_mem.int_350en) 2055 | en350_setting = RadioSetting("int_350en", 2056 | "Unlock 350-400MHz RX (350 En)", val) 2057 | 2058 | val = RadioSettingValueBoolean(_mem.int_scren) 2059 | en_scrambler_setting = RadioSetting("int_scren", 2060 | "Scrambler enabled (ScraEn)", val) 2061 | 2062 | # ----------------- Driver Info 2063 | 2064 | if self.FIRMWARE_VERSION == "": 2065 | firmware = "To get the firmware version please download" \ 2066 | "the image from the radio first" 2067 | else: 2068 | firmware = self.FIRMWARE_VERSION 2069 | 2070 | append_label(roinfo, "Firmware Version", firmware) 2071 | append_label(roinfo, "Driver version", DRIVER_VERSION) 2072 | 2073 | # ----------------- Calibration 2074 | 2075 | val = RadioSettingValueBoolean(False) 2076 | 2077 | def validate_upload_calibration(value): 2078 | if value and not self.upload_calibration: 2079 | msg = "This option may brake your radio!!!\n" \ 2080 | "You are doing this at your own risk.\n" \ 2081 | "Make sure you have a working calibration backup.\n" \ 2082 | "Don't use it unless you know what you're doing." 2083 | ret = wx.MessageBox(msg, "Warning", wx.OK | wx.CANCEL | 2084 | wx.CANCEL_DEFAULT | wx.ICON_WARNING) 2085 | value = ret == wx.OK 2086 | self.upload_calibration = value 2087 | return value 2088 | 2089 | val.set_validate_callback(validate_upload_calibration) 2090 | radio_setting = RadioSetting("upload_calibration", 2091 | "Upload calibration", val) 2092 | calibration.append(radio_setting) 2093 | 2094 | radio_setting_group = RadioSettingGroup("squelch_calibration", 2095 | "Squelch") 2096 | calibration.append(radio_setting_group) 2097 | 2098 | bands = {"sqlBand1_3": "Frequency Band 1-3", 2099 | "sqlBand4_7": "Frequency Band 4-7"} 2100 | for bnd, bndn in bands.items(): 2101 | append_label(radio_setting_group, 2102 | "=" * 6 + " " + bndn + " " + "=" * 300, "=" * 300) 2103 | for sql in range(0, 10): 2104 | prefix = "_mem.cal." + bnd + "." 2105 | postfix = "[" + str(sql) + "]" 2106 | append_label(radio_setting_group, "Squelch " + str(sql)) 2107 | 2108 | name = prefix + "openRssiThr" + postfix 2109 | tempval = min_max_def(eval(name), 0, 255, 0) 2110 | val = RadioSettingValueInteger(0, 255, tempval) 2111 | radio_setting = RadioSetting(name, "RSSI threshold open", val) 2112 | radio_setting_group.append(radio_setting) 2113 | 2114 | name = prefix + "closeRssiThr" + postfix 2115 | tempval = min_max_def(eval(name), 0, 255, 0) 2116 | val = RadioSettingValueInteger(0, 255, tempval) 2117 | radio_setting = RadioSetting(name, "RSSI threshold close", val) 2118 | radio_setting_group.append(radio_setting) 2119 | 2120 | name = prefix + "openNoiseThr" + postfix 2121 | tempval = min_max_def(eval(name), 0, 127, 0) 2122 | val = RadioSettingValueInteger(0, 127, tempval) 2123 | radio_setting = RadioSetting(name, "Noise threshold open", val) 2124 | radio_setting_group.append(radio_setting) 2125 | 2126 | name = prefix + "closeNoiseThr" + postfix 2127 | tempval = min_max_def(eval(name), 0, 127, 0) 2128 | val = RadioSettingValueInteger(0, 127, tempval) 2129 | radio_setting = RadioSetting(name, "Noise threshold close", 2130 | val) 2131 | radio_setting_group.append(radio_setting) 2132 | 2133 | name = prefix + "openGlitchThr" + postfix 2134 | tempval = min_max_def(eval(name), 0, 255, 0) 2135 | val = RadioSettingValueInteger(0, 255, tempval) 2136 | radio_setting = RadioSetting(name, "Glitch threshold open", 2137 | val) 2138 | radio_setting_group.append(radio_setting) 2139 | 2140 | name = prefix + "closeGlitchThr" + postfix 2141 | tempval = min_max_def(eval(name), 0, 255, 0) 2142 | val = RadioSettingValueInteger(0, 255, tempval) 2143 | radio_setting = RadioSetting(name, "Glitch threshold close", 2144 | val) 2145 | radio_setting_group.append(radio_setting) 2146 | 2147 | # 2148 | 2149 | radio_setting_group = RadioSettingGroup("rssi_level_calibration", 2150 | "RSSI levels") 2151 | calibration.append(radio_setting_group) 2152 | 2153 | bands = {"rssiLevelsBands1_2": "1-2 ", "rssiLevelsBands3_7": "3-7 "} 2154 | for bnd, bndn in bands.items(): 2155 | append_label(radio_setting_group, 2156 | "=" * 6 + 2157 | " RSSI levels for QS original small bar graph, bands " 2158 | + bndn + "=" * 300, "=" * 300) 2159 | for lvl in [1, 2, 4, 6]: 2160 | name = "_mem.cal." + bnd + ".level" + str(lvl) 2161 | tempval = min_max_def(eval(name), 0, 65535, 0) 2162 | val = RadioSettingValueInteger(0, 65535, tempval) 2163 | radio_setting = RadioSetting(name, "Level " + str(lvl), val) 2164 | radio_setting_group.append(radio_setting) 2165 | 2166 | # 2167 | 2168 | radio_setting_group = RadioSettingGroup("tx_power_calibration", 2169 | "TX power") 2170 | calibration.append(radio_setting_group) 2171 | 2172 | for bnd in range(0, 7): 2173 | append_label(radio_setting_group, "=" * 6 + " TX power band " 2174 | + str(bnd+1) + " " + "=" * 300, "=" * 300) 2175 | powers = {"low": "Low", "mid": "Medium", "hi": "High"} 2176 | for pwr, pwrn in powers.items(): 2177 | append_label(radio_setting_group, pwrn) 2178 | bounds = ["lower", "center", "upper"] 2179 | for bound in bounds: 2180 | name = f"_mem.cal.txp[{bnd}].{pwr}.{bound}" 2181 | tempval = min_max_def(eval(name), 0, 255, 0) 2182 | val = RadioSettingValueInteger(0, 255, tempval) 2183 | radio_setting = RadioSetting(name, bound.capitalize(), val) 2184 | radio_setting_group.append(radio_setting) 2185 | 2186 | # 2187 | 2188 | radio_setting_group = RadioSettingGroup("battery_calibration", 2189 | "Battery") 2190 | calibration.append(radio_setting_group) 2191 | 2192 | for lvl in range(0, 6): 2193 | name = "_mem.cal.batLvl[" + str(lvl) + "]" 2194 | temp_val = min_max_def(eval(name), 0, 4999, 4999) 2195 | val = RadioSettingValueInteger(0, 4999, temp_val) 2196 | radio_setting = \ 2197 | RadioSetting(name, "Level " + str(lvl) + 2198 | (" (voltage calibration)" if lvl == 3 else ""), 2199 | val) 2200 | radio_setting_group.append(radio_setting) 2201 | 2202 | radio_setting_group = RadioSettingGroup("vox_calibration", "VOX") 2203 | calibration.append(radio_setting_group) 2204 | 2205 | for lvl in range(0, 10): 2206 | append_label(radio_setting_group, "Level " + str(lvl + 1)) 2207 | 2208 | name = "_mem.cal.vox1Thr[" + str(lvl) + "]" 2209 | val = RadioSettingValueInteger(0, 65535, eval(name)) 2210 | radio_setting = RadioSetting(name, "On", val) 2211 | radio_setting_group.append(radio_setting) 2212 | 2213 | name = "_mem.cal.vox0Thr[" + str(lvl) + "]" 2214 | val = RadioSettingValueInteger(0, 65535, eval(name)) 2215 | radio_setting = RadioSetting(name, "Off", val) 2216 | radio_setting_group.append(radio_setting) 2217 | 2218 | radio_setting_group = RadioSettingGroup("mic_calibration", 2219 | "Microphone sensitivity") 2220 | calibration.append(radio_setting_group) 2221 | 2222 | for lvl in range(0, 5): 2223 | name = "_mem.cal.micLevel[" + str(lvl) + "]" 2224 | tempval = min_max_def(eval(name), 0, 31, 31) 2225 | val = RadioSettingValueInteger(0, 31, tempval) 2226 | radio_setting = RadioSetting(name, "Level " + str(lvl), val) 2227 | radio_setting_group.append(radio_setting) 2228 | 2229 | radio_setting_group = RadioSettingGroup("other_calibration", "Other") 2230 | calibration.append(radio_setting_group) 2231 | 2232 | name = "_mem.cal.xtalFreqLow" 2233 | temp_val = min_max_def(eval(name), -1000, 1000, 0) 2234 | val = RadioSettingValueInteger(-1000, 1000, temp_val) 2235 | radio_setting = RadioSetting(name, "Xtal frequecy low", val) 2236 | radio_setting_group.append(radio_setting) 2237 | 2238 | name = "_mem.cal.volumeGain" 2239 | temp_val = min_max_def(eval(name), 0, 63, 58) 2240 | val = RadioSettingValueInteger(0, 63, temp_val) 2241 | radio_setting = RadioSetting(name, "Volume gain", val) 2242 | radio_setting_group.append(radio_setting) 2243 | 2244 | name = "_mem.cal.dacGain" 2245 | temp_val = min_max_def(eval(name), 0, 15, 8) 2246 | val = RadioSettingValueInteger(0, 15, temp_val) 2247 | radio_setting = RadioSetting(name, "DAC gain", val) 2248 | radio_setting_group.append(radio_setting) 2249 | 2250 | # -------- LAYOUT 2251 | 2252 | basic.append(squelch_setting) 2253 | basic.append(rx_mode_setting) 2254 | basic.append(call_channel_setting) 2255 | basic.append(auto_keypad_lock_setting) 2256 | basic.append(tx_t_out_setting) 2257 | basic.append(bat_save_setting) 2258 | basic.append(scn_rev_setting) 2259 | if _mem.BUILD_OPTIONS.ENABLE_NOAA: 2260 | basic.append(noaa_auto_scan_setting) 2261 | if _mem.BUILD_OPTIONS.ENABLE_AM_FIX: 2262 | basic.append(am_fix_setting) 2263 | 2264 | append_label(basic, 2265 | "=" * 6 + " Display settings " + "=" * 300, "=" * 300) 2266 | 2267 | basic.append(bat_txt_setting) 2268 | basic.append(mic_bar_setting) 2269 | basic.append(ch_disp_setting) 2270 | basic.append(p_on_msg_setting) 2271 | basic.append(logo1_setting) 2272 | basic.append(logo2_setting) 2273 | 2274 | append_label(basic, "=" * 6 + " Backlight settings " 2275 | + "=" * 300, "=" * 300) 2276 | 2277 | basic.append(back_lt_setting) 2278 | basic.append(bl_min_setting) 2279 | basic.append(bl_max_setting) 2280 | basic.append(blt_trx_setting) 2281 | 2282 | append_label(basic, "=" * 6 + " Audio related settings " 2283 | + "=" * 300, "=" * 300) 2284 | 2285 | if _mem.BUILD_OPTIONS.ENABLE_VOX: 2286 | basic.append(vox_setting) 2287 | basic.append(mic_gain_setting) 2288 | basic.append(beep_setting) 2289 | basic.append(roger_setting) 2290 | basic.append(ste_setting) 2291 | basic.append(rp_ste_setting) 2292 | if _mem.BUILD_OPTIONS.ENABLE_VOICE: 2293 | basic.append(voice_setting) 2294 | if _mem.BUILD_OPTIONS.ENABLE_ALARM: 2295 | basic.append(alarm_setting) 2296 | 2297 | append_label(basic, "=" * 6 + " Radio state " + "=" * 300, "=" * 300) 2298 | 2299 | basic.append(freq0_setting) 2300 | basic.append(freq1_setting) 2301 | basic.append(tx_vfo_setting) 2302 | basic.append(keypad_cock_setting) 2303 | 2304 | advanced.append(freq_mode_allowed_setting) 2305 | advanced.append(bat_type_setting) 2306 | advanced.append(s0_level_setting) 2307 | advanced.append(s9_level_setting) 2308 | if _mem.BUILD_OPTIONS.ENABLE_PWRON_PASSWORD: 2309 | advanced.append(pswd_setting) 2310 | 2311 | if _mem.BUILD_OPTIONS.ENABLE_DTMF_CALLING: 2312 | dtmf.append(sep_code_setting) 2313 | dtmf.append(group_code_setting) 2314 | dtmf.append(first_code_per_setting) 2315 | dtmf.append(spec_per_setting) 2316 | dtmf.append(code_per_setting) 2317 | dtmf.append(code_int_setting) 2318 | if _mem.BUILD_OPTIONS.ENABLE_DTMF_CALLING: 2319 | dtmf.append(ani_id_setting) 2320 | dtmf.append(up_code_setting) 2321 | dtmf.append(dw_code_setting) 2322 | dtmf.append(d_prel_setting) 2323 | dtmf.append(dtmf_side_tone_setting) 2324 | if _mem.BUILD_OPTIONS.ENABLE_DTMF_CALLING: 2325 | dtmf.append(dtmf_resp_setting) 2326 | dtmf.append(d_hold_setting) 2327 | dtmf.append(d_live_setting) 2328 | dtmf.append(perm_kill_setting) 2329 | dtmf.append(kill_code_setting) 2330 | dtmf.append(rev_code_setting) 2331 | dtmf.append(killed_setting) 2332 | 2333 | unlock.append(f_lock_setting) 2334 | unlock.append(tx200_setting) 2335 | unlock.append(tx350_setting) 2336 | unlock.append(tx500_setting) 2337 | unlock.append(en350_setting) 2338 | unlock.append(en_scrambler_setting) 2339 | 2340 | return top 2341 | 2342 | def set_memory(self, memory): 2343 | """ 2344 | Store details about a high-level memory to the memory map 2345 | This is called when a user edits a memory in the UI 2346 | """ 2347 | number = memory.number-1 2348 | att_num = number if number < 200 else 200 + int((number - 200) / 2) 2349 | 2350 | # Get a low-level memory object mapped to the image 2351 | _mem_chan = self._memobj.channel[number] 2352 | _mem_attr = self._memobj.ch_attr[att_num] 2353 | 2354 | _mem_attr.is_scanlist1 = 0 2355 | _mem_attr.is_scanlist2 = 0 2356 | _mem_attr.compander = 0 2357 | _mem_attr.is_free = 1 2358 | _mem_attr.band = 0x7 2359 | 2360 | # empty memory 2361 | if memory.empty: 2362 | _mem_chan.set_raw("\xFF" * 16) 2363 | if number < 200: 2364 | _mem_chname = self._memobj.channelname[number] 2365 | _mem_chname.set_raw("\xFF" * 16) 2366 | return memory 2367 | 2368 | # find band 2369 | band = self._find_band(memory.freq) 2370 | 2371 | # mode 2372 | tmp_mode = self.get_features().valid_modes.index(memory.mode) 2373 | _mem_chan.modulation = tmp_mode / 2 2374 | _mem_chan.bandwidth = tmp_mode % 2 2375 | if memory.mode == "USB": 2376 | _mem_chan.bandwidth = 1 # narrow 2377 | 2378 | # frequency/offset 2379 | _mem_chan.freq = memory.freq/10 2380 | _mem_chan.offset = memory.offset/10 2381 | 2382 | if memory.duplex == "": 2383 | _mem_chan.offset = 0 2384 | _mem_chan.offsetDir = 0 2385 | elif memory.duplex == '-': 2386 | _mem_chan.offsetDir = FLAGS1_OFFSET_MINUS 2387 | elif memory.duplex == '+': 2388 | _mem_chan.offsetDir = FLAGS1_OFFSET_PLUS 2389 | elif memory.duplex == 'off': 2390 | # we fake tx disable by setting the tx freq to 0 MHz 2391 | _mem_chan.offsetDir = FLAGS1_OFFSET_MINUS 2392 | _mem_chan.offset = _mem_chan.freq 2393 | # set band 2394 | 2395 | _mem_attr.is_free = 0 2396 | _mem_attr.band = band 2397 | 2398 | # channels >200 are the 14 VFO chanells and don't have names 2399 | if number < 200: 2400 | _mem_chname = self._memobj.channelname[number] 2401 | tag = memory.name.ljust(10) + "\x00"*6 2402 | _mem_chname.name = tag # Store the alpha tag 2403 | 2404 | # tone data 2405 | self._set_tone(memory, _mem_chan) 2406 | 2407 | # step 2408 | _mem_chan.step = STEPS.index(memory.tuning_step) 2409 | 2410 | # tx power 2411 | if str(memory.power) == str(UVK5_POWER_LEVELS[2]): 2412 | _mem_chan.txpower = POWER_HIGH 2413 | elif str(memory.power) == str(UVK5_POWER_LEVELS[1]): 2414 | _mem_chan.txpower = POWER_MEDIUM 2415 | else: 2416 | _mem_chan.txpower = POWER_LOW 2417 | 2418 | # -------- EXTRA SETTINGS 2419 | 2420 | def get_setting(name, def_val): 2421 | if name in memory.extra: 2422 | return int(memory.extra[name].value) 2423 | return def_val 2424 | 2425 | _mem_chan.busyChLockout = get_setting("busyChLockout", False) 2426 | _mem_chan.dtmf_pttid = get_setting("pttid", 0) 2427 | _mem_chan.freq_reverse = get_setting("frev", False) 2428 | _mem_chan.dtmf_decode = get_setting("dtmfdecode", False) 2429 | _mem_chan.scrambler = get_setting("scrambler", 0) 2430 | _mem_attr.compander = get_setting("compander", 0) 2431 | if number < 200: 2432 | tmp_val = get_setting("scanlists", 0) 2433 | _mem_attr.is_scanlist1 = bool(tmp_val & 1) 2434 | _mem_attr.is_scanlist2 = bool(tmp_val & 2) 2435 | 2436 | return memory 2437 | -------------------------------------------------------------------------------- /uvk5_egzumer.py: -------------------------------------------------------------------------------- 1 | # Quansheng UV-K5 driver (c) 2023 Jacek Lipkowski 2 | # Adapted For UV-K5 EGZUMER custom software By EGZUMER, JOC2 3 | # 4 | # based on template.py Copyright 2012 Dan Smith 5 | # 6 | # 7 | # This is a preliminary version of a driver for the UV-K5 8 | # It is based on my reverse engineering effort described here: 9 | # https://github.com/sq5bpf/uvk5-reverse-engineering 10 | # 11 | # Warning: this driver is experimental, it may brick your radio, 12 | # eat your lunch and mess up your configuration. 13 | # 14 | # 15 | # This program is free software: you can redistribute it and/or modify 16 | # it under the terms of the GNU General Public License as published by 17 | # the Free Software Foundation, either version 2 of the License, or 18 | # (at your option) any later version. 19 | # 20 | # This program is distributed in the hope that it will be useful, 21 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 22 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 23 | # GNU General Public License for more details. 24 | # 25 | # You should have received a copy of the GNU General Public License 26 | # along with this program. If not, see . 27 | 28 | 29 | import struct 30 | import logging 31 | import wx 32 | 33 | from chirp import chirp_common, directory, bitwise, memmap, errors, util 34 | from chirp.settings import RadioSetting, RadioSettingGroup, \ 35 | RadioSettingValueBoolean, RadioSettingValueList, \ 36 | RadioSettingValueInteger, RadioSettingValueString, \ 37 | RadioSettings, InvalidValueError 38 | 39 | LOG = logging.getLogger(__name__) 40 | 41 | # Show the obfuscated version of commands. Not needed normally, but 42 | # might be useful for someone who is debugging a similar radio 43 | DEBUG_SHOW_OBFUSCATED_COMMANDS = False 44 | 45 | # Show the memory being written/received. Not needed normally, because 46 | # this is the same information as in the packet hexdumps, but 47 | # might be useful for someone debugging some obscure memory issue 48 | DEBUG_SHOW_MEMORY_ACTIONS = False 49 | 50 | # TODO: remove the driver version when it's in mainline chirp 51 | DRIVER_VERSION = "Quansheng UV-K5/K6/5R driver (c) egzumer" 52 | VALEUR_COMPILER = "ENABLE" 53 | 54 | MEM_FORMAT = """ 55 | //#seekto 0x0000; 56 | struct { 57 | ul32 freq; 58 | ul32 offset; 59 | 60 | // 0x08 61 | u8 rxcode; 62 | u8 txcode; 63 | 64 | // 0x0A 65 | u8 txcodeflag:4, 66 | rxcodeflag:4; 67 | 68 | // 0x0B 69 | u8 modulation:4, 70 | offsetDir:4; 71 | 72 | // 0x0C 73 | u8 __UNUSED1:3, 74 | busyChLockout:1, 75 | txpower:2, 76 | bandwidth:1, 77 | freq_reverse:1; 78 | 79 | // 0x0D 80 | u8 __UNUSED2:4, 81 | dtmf_pttid:3, 82 | dtmf_decode:1; 83 | 84 | // 0x0E 85 | u8 step; 86 | u8 scrambler; 87 | 88 | } channel[214]; 89 | 90 | //#seekto 0xd60; 91 | struct { 92 | u8 is_scanlist1:1, 93 | is_scanlist2:1, 94 | compander:2, 95 | is_free:1, 96 | band:3; 97 | } ch_attr[200]; 98 | 99 | #seekto 0xe40; 100 | ul16 fmfreq[20]; 101 | 102 | #seekto 0xe70; 103 | u8 call_channel; 104 | u8 squelch; 105 | u8 max_talk_time; 106 | u8 noaa_autoscan; 107 | u8 key_lock; 108 | u8 vox_switch; 109 | u8 vox_level; 110 | u8 mic_gain; 111 | 112 | 113 | u8 backlight_min:4, 114 | backlight_max:4; 115 | 116 | u8 channel_display_mode; 117 | u8 crossband; 118 | u8 battery_save; 119 | u8 dual_watch; 120 | u8 backlight_time; 121 | u8 ste; 122 | u8 freq_mode_allowed; 123 | 124 | #seekto 0xe80; 125 | u8 ScreenChannel_A; 126 | u8 MrChannel_A; 127 | u8 FreqChannel_A; 128 | u8 ScreenChannel_B; 129 | u8 MrChannel_B; 130 | u8 FreqChannel_B; 131 | u8 NoaaChannel_A; 132 | u8 NoaaChannel_B; 133 | 134 | #seekto 0xe90; 135 | 136 | u8 keyM_longpress_action:7, 137 | button_beep:1; 138 | 139 | u8 key1_shortpress_action; 140 | u8 key1_longpress_action; 141 | u8 key2_shortpress_action; 142 | u8 key2_longpress_action; 143 | u8 scan_resume_mode; 144 | u8 auto_keypad_lock; 145 | u8 power_on_dispmode; 146 | ul32 password; 147 | 148 | #seekto 0xea0; 149 | u8 voice; 150 | u8 s0_level; 151 | u8 s9_level; 152 | 153 | #seekto 0xea8; 154 | u8 alarm_mode; 155 | u8 roger_beep; 156 | u8 rp_ste; 157 | u8 TX_VFO; 158 | u8 Battery_type; 159 | 160 | #seekto 0xeb0; 161 | char logo_line1[16]; 162 | char logo_line2[16]; 163 | 164 | //#seekto 0xed0; 165 | struct { 166 | u8 side_tone; 167 | char separate_code; 168 | char group_call_code; 169 | u8 decode_response; 170 | u8 auto_reset_time; 171 | u8 preload_time; 172 | u8 first_code_persist_time; 173 | u8 hash_persist_time; 174 | u8 code_persist_time; 175 | u8 code_interval_time; 176 | u8 permit_remote_kill; 177 | 178 | #seekto 0xee0; 179 | char local_code[3]; 180 | #seek 5; 181 | char kill_code[5]; 182 | #seek 3; 183 | char revive_code[5]; 184 | #seek 3; 185 | char up_code[16]; 186 | char down_code[16]; 187 | } dtmf; 188 | 189 | //#seekto 0xf18; 190 | u8 slDef; 191 | u8 sl1PriorEnab; 192 | u8 sl1PriorCh1; 193 | u8 sl1PriorCh2; 194 | u8 sl2PriorEnab; 195 | u8 sl2PriorCh1; 196 | u8 sl2PriorCh2; 197 | 198 | #seekto 0xf40; 199 | u8 int_flock; 200 | u8 int_350tx; 201 | u8 int_KILLED; 202 | u8 int_200tx; 203 | u8 int_500tx; 204 | u8 int_350en; 205 | u8 int_scren; 206 | 207 | 208 | u8 backlight_on_TX_RX:2, 209 | AM_fix:1, 210 | mic_bar:1, 211 | battery_text:2, 212 | live_DTMF_decoder:1, 213 | unknown:1; 214 | 215 | 216 | #seekto 0xf50; 217 | struct { 218 | char name[16]; 219 | } channelname[200]; 220 | 221 | #seekto 0x1c00; 222 | struct { 223 | char name[8]; 224 | char number[3]; 225 | #seek 5; 226 | } dtmfcontact[16]; 227 | 228 | struct { 229 | struct { 230 | #seekto 0x1E00; 231 | u8 openRssiThr[10]; 232 | #seekto 0x1E10; 233 | u8 closeRssiThr[10]; 234 | #seekto 0x1E20; 235 | u8 openNoiseThr[10]; 236 | #seekto 0x1E30; 237 | u8 closeNoiseThr[10]; 238 | #seekto 0x1E40; 239 | u8 closeGlitchThr[10]; 240 | #seekto 0x1E50; 241 | u8 openGlitchThr[10]; 242 | } sqlBand4_7; 243 | 244 | struct { 245 | #seekto 0x1E60; 246 | u8 openRssiThr[10]; 247 | #seekto 0x1E70; 248 | u8 closeRssiThr[10]; 249 | #seekto 0x1E80; 250 | u8 openNoiseThr[10]; 251 | #seekto 0x1E90; 252 | u8 closeNoiseThr[10]; 253 | #seekto 0x1EA0; 254 | u8 closeGlitchThr[10]; 255 | #seekto 0x1EB0; 256 | u8 openGlitchThr[10]; 257 | } sqlBand1_3; 258 | 259 | #seekto 0x1EC0; 260 | struct { 261 | ul16 level1; 262 | ul16 level2; 263 | ul16 level4; 264 | ul16 level6; 265 | } rssiLevelsBands3_7; 266 | 267 | struct { 268 | ul16 level1; 269 | ul16 level2; 270 | ul16 level4; 271 | ul16 level6; 272 | } rssiLevelsBands1_2; 273 | 274 | struct { 275 | struct { 276 | u8 lower; 277 | u8 center; 278 | u8 upper; 279 | } low; 280 | struct { 281 | u8 lower; 282 | u8 center; 283 | u8 upper; 284 | } mid; 285 | struct { 286 | u8 lower; 287 | u8 center; 288 | u8 upper; 289 | } hi; 290 | #seek 7; 291 | } txp[7]; 292 | 293 | #seekto 0x1F40; 294 | ul16 batLvl[6]; 295 | 296 | #seekto 0x1F50; 297 | ul16 vox1Thr[10]; 298 | 299 | #seekto 0x1F68; 300 | ul16 vox0Thr[10]; 301 | 302 | #seekto 0x1F80; 303 | u8 micLevel[5]; 304 | 305 | #seekto 0x1F88; 306 | il16 xtalFreqLow; 307 | 308 | #seekto 0x1F8E; 309 | u8 volumeGain; 310 | u8 dacGain; 311 | } cal; 312 | 313 | 314 | #seekto 0x1FF0; 315 | struct { 316 | u8 ENABLE_DTMF_CALLING:1, 317 | ENABLE_PWRON_PASSWORD:1, 318 | ENABLE_TX1750:1, 319 | ENABLE_ALARM:1, 320 | ENABLE_VOX:1, 321 | ENABLE_VOICE:1, 322 | ENABLE_NOAA:1, 323 | ENABLE_FMRADIO:1; 324 | u8 __UNUSED:3, 325 | ENABLE_AM_FIX:1, 326 | ENABLE_BLMIN_TMP_OFF:1, 327 | ENABLE_RAW_DEMODULATORS:1, 328 | ENABLE_WIDE_RX:1, 329 | ENABLE_FLASHLIGHT:1; 330 | } BUILD_OPTIONS; 331 | 332 | """ 333 | 334 | 335 | # flags1 336 | FLAGS1_OFFSET_NONE = 0b00 337 | FLAGS1_OFFSET_MINUS = 0b10 338 | FLAGS1_OFFSET_PLUS = 0b01 339 | 340 | POWER_HIGH = 0b10 341 | POWER_MEDIUM = 0b01 342 | POWER_LOW = 0b00 343 | 344 | # dtmf_flags 345 | PTTID_LIST = ["OFF", "UP CODE", "DOWN CODE", "UP+DOWN CODE", "APOLLO QUINDAR"] 346 | 347 | # power 348 | UVK5_POWER_LEVELS = [chirp_common.PowerLevel("Low", watts=1.50), 349 | chirp_common.PowerLevel("Med", watts=3.00), 350 | chirp_common.PowerLevel("High", watts=5.00), 351 | ] 352 | 353 | # scrambler 354 | SCRAMBLER_LIST = ["OFF", "2600Hz", "2700Hz", "2800Hz", "2900Hz", "3000Hz", 355 | "3100Hz", "3200Hz", "3300Hz", "3400Hz", "3500Hz"] 356 | # compander 357 | COMPANDER_LIST = ["OFF", "TX", "RX", "TX/RX"] 358 | # rx mode 359 | RXMODE_LIST = ["MAIN ONLY", "DUAL RX RESPOND", "CROSS BAND", "MAIN TX DUAL RX"] 360 | # channel display mode 361 | CHANNELDISP_LIST = ["Frequency", "Channel Number", "Name", "Name + Frequency"] 362 | 363 | # TalkTime 364 | TALK_TIME_LIST = ["30 sec", "1 min", "2 min", "3 min", "4 min", "5 min", 365 | "6 min", "7 min", "8 min", "9 min", "15 min"] 366 | 367 | # battery save 368 | BATSAVE_LIST = ["OFF", "1:1", "1:2", "1:3", "1:4"] 369 | 370 | # battery type 371 | BATTYPE_LIST = ["1600 mAh", "2200 mAh"] 372 | # bat txt 373 | BAT_TXT_LIST = ["NONE", "VOLTAGE", "PERCENT"] 374 | # Backlight auto mode 375 | BACKLIGHT_LIST = ["OFF", "5s", "10s", "20s", "1min", "2min", "4min", 376 | "Always On"] 377 | 378 | # Backlight LVL 379 | BACKLIGHT_LVL_LIST = ["0", "1", "2", "3", "4", "5", "6", "7", "8", "9", "10"] 380 | 381 | # Backlight _TX_RX_LIST 382 | BACKLIGHT_TX_RX_LIST = ["OFF", "TX", "RX", "TX/RX"] 383 | 384 | # steps TODO: change order 385 | STEPS = [2.5, 5, 6.25, 10, 12.5, 25, 8.33, 0.01, 0.05, 0.1, 0.25, 0.5, 1, 1.25, 386 | 9, 15, 20, 30, 50, 100, 125, 200, 250, 500] 387 | 388 | # ctcss/dcs codes 389 | TMODES = ["", "Tone", "DTCS", "DTCS"] 390 | TONE_NONE = 0 391 | TONE_CTCSS = 1 392 | TONE_DCS = 2 393 | TONE_RDCS = 3 394 | 395 | 396 | CTCSS_TONES = [ 397 | 67.0, 69.3, 71.9, 74.4, 77.0, 79.7, 82.5, 85.4, 398 | 88.5, 91.5, 94.8, 97.4, 100.0, 103.5, 107.2, 110.9, 399 | 114.8, 118.8, 123.0, 127.3, 131.8, 136.5, 141.3, 146.2, 400 | 151.4, 156.7, 159.8, 162.2, 165.5, 167.9, 171.3, 173.8, 401 | 177.3, 179.9, 183.5, 186.2, 189.9, 192.8, 196.6, 199.5, 402 | 203.5, 206.5, 210.7, 218.1, 225.7, 229.1, 233.6, 241.8, 403 | 250.3, 254.1 404 | ] 405 | 406 | # lifted from ft4.py 407 | DTCS_CODES = [ # TODO: add negative codes 408 | 23, 25, 26, 31, 32, 36, 43, 47, 51, 53, 54, 409 | 65, 71, 72, 73, 74, 114, 115, 116, 122, 125, 131, 410 | 132, 134, 143, 145, 152, 155, 156, 162, 165, 172, 174, 411 | 205, 212, 223, 225, 226, 243, 244, 245, 246, 251, 252, 412 | 255, 261, 263, 265, 266, 271, 274, 306, 311, 315, 325, 413 | 331, 332, 343, 346, 351, 356, 364, 365, 371, 411, 412, 414 | 413, 423, 431, 432, 445, 446, 452, 454, 455, 462, 464, 415 | 465, 466, 503, 506, 516, 523, 526, 532, 546, 565, 606, 416 | 612, 624, 627, 631, 632, 654, 662, 664, 703, 712, 723, 417 | 731, 732, 734, 743, 754 418 | ] 419 | 420 | # flock list extended 421 | FLOCK_LIST = ["DEFAULT+ (137-174, 400-470 + Tx200, Tx350, Tx500)", 422 | "FCC HAM (144-148, 420-450)", 423 | "CE HAM (144-146, 430-440)", 424 | "GB HAM (144-148, 430-440)", 425 | "137-174, 400-430", 426 | "137-174, 400-438", 427 | "Disable All", 428 | "Unlock All"] 429 | 430 | SCANRESUME_LIST = ["Listen 5 seconds and resume (TIMEOUT)", 431 | "Listen until signal dissapears (CARRIER)", 432 | "Stop scanning after receiving a signal (STOP)"] 433 | WELCOME_LIST = ["FULL", "MESSAGE", "VOLTAGE", "NONE"] 434 | VOICE_LIST = ["OFF", "Chinese", "English"] 435 | 436 | # ACTIVE CHANNEL 437 | TX_VFO_LIST = ["A", "B"] 438 | ALARMMODE_LIST = ["SITE", "TONE"] 439 | REMENDOFTALK_LIST = ["OFF", "ROGER", "MDC"] 440 | RTE_LIST = ["OFF", "100ms", "200ms", "300ms", "400ms", 441 | "500ms", "600ms", "700ms", "800ms", "900ms", "1000ms"] 442 | VOX_LIST = ["OFF", "1", "2", "3", "4", "5", "6", "7", "8", "9", "10"] 443 | 444 | MEM_SIZE = 0x2000 # size of all memory 445 | PROG_SIZE = 0x1d00 # size of the memory that we will write 446 | MEM_BLOCK = 0x80 # largest block of memory that we can reliably write 447 | CAL_START = 0x1E00 # calibration memory start address 448 | 449 | # fm radio supported frequencies 450 | FMMIN = 76.0 451 | FMMAX = 108.0 452 | 453 | # bands supported by the UV-K5 454 | BANDS_STANDARD = { 455 | 0: [ 50.0, 76.0], 456 | 1: [108.0, 136.9999], 457 | 2: [137.0, 173.9999], 458 | 3: [174.0, 349.9999], 459 | 4: [350.0, 399.9999], 460 | 5: [400.0, 469.9999], 461 | 6: [470.0, 600.0] 462 | } 463 | 464 | BANDS_WIDE = { 465 | 0: [ 18.0, 108.0], 466 | 1: [108.0, 136.9999], 467 | 2: [137.0, 173.9999], 468 | 3: [174.0, 349.9999], 469 | 4: [350.0, 399.9999], 470 | 5: [400.0, 469.9999], 471 | 6: [470.0, 1300.0] 472 | } 473 | 474 | SCANLIST_LIST = ["None", "List1", "List2", "Both"] 475 | SCANLIST_SELECT_LIST = ["LIST1", "LIST2", "ALL"] 476 | 477 | DTMF_CHARS = "0123456789ABCD*# " 478 | DTMF_CHARS_ID = "0123456789ABCDabcd" 479 | DTMF_CHARS_KILL = "0123456789ABCDabcd" 480 | DTMF_CHARS_UPDOWN = "0123456789ABCDabcd#* " 481 | DTMF_CODE_CHARS = "ABCD*# " 482 | DTMF_DECODE_RESPONSE_LIST = ["DO NOTHING", "RING", "REPLY", "BOTH"] 483 | 484 | KEYACTIONS_LIST = ["NONE", 485 | "FLASHLIGHT", 486 | "POWER", 487 | "MONITOR", 488 | "SCAN", 489 | "VOX", 490 | "ALARM", 491 | "FM RADIO", 492 | "1750Hz TONE", 493 | "LOCK KEYPAD", 494 | "Switch main VFO (SWITCH VFO)", 495 | "Switch frequency/memory mode (VFO/MR)", 496 | "Switch demodulation (SWITCH DEMODUL)" 497 | ] 498 | 499 | MIC_GAIN_LIST = ["+1.1dB","+4.0dB","+8.0dB","+12.0dB","+15.1dB"] 500 | 501 | def xorarr(data: bytes): 502 | """the communication is obfuscated using this fine mechanism""" 503 | tbl = [22, 108, 20, 230, 46, 145, 13, 64, 33, 53, 213, 64, 19, 3, 233, 128] 504 | ret = b"" 505 | idx = 0 506 | for byte in data: 507 | ret += bytes([byte ^ tbl[idx]]) 508 | idx = (idx+1) % len(tbl) 509 | return ret 510 | 511 | 512 | def calculate_crc16_xmodem(data: bytes): 513 | """ 514 | if this crc was used for communication to AND from the radio, then it 515 | would be a measure to increase reliability. 516 | but it's only used towards the radio, so it's for further obfuscation 517 | """ 518 | poly = 0x1021 519 | crc = 0x0 520 | for byte in data: 521 | crc = crc ^ (byte << 8) 522 | for _ in range(8): 523 | crc = crc << 1 524 | if crc & 0x10000: 525 | crc = (crc ^ poly) & 0xFFFF 526 | return crc & 0xFFFF 527 | 528 | 529 | def _send_command(serport, data: bytes): 530 | """Send a command to UV-K5 radio""" 531 | LOG.debug("Sending command (unobfuscated) len=0x%4.4x:\n%s", 532 | len(data), util.hexprint(data)) 533 | 534 | crc = calculate_crc16_xmodem(data) 535 | data2 = data + struct.pack("HBB", 0xabcd, len(data), 0) + \ 538 | xorarr(data2) + \ 539 | struct.pack(">H", 0xdcba) 540 | if DEBUG_SHOW_OBFUSCATED_COMMANDS: 541 | LOG.debug("Sending command (obfuscated):\n%s", util.hexprint(command)) 542 | try: 543 | result = serport.write(command) 544 | except Exception as e: 545 | raise errors.RadioError("Error writing data to radio") from e 546 | return result 547 | 548 | 549 | def _receive_reply(serport): 550 | header = serport.read(4) 551 | if len(header) != 4: 552 | LOG.warning("Header short read: [%s] len=%i", 553 | util.hexprint(header), len(header)) 554 | raise errors.RadioError("Header short read") 555 | if header[0] != 0xAB or header[1] != 0xCD or header[3] != 0x00: 556 | LOG.warning("Bad response header: %s len=%i", 557 | util.hexprint(header), len(header)) 558 | raise errors.RadioError("Bad response header") 559 | 560 | cmd = serport.read(int(header[2])) 561 | if len(cmd) != int(header[2]): 562 | LOG.warning("Body short read: [%s] len=%i", 563 | util.hexprint(cmd), len(cmd)) 564 | raise errors.RadioError("Command body short read") 565 | 566 | footer = serport.read(4) 567 | 568 | if len(footer) != 4: 569 | LOG.warning("Footer short read: [%s] len=%i", 570 | util.hexprint(footer), len(footer)) 571 | raise errors.RadioError("Footer short read") 572 | 573 | if footer[2] != 0xDC or footer[3] != 0xBA: 574 | LOG.debug("Reply before bad response footer (obfuscated)" 575 | "len=0x%4.4x:\n%s", len(cmd), util.hexprint(cmd)) 576 | LOG.warning("Bad response footer: %s len=%i", 577 | util.hexprint(footer), len(footer)) 578 | raise errors.RadioError("Bad response footer") 579 | 580 | if DEBUG_SHOW_OBFUSCATED_COMMANDS: 581 | LOG.debug("Received reply (obfuscated) len=0x%4.4x:\n%s", 582 | len(cmd), util.hexprint(cmd)) 583 | 584 | cmd2 = xorarr(cmd) 585 | 586 | LOG.debug("Received reply (unobfuscated) len=0x%4.4x:\n%s", 587 | len(cmd2), util.hexprint(cmd2)) 588 | 589 | return cmd2 590 | 591 | 592 | def _getstring(data: bytes, begin, maxlen): 593 | tmplen = min(maxlen+1, len(data)) 594 | ss = [data[i] for i in range(begin, tmplen)] 595 | key = 0 596 | for key, val in enumerate(ss): 597 | if val < ord(' ') or val > ord('~'): 598 | return ''.join(chr(x) for x in ss[0:key]) 599 | return '' 600 | 601 | 602 | def _sayhello(serport): 603 | hellopacket = b"\x14\x05\x04\x00\x6a\x39\x57\x64" 604 | 605 | tries = 5 606 | while True: 607 | LOG.debug("Sending hello packet") 608 | _send_command(serport, hellopacket) 609 | rep = _receive_reply(serport) 610 | if rep: 611 | break 612 | tries -= 1 613 | if tries == 0: 614 | LOG.warning("Failed to initialise radio") 615 | raise errors.RadioError("Failed to initialize radio") 616 | if rep.startswith(b'\x18\x05'): 617 | raise errors.RadioError("Radio is in programming mode, " 618 | "restart radio into normal mode") 619 | firmware = _getstring(rep, 4, 24) 620 | 621 | LOG.info("Found firmware: %s", firmware) 622 | return firmware 623 | 624 | 625 | def _readmem(serport, offset, length): 626 | LOG.debug("Sending readmem offset=0x%4.4x len=0x%4.4x", offset, length) 627 | 628 | readmem = b"\x1b\x05\x08\x00" + \ 629 | struct.pack("> 8) & 0xff): 660 | return True 661 | 662 | LOG.warning("Bad data from writemem") 663 | raise errors.RadioError("Bad response to writemem") 664 | 665 | 666 | def _resetradio(serport): 667 | resetpacket = b"\xdd\x05\x00\x00" 668 | _send_command(serport, resetpacket) 669 | 670 | 671 | def do_download(radio): 672 | """download eeprom from radio""" 673 | serport = radio.pipe 674 | serport.timeout = 0.5 675 | status = chirp_common.Status() 676 | status.cur = 0 677 | status.max = MEM_SIZE 678 | status.msg = "Downloading from radio" 679 | radio.status_fn(status) 680 | 681 | eeprom = b"" 682 | f = _sayhello(serport) 683 | if f: 684 | radio.FIRMWARE_VERSION = f 685 | else: 686 | raise errors.RadioError("Failed to initialize radio") 687 | 688 | addr = 0 689 | while addr < MEM_SIZE: 690 | data = _readmem(serport, addr, MEM_BLOCK) 691 | status.cur = addr 692 | radio.status_fn(status) 693 | 694 | if data and len(data) == MEM_BLOCK: 695 | eeprom += data 696 | addr += MEM_BLOCK 697 | else: 698 | raise errors.RadioError("Memory download incomplete") 699 | 700 | return memmap.MemoryMapBytes(eeprom) 701 | 702 | 703 | def do_upload(radio): 704 | """upload configuration to radio eeprom""" 705 | serport = radio.pipe 706 | serport.timeout = 0.5 707 | status = chirp_common.Status() 708 | status.cur = 0 709 | status.msg = "Uploading to radio" 710 | 711 | if radio.upload_calibration: 712 | status.max = MEM_SIZE-CAL_START 713 | start_addr = CAL_START 714 | stop_addr = MEM_SIZE 715 | else: 716 | status.max = PROG_SIZE 717 | start_addr = 0 718 | stop_addr = PROG_SIZE 719 | 720 | radio.status_fn(status) 721 | 722 | f = _sayhello(serport) 723 | if f: 724 | radio.FIRMWARE_VERSION = f 725 | else: 726 | return False 727 | 728 | addr = start_addr 729 | while addr < stop_addr: 730 | dat = radio.get_mmap()[addr:addr+MEM_BLOCK] 731 | _writemem(serport, dat, addr) 732 | status.cur = addr - start_addr 733 | radio.status_fn(status) 734 | if dat: 735 | addr += MEM_BLOCK 736 | else: 737 | raise errors.RadioError("Memory upload incomplete") 738 | status.msg = "Uploaded OK" 739 | 740 | _resetradio(serport) 741 | 742 | return True 743 | 744 | 745 | def min_max_def(value, min_val, max_val, default): 746 | """returns value if in bounds or default otherwise""" 747 | if min_val is not None and value < min_val: 748 | return default 749 | if max_val is not None and value > max_val: 750 | return default 751 | return value 752 | 753 | def list_def(value, lst, default): 754 | """return value if is in the list, default otherwise""" 755 | if isinstance(default, str): 756 | default = lst.index(default) 757 | if value < 0 or value >= len(lst): 758 | return default 759 | return value 760 | 761 | @directory.register 762 | class UVK5Radio(chirp_common.CloneModeRadio): 763 | """Quansheng UV-K5""" 764 | VENDOR = "Quansheng" 765 | MODEL = "UV-K5 (egzumer)" 766 | BAUD_RATE = 38400 767 | NEEDS_COMPAT_SERIAL = False 768 | FIRMWARE_VERSION = "" 769 | 770 | upload_calibration = False 771 | 772 | def _find_band(self, hz): 773 | mhz = hz/1000000.0 774 | bands = BANDS_WIDE if self._memobj.BUILD_OPTIONS.ENABLE_WIDE_RX \ 775 | else BANDS_STANDARD 776 | for bnd, rng in bands.items(): 777 | if rng[0] <= mhz <= rng[1]: 778 | return bnd 779 | return False 780 | 781 | def _get_vfo_channel_names(self): 782 | """generates VFO_CHANNEL_NAMES""" 783 | is_wide = self._memobj.BUILD_OPTIONS.ENABLE_WIDE_RX 784 | bands = BANDS_STANDARD if not is_wide else BANDS_WIDE 785 | names = [] 786 | for bnd, rng in bands.items(): 787 | name = f"F{bnd + 1}({round(rng[0])}M-{round(rng[1])}M)" 788 | names.append(name + "A") 789 | names.append(name + "B") 790 | return names 791 | 792 | def _get_specials(self): 793 | """generates SPECIALS""" 794 | specials = {} 795 | for idx, name in enumerate(self._get_vfo_channel_names()): 796 | specials[name] = 200 + idx 797 | return specials 798 | 799 | @classmethod 800 | def get_prompts(cls): 801 | rp = chirp_common.RadioPrompts() 802 | rp.experimental = \ 803 | _('This is an experimental driver for the Quansheng UV-K5. ' 804 | 'It may harm your radio, or worse. Use at your own risk.\n\n' 805 | 'Before attempting to do any changes please download' 806 | 'the memory image from the radio with chirp ' 807 | 'and keep it. This can be later used to recover the ' 808 | 'original settings. \n\n' 809 | 'some details are not yet implemented') 810 | rp.pre_download = _( 811 | "1. Turn radio on.\n" 812 | "2. Connect cable to mic/spkr connector.\n" 813 | "3. Make sure connector is firmly connected.\n" 814 | "4. Click OK to download image from device.\n\n" 815 | "It may not work if you turn on the radio " 816 | "with the cable already attached\n") 817 | rp.pre_upload = _( 818 | "1. Turn radio on.\n" 819 | "2. Connect cable to mic/spkr connector.\n" 820 | "3. Make sure connector is firmly connected.\n" 821 | "4. Click OK to upload the image to device.\n\n" 822 | "It may not work if you turn on the radio " 823 | "with the cable already attached") 824 | return rp 825 | 826 | # Return information about this radio's features, including 827 | # how many memories it has, what bands it supports, etc 828 | def get_features(self): 829 | rf = chirp_common.RadioFeatures() 830 | rf.has_bank = False 831 | rf.valid_dtcs_codes = DTCS_CODES 832 | rf.has_rx_dtcs = True 833 | rf.has_ctone = True 834 | rf.has_settings = True 835 | rf.has_comment = False 836 | rf.valid_name_length = 10 837 | rf.valid_power_levels = UVK5_POWER_LEVELS 838 | rf.valid_special_chans = self._get_vfo_channel_names() 839 | rf.valid_duplexes = ["", "-", "+", "off"] 840 | 841 | steps = STEPS.copy() 842 | steps.sort() 843 | rf.valid_tuning_steps = steps 844 | 845 | rf.valid_tmodes = ["", "Tone", "TSQL", "DTCS", "Cross"] 846 | rf.valid_cross_modes = ["Tone->Tone", "Tone->DTCS", "DTCS->Tone", 847 | "->Tone", "->DTCS", "DTCS->", "DTCS->DTCS"] 848 | 849 | rf.valid_characters = chirp_common.CHARSET_ASCII 850 | rf.valid_modes = ["FM", "NFM", "AM", "NAM", "USB"] 851 | 852 | rf.valid_skips = [""] 853 | 854 | # This radio supports memories 1-200, 201-214 are the VFO memories 855 | rf.memory_bounds = (1, 200) 856 | 857 | # This is what the BK4819 chip supports 858 | # Will leave it in a comment, might be useful someday 859 | # rf.valid_bands = [(18000000, 620000000), 860 | # (840000000, 1300000000) 861 | # ] 862 | rf.valid_bands = [] 863 | bands = BANDS_WIDE if self._memobj.BUILD_OPTIONS.ENABLE_WIDE_RX \ 864 | else BANDS_STANDARD 865 | for _, rng in bands.items(): 866 | rf.valid_bands.append( 867 | (int(rng[0]*1000000), int(rng[1]*1000000))) 868 | return rf 869 | 870 | # Do a download of the radio from the serial port 871 | def sync_in(self): 872 | self._mmap = do_download(self) 873 | self.process_mmap() 874 | 875 | # Do an upload of the radio to the serial port 876 | def sync_out(self): 877 | do_upload(self) 878 | 879 | # Convert the raw byte array into a memory object structure 880 | def process_mmap(self): 881 | self._memobj = bitwise.parse(MEM_FORMAT, self._mmap) 882 | 883 | # Return a raw representation of the memory object, which 884 | # is very helpful for development 885 | def get_raw_memory(self, number): 886 | return repr(self._memobj.channel[number-1]) 887 | 888 | def validate_memory(self, mem): 889 | msgs = super().validate_memory(mem) 890 | 891 | if mem.duplex == 'off': 892 | return msgs 893 | 894 | # find tx frequency 895 | if mem.duplex == '-': 896 | txfreq = mem.freq - mem.offset 897 | elif mem.duplex == '+': 898 | txfreq = mem.freq + mem.offset 899 | else: 900 | txfreq = mem.freq 901 | 902 | # find band 903 | band = self._find_band(txfreq) 904 | if band is False: 905 | msg = f"Transmit frequency {txfreq/1000000.0:.4f}MHz " \ 906 | "is not supported by this radio" 907 | msgs.append(chirp_common.ValidationWarning(msg)) 908 | 909 | band = self._find_band(mem.freq) 910 | if band is False: 911 | msg = f"The frequency {mem.freq/1000000.0:%.4f}MHz " \ 912 | "is not supported by this radio" 913 | msgs.append(chirp_common.ValidationWarning(msg)) 914 | 915 | return msgs 916 | 917 | def _set_tone(self, mem, _mem): 918 | ((txmode, txtone, txpol), 919 | (rxmode, rxtone, rxpol)) = chirp_common.split_tone_encode(mem) 920 | 921 | if txmode == "Tone": 922 | txtoval = CTCSS_TONES.index(txtone) 923 | txmoval = 0b01 924 | elif txmode == "DTCS": 925 | txmoval = txpol == "R" and 0b11 or 0b10 926 | txtoval = DTCS_CODES.index(txtone) 927 | else: 928 | txmoval = 0 929 | txtoval = 0 930 | 931 | if rxmode == "Tone": 932 | rxtoval = CTCSS_TONES.index(rxtone) 933 | rxmoval = 0b01 934 | elif rxmode == "DTCS": 935 | rxmoval = rxpol == "R" and 0b11 or 0b10 936 | rxtoval = DTCS_CODES.index(rxtone) 937 | else: 938 | rxmoval = 0 939 | rxtoval = 0 940 | 941 | _mem.rxcodeflag = rxmoval 942 | _mem.txcodeflag = txmoval 943 | _mem.rxcode = rxtoval 944 | _mem.txcode = txtoval 945 | 946 | def _get_tone(self, mem, _mem): 947 | rxtype = _mem.rxcodeflag 948 | txtype = _mem.txcodeflag 949 | rx_tmode = TMODES[rxtype] 950 | tx_tmode = TMODES[txtype] 951 | 952 | rx_tone = tx_tone = None 953 | 954 | if tx_tmode == "Tone": 955 | if _mem.txcode < len(CTCSS_TONES): 956 | tx_tone = CTCSS_TONES[_mem.txcode] 957 | else: 958 | tx_tone = 0 959 | tx_tmode = "" 960 | elif tx_tmode == "DTCS": 961 | if _mem.txcode < len(DTCS_CODES): 962 | tx_tone = DTCS_CODES[_mem.txcode] 963 | else: 964 | tx_tone = 0 965 | tx_tmode = "" 966 | 967 | if rx_tmode == "Tone": 968 | if _mem.rxcode < len(CTCSS_TONES): 969 | rx_tone = CTCSS_TONES[_mem.rxcode] 970 | else: 971 | rx_tone = 0 972 | rx_tmode = "" 973 | elif rx_tmode == "DTCS": 974 | if _mem.rxcode < len(DTCS_CODES): 975 | rx_tone = DTCS_CODES[_mem.rxcode] 976 | else: 977 | rx_tone = 0 978 | rx_tmode = "" 979 | 980 | tx_pol = txtype == 0x03 and "R" or "N" 981 | rx_pol = rxtype == 0x03 and "R" or "N" 982 | 983 | chirp_common.split_tone_decode(mem, (tx_tmode, tx_tone, tx_pol), 984 | (rx_tmode, rx_tone, rx_pol)) 985 | 986 | # Extract a high-level memory object from the low-level memory map 987 | # This is called to populate a memory in the UI 988 | def get_memory(self, number): 989 | 990 | mem = chirp_common.Memory() 991 | 992 | if isinstance(number, str): 993 | ch_num = self._get_specials()[number] 994 | mem.extd_number = number 995 | else: 996 | ch_num = number - 1 997 | 998 | mem.number = ch_num + 1 999 | 1000 | _mem = self._memobj.channel[ch_num] 1001 | 1002 | is_empty = False 1003 | # We'll consider any blank (i.e. 0MHz frequency) to be empty 1004 | if (_mem.freq == 0xffffffff) or (_mem.freq == 0): 1005 | is_empty = True 1006 | 1007 | # We'll also look at the channel attributes if a memory has them 1008 | tmpscn = SCANLIST_LIST[0] 1009 | tmp_comp = 0 1010 | if ch_num < 200: 1011 | _mem3 = self._memobj.ch_attr[ch_num] 1012 | # free memory bit 1013 | if _mem3.is_free: 1014 | is_empty = True 1015 | # scanlists 1016 | temp_val = _mem3.is_scanlist1 + _mem3.is_scanlist2 * 2 1017 | tmpscn = SCANLIST_LIST[temp_val] 1018 | tmp_comp = list_def(_mem3.compander, COMPANDER_LIST, 0) 1019 | 1020 | if is_empty: 1021 | mem.empty = True 1022 | # set some sane defaults: 1023 | mem.power = UVK5_POWER_LEVELS[2] 1024 | mem.extra = RadioSettingGroup("Extra", "extra") 1025 | 1026 | val = RadioSettingValueBoolean(False) 1027 | rs = RadioSetting("busyChLockout", "BusyCL", val) 1028 | mem.extra.append(rs) 1029 | 1030 | val = RadioSettingValueBoolean(False) 1031 | rs = RadioSetting("frev", "FreqRev", val) 1032 | mem.extra.append(rs) 1033 | 1034 | val = RadioSettingValueList(PTTID_LIST) 1035 | rs = RadioSetting("pttid", "PTTID", val) 1036 | mem.extra.append(rs) 1037 | 1038 | val = RadioSettingValueBoolean(False) 1039 | rs = RadioSetting("dtmfdecode", "DTMF decode", val) 1040 | if self._memobj.BUILD_OPTIONS.ENABLE_DTMF_CALLING: 1041 | mem.extra.append(rs) 1042 | 1043 | val = RadioSettingValueList(SCRAMBLER_LIST) 1044 | rs = RadioSetting("scrambler", "Scrambler", val) 1045 | mem.extra.append(rs) 1046 | 1047 | val = RadioSettingValueList(COMPANDER_LIST) 1048 | rs = RadioSetting("compander", "Compander", val) 1049 | mem.extra.append(rs) 1050 | 1051 | val = RadioSettingValueList(SCANLIST_LIST) 1052 | rs = RadioSetting("scanlists", "Scanlists", val) 1053 | mem.extra.append(rs) 1054 | 1055 | # actually the step and duplex are overwritten by chirp based on 1056 | # bandplan. they are here to document sane defaults for IARU r1 1057 | # mem.tuning_step = 25.0 1058 | # mem.duplex = "off" 1059 | 1060 | return mem 1061 | 1062 | if ch_num > 199: 1063 | mem.name = self._get_vfo_channel_names()[ch_num-200] 1064 | mem.immutable = ["name", "scanlists"] 1065 | else: 1066 | _mem2 = self._memobj.channelname[ch_num] 1067 | for char in _mem2.name: 1068 | if str(char) == "\xFF" or str(char) == "\x00": 1069 | break 1070 | mem.name += str(char) 1071 | mem.name = mem.name.rstrip() 1072 | 1073 | # Convert your low-level frequency to Hertz 1074 | mem.freq = int(_mem.freq)*10 1075 | mem.offset = int(_mem.offset)*10 1076 | 1077 | if mem.offset == 0: 1078 | mem.duplex = '' 1079 | else: 1080 | if _mem.offsetDir == FLAGS1_OFFSET_MINUS: 1081 | if _mem.freq == _mem.offset: 1082 | # fake tx disable by setting tx to 0 MHz 1083 | mem.duplex = 'off' 1084 | mem.offset = 0 1085 | else: 1086 | mem.duplex = '-' 1087 | elif _mem.offsetDir == FLAGS1_OFFSET_PLUS: 1088 | mem.duplex = '+' 1089 | else: 1090 | mem.duplex = '' 1091 | 1092 | # tone data 1093 | self._get_tone(mem, _mem) 1094 | 1095 | # mode 1096 | temp_modes = self.get_features().valid_modes 1097 | temp_modul = _mem.modulation*2 + _mem.bandwidth 1098 | if temp_modul < len(temp_modes): 1099 | mem.mode = temp_modes[temp_modul] 1100 | elif temp_modul == 5: # USB with narrow setting 1101 | mem.mode = temp_modes[4] 1102 | elif temp_modul >= len(temp_modes): 1103 | mem.mode = "UNSUPPORTED BY CHIRP" 1104 | 1105 | # tuning step 1106 | tstep = _mem.step 1107 | if tstep < len(STEPS): 1108 | mem.tuning_step = STEPS[tstep] 1109 | else: 1110 | mem.tuning_step = 2.5 1111 | 1112 | # power 1113 | if _mem.txpower == POWER_HIGH: 1114 | mem.power = UVK5_POWER_LEVELS[2] 1115 | elif _mem.txpower == POWER_MEDIUM: 1116 | mem.power = UVK5_POWER_LEVELS[1] 1117 | else: 1118 | mem.power = UVK5_POWER_LEVELS[0] 1119 | 1120 | # We'll consider any blank (i.e. 0MHz frequency) to be empty 1121 | if (_mem.freq == 0xffffffff) or (_mem.freq == 0): 1122 | mem.empty = True 1123 | else: 1124 | mem.empty = False 1125 | 1126 | mem.extra = RadioSettingGroup("Extra", "extra") 1127 | 1128 | # BusyCL 1129 | val = RadioSettingValueBoolean(_mem.busyChLockout) 1130 | rs = RadioSetting("busyChLockout", "Busy Ch Lockout (BusyCL)", val) 1131 | mem.extra.append(rs) 1132 | 1133 | # Frequency reverse 1134 | val = RadioSettingValueBoolean(_mem.freq_reverse) 1135 | rs = RadioSetting("frev", "Reverse Frequencies (R)", val) 1136 | mem.extra.append(rs) 1137 | 1138 | # PTTID 1139 | pttid = list_def(_mem.dtmf_pttid, PTTID_LIST, 0) 1140 | val = RadioSettingValueList(PTTID_LIST, None, pttid) 1141 | rs = RadioSetting("pttid", "PTT ID (PTT ID)", val) 1142 | mem.extra.append(rs) 1143 | 1144 | # DTMF DECODE 1145 | val = RadioSettingValueBoolean(_mem.dtmf_decode) 1146 | rs = RadioSetting("dtmfdecode", "DTMF decode (D Decd)", val) 1147 | if self._memobj.BUILD_OPTIONS.ENABLE_DTMF_CALLING: 1148 | mem.extra.append(rs) 1149 | 1150 | # Scrambler 1151 | enc = list_def(_mem.scrambler, SCRAMBLER_LIST, 0) 1152 | val = RadioSettingValueList(SCRAMBLER_LIST, None, enc) 1153 | rs = RadioSetting("scrambler", "Scrambler (Scramb)", val) 1154 | mem.extra.append(rs) 1155 | 1156 | # Compander 1157 | val = RadioSettingValueList(COMPANDER_LIST, None, tmp_comp) 1158 | rs = RadioSetting("compander", "Compander (Compnd)", val) 1159 | mem.extra.append(rs) 1160 | 1161 | val = RadioSettingValueList(SCANLIST_LIST, tmpscn) 1162 | rs = RadioSetting("scanlists", "Scanlists (SList)", val) 1163 | mem.extra.append(rs) 1164 | 1165 | return mem 1166 | 1167 | def set_settings(self, settings): 1168 | _mem = self._memobj 1169 | for element in settings: 1170 | if not isinstance(element, RadioSetting): 1171 | self.set_settings(element) 1172 | continue 1173 | 1174 | elname = element.get_name() 1175 | 1176 | # basic settings 1177 | 1178 | # VFO_A e80 ScreenChannel_A 1179 | if elname == "VFO_A_chn": 1180 | _mem.ScreenChannel_A = int(element.value) 1181 | if _mem.ScreenChannel_A < 200: 1182 | _mem.MrChannel_A = _mem.ScreenChannel_A 1183 | elif _mem.ScreenChannel_A < 207: 1184 | _mem.FreqChannel_A = _mem.ScreenChannel_A 1185 | else: 1186 | _mem.NoaaChannel_A = _mem.ScreenChannel_A 1187 | 1188 | # VFO_B e83 1189 | elif elname == "VFO_B_chn": 1190 | _mem.ScreenChannel_B = int(element.value) 1191 | if _mem.ScreenChannel_B < 200: 1192 | _mem.MrChannel_B = _mem.ScreenChannel_B 1193 | elif _mem.ScreenChannel_B < 207: 1194 | _mem.FreqChannel_B = _mem.ScreenChannel_B 1195 | else: 1196 | _mem.NoaaChannel_B = _mem.ScreenChannel_B 1197 | 1198 | # TX_VFO channel selected A,B 1199 | elif elname == "TX_VFO": 1200 | _mem.TX_VFO = TX_VFO_LIST.index(str(element.value)) 1201 | 1202 | # call channel 1203 | elif elname == "call_channel": 1204 | _mem.call_channel = int(element.value)-1 1205 | 1206 | # squelch 1207 | elif elname == "squelch": 1208 | _mem.squelch = int(element.value) 1209 | 1210 | # TOT 1211 | elif elname == "tot": 1212 | _mem.max_talk_time = TALK_TIME_LIST.index(str(element.value)) 1213 | 1214 | # NOAA autoscan 1215 | elif elname == "noaa_autoscan": 1216 | _mem.noaa_autoscan = int(element.value) 1217 | 1218 | # VOX 1219 | elif elname == "vox": 1220 | voxvalue = VOX_LIST.index(str(element.value)) 1221 | _mem.vox_switch = voxvalue > 0 1222 | _mem.vox_level = (voxvalue - 1) if _mem.vox_switch else 0 1223 | 1224 | # mic gain 1225 | elif elname == "mic_gain": 1226 | _mem.mic_gain = int(element.value) 1227 | 1228 | # Channel display mode 1229 | elif elname == "channel_display_mode": 1230 | _mem.channel_display_mode = CHANNELDISP_LIST.index( 1231 | str(element.value)) 1232 | 1233 | # RX Mode 1234 | elif elname == "rx_mode": 1235 | tmptxmode = RXMODE_LIST.index(str(element.value)) 1236 | tmpmainvfo = _mem.TX_VFO + 1 1237 | _mem.crossband = tmpmainvfo * bool(tmptxmode & 0b10) 1238 | _mem.dual_watch = tmpmainvfo * bool(tmptxmode & 0b01) 1239 | 1240 | # Battery Save 1241 | elif elname == "battery_save": 1242 | _mem.battery_save = BATSAVE_LIST.index(str(element.value)) 1243 | 1244 | # Backlight auto mode 1245 | elif elname == "backlight_time": 1246 | _mem.backlight_time = BACKLIGHT_LIST.index(str(element.value)) 1247 | 1248 | # Backlight min 1249 | elif elname == "backlight_min": 1250 | _mem.backlight_min = \ 1251 | BACKLIGHT_LVL_LIST.index(str(element.value)) 1252 | 1253 | # Backlight max 1254 | elif elname == "backlight_max": 1255 | _mem.backlight_max = \ 1256 | BACKLIGHT_LVL_LIST.index(str(element.value)) 1257 | 1258 | # Backlight TX_RX 1259 | elif elname == "backlight_on_TX_RX": 1260 | _mem.backlight_on_TX_RX = \ 1261 | BACKLIGHT_TX_RX_LIST.index(str(element.value)) 1262 | # AM_fix 1263 | elif elname == "AM_fix": 1264 | _mem.AM_fix = int(element.value) 1265 | 1266 | # mic_bar 1267 | elif elname == "mem.mic_bar": 1268 | _mem.mic_bar = int(element.value) 1269 | 1270 | # Batterie txt 1271 | elif elname == "_mem.battery_text": 1272 | _mem.battery_text = \ 1273 | BAT_TXT_LIST.index(str(element.value)) 1274 | 1275 | # Tail tone elimination 1276 | elif elname == "ste": 1277 | _mem.ste = int(element.value) 1278 | 1279 | # VFO Open 1280 | elif elname == "freq_mode_allowed": 1281 | _mem.freq_mode_allowed = int(element.value) 1282 | 1283 | # Beep control 1284 | elif elname == "button_beep": 1285 | _mem.button_beep = int(element.value) 1286 | 1287 | # Scan resume mode 1288 | elif elname == "scan_resume_mode": 1289 | _mem.scan_resume_mode = SCANRESUME_LIST.index( 1290 | str(element.value)) 1291 | 1292 | # Keypad lock 1293 | elif elname == "key_lock": 1294 | _mem.key_lock = int(element.value) 1295 | 1296 | # Auto keypad lock 1297 | elif elname == "auto_keypad_lock": 1298 | _mem.auto_keypad_lock = int(element.value) 1299 | 1300 | # Power on display mode 1301 | elif elname == "welcome_mode": 1302 | _mem.power_on_dispmode = WELCOME_LIST.index(str(element.value)) 1303 | 1304 | # Keypad Tone 1305 | elif elname == "voice": 1306 | _mem.voice = VOICE_LIST.index(str(element.value)) 1307 | 1308 | elif elname == "s0_level": 1309 | _mem.s0_level = -int(element.value) 1310 | 1311 | elif elname == "s9_level": 1312 | _mem.s9_level = -int(element.value) 1313 | 1314 | elif elname == "password": 1315 | if element.value.get_value() is None or element.value == "": 1316 | _mem.password = 0xFFFFFFFF 1317 | else: 1318 | _mem.password = int(element.value) 1319 | 1320 | # Alarm mode 1321 | elif elname == "alarm_mode": 1322 | _mem.alarm_mode = ALARMMODE_LIST.index(str(element.value)) 1323 | 1324 | # Reminding of end of talk 1325 | elif elname == "roger_beep": 1326 | _mem.roger_beep = REMENDOFTALK_LIST.index(str(element.value)) 1327 | 1328 | # Repeater tail tone elimination 1329 | elif elname == "rp_ste": 1330 | _mem.rp_ste = RTE_LIST.index( 1331 | str(element.value)) 1332 | 1333 | # Logo string 1 1334 | elif elname == "logo1": 1335 | bts = str(element.value).rstrip("\x20\xff\x00")+"\x00"*12 1336 | _mem.logo_line1 = bts[0:12]+"\x00\xff\xff\xff" 1337 | 1338 | # Logo string 2 1339 | elif elname == "logo2": 1340 | bts = str(element.value).rstrip("\x20\xff\x00")+"\x00"*12 1341 | _mem.logo_line2 = bts[0:12]+"\x00\xff\xff\xff" 1342 | 1343 | # unlock settings 1344 | 1345 | # FLOCK 1346 | elif elname == "int_flock": 1347 | _mem.int_flock = FLOCK_LIST.index(str(element.value)) 1348 | 1349 | # 350TX 1350 | elif elname == "int_350tx": 1351 | _mem.int_350tx = int(element.value) 1352 | 1353 | # KILLED 1354 | elif elname == "int_KILLED": 1355 | _mem.int_KILLED = int(element.value) 1356 | 1357 | # 200TX 1358 | elif elname == "int_200tx": 1359 | _mem.int_200tx = int(element.value) 1360 | 1361 | # 500TX 1362 | elif elname == "int_500tx": 1363 | _mem.int_500tx = int(element.value) 1364 | 1365 | # 350EN 1366 | elif elname == "int_350en": 1367 | _mem.int_350en = int(element.value) 1368 | 1369 | # SCREN 1370 | elif elname == "int_scren": 1371 | _mem.int_scren = int(element.value) 1372 | 1373 | # battery type 1374 | elif elname == "Battery_type": 1375 | _mem.Battery_type = BATTYPE_LIST.index(str(element.value)) 1376 | # fm radio 1377 | for i in range(1, 21): 1378 | freqname = "FM_" + str(i) 1379 | if elname == freqname: 1380 | val = str(element.value).strip() 1381 | try: 1382 | val2 = int(float(val)*10) 1383 | except Exception: 1384 | val2 = 0xffff 1385 | 1386 | if val2 < FMMIN*10 or val2 > FMMAX*10: 1387 | val2 = 0xffff 1388 | # raise errors.InvalidValueError( 1389 | # "FM radio frequency should be a value " 1390 | # "in the range %.1f - %.1f" % (FMMIN , FMMAX)) 1391 | _mem.fmfreq[i-1] = val2 1392 | 1393 | # dtmf settings 1394 | if elname == "dtmf_side_tone": 1395 | _mem.dtmf.side_tone = \ 1396 | int(element.value) 1397 | 1398 | elif elname == "dtmf_separate_code": 1399 | _mem.dtmf.separate_code = str(element.value) 1400 | 1401 | elif elname == "dtmf_group_call_code": 1402 | _mem.dtmf.group_call_code = element.value 1403 | 1404 | elif elname == "dtmf_decode_response": 1405 | _mem.dtmf.decode_response = \ 1406 | DTMF_DECODE_RESPONSE_LIST.index(str(element.value)) 1407 | 1408 | elif elname == "dtmf_auto_reset_time": 1409 | _mem.dtmf.auto_reset_time = \ 1410 | int(int(element.value)/10) 1411 | 1412 | elif elname == "dtmf_preload_time": 1413 | _mem.dtmf.preload_time = \ 1414 | int(int(element.value)/10) 1415 | 1416 | elif elname == "dtmf_first_code_persist_time": 1417 | _mem.dtmf.first_code_persist_time = \ 1418 | int(int(element.value)/10) 1419 | 1420 | elif elname == "dtmf_hash_persist_time": 1421 | _mem.dtmf.hash_persist_time = \ 1422 | int(int(element.value)/10) 1423 | 1424 | elif elname == "dtmf_code_persist_time": 1425 | _mem.dtmf.code_persist_time = \ 1426 | int(int(element.value)/10) 1427 | 1428 | elif elname == "dtmf_code_interval_time": 1429 | _mem.dtmf.code_interval_time = \ 1430 | int(int(element.value)/10) 1431 | 1432 | elif elname == "dtmf_permit_remote_kill": 1433 | _mem.dtmf.permit_remote_kill = \ 1434 | int(element.value) 1435 | 1436 | elif elname == "dtmf_dtmf_local_code": 1437 | k = str(element.value).rstrip("\x20\xff\x00") + "\x00"*3 1438 | _mem.dtmf.local_code = k[0:3] 1439 | 1440 | elif elname == "dtmf_dtmf_up_code": 1441 | k = str(element.value).strip("\x20\xff\x00") + "\x00"*16 1442 | _mem.dtmf.up_code = k[0:16] 1443 | 1444 | elif elname == "dtmf_dtmf_down_code": 1445 | k = str(element.value).rstrip("\x20\xff\x00") + "\x00"*16 1446 | _mem.dtmf.down_code = k[0:16] 1447 | 1448 | elif elname == "dtmf_kill_code": 1449 | k = str(element.value).strip("\x20\xff\x00") + "\x00"*5 1450 | _mem.dtmf.kill_code = k[0:5] 1451 | 1452 | elif elname == "dtmf_revive_code": 1453 | k = str(element.value).strip("\x20\xff\x00") + "\x00"*5 1454 | _mem.dtmf.revive_code = k[0:5] 1455 | 1456 | elif elname == "live_DTMF_decoder": 1457 | _mem.live_DTMF_decoder = int(element.value) 1458 | 1459 | # dtmf contacts 1460 | for i in range(1, 17): 1461 | varname = "DTMF_" + str(i) 1462 | if elname == varname: 1463 | k = str(element.value).rstrip("\x20\xff\x00") + "\x00"*8 1464 | _mem.dtmfcontact[i-1].name = k[0:8] 1465 | 1466 | varnumname = "DTMFNUM_" + str(i) 1467 | if elname == varnumname: 1468 | k = str(element.value).rstrip("\x20\xff\x00") + "\xff"*3 1469 | _mem.dtmfcontact[i-1].number = k[0:3] 1470 | 1471 | # scanlist stuff 1472 | if elname == "slDef": 1473 | _mem.slDef = SCANLIST_SELECT_LIST.index( 1474 | str(element.value)) 1475 | 1476 | elif elname == "sl1PriorEnab": 1477 | _mem.sl1PriorEnab = \ 1478 | int(element.value) 1479 | 1480 | elif elname == "sl2PriorEnab": 1481 | _mem.sl2PriorEnab = \ 1482 | int(element.value) 1483 | 1484 | elif elname == "sl1PriorCh1" or \ 1485 | elname == "sl1PriorCh2" or \ 1486 | elname == "sl2PriorCh1" or \ 1487 | elname == "sl2PriorCh2": 1488 | 1489 | val = int(element.value) 1490 | 1491 | if val > 200 or val < 1: 1492 | val = 0xff 1493 | else: 1494 | val -= 1 1495 | 1496 | if elname == "sl1PriorCh1": 1497 | _mem.sl1PriorCh1 = val 1498 | if elname == "sl1PriorCh2": 1499 | _mem.sl1PriorCh2 = val 1500 | if elname == "sl2PriorCh1": 1501 | _mem.sl2PriorCh1 = val 1502 | if elname == "sl2PriorCh2": 1503 | _mem.sl2PriorCh2 = val 1504 | 1505 | if elname == "key1_shortpress_action": 1506 | _mem.key1_shortpress_action = KEYACTIONS_LIST.index( 1507 | str(element.value)) 1508 | 1509 | elif elname == "key1_longpress_action": 1510 | _mem.key1_longpress_action = KEYACTIONS_LIST.index( 1511 | str(element.value)) 1512 | 1513 | elif elname == "key2_shortpress_action": 1514 | _mem.key2_shortpress_action = KEYACTIONS_LIST.index( 1515 | str(element.value)) 1516 | 1517 | elif elname == "key2_longpress_action": 1518 | _mem.key2_longpress_action = KEYACTIONS_LIST.index( 1519 | str(element.value)) 1520 | 1521 | elif elname == "keyM_longpress_action": 1522 | _mem.keyM_longpress_action = KEYACTIONS_LIST.index( 1523 | str(element.value)) 1524 | 1525 | 1526 | elif element.changed() and elname.startswith("_mem.cal."): 1527 | exec(elname + " = element.value.get_value()") 1528 | 1529 | 1530 | def get_settings(self): 1531 | _mem = self._memobj 1532 | basic = RadioSettingGroup("basic", "Basic Settings") 1533 | advanced = RadioSettingGroup("advanced", "Advanced Settings") 1534 | keya = RadioSettingGroup("keya", "Programmable Keys") 1535 | dtmf = RadioSettingGroup("dtmf", "DTMF Settings") 1536 | dtmfc = RadioSettingGroup("dtmfc", "DTMF Contacts") 1537 | scanl = RadioSettingGroup("scn", "Scan Lists") 1538 | unlock = RadioSettingGroup("unlock", "Unlock Settings") 1539 | fmradio = RadioSettingGroup("fmradio", "FM Radio") 1540 | calibration = RadioSettingGroup("calibration", "Calibration") 1541 | 1542 | roinfo = RadioSettingGroup("roinfo", "Driver Information") 1543 | top = RadioSettings() 1544 | top.append(basic) 1545 | top.append(advanced) 1546 | top.append(keya) 1547 | top.append(dtmf) 1548 | if _mem.BUILD_OPTIONS.ENABLE_DTMF_CALLING: 1549 | top.append(dtmfc) 1550 | top.append(scanl) 1551 | top.append(unlock) 1552 | if _mem.BUILD_OPTIONS.ENABLE_FMRADIO: 1553 | top.append(fmradio) 1554 | top.append(roinfo) 1555 | top.append(calibration) 1556 | 1557 | # helper function 1558 | def append_label(radio_setting, label, descr = ""): 1559 | if not hasattr(append_label, 'idx'): 1560 | append_label.idx = 0 1561 | 1562 | val = RadioSettingValueString(len(descr), len(descr), descr) 1563 | val.set_mutable(False) 1564 | rs = RadioSetting("label" + str(append_label.idx), label, val) 1565 | append_label.idx += 1 1566 | radio_setting.append(rs) 1567 | 1568 | # Programmable keys 1569 | def get_action(action_num): 1570 | """"get actual key action""" 1571 | has_alarm = self._memobj.BUILD_OPTIONS.ENABLE_ALARM 1572 | has1750 = self._memobj.BUILD_OPTIONS.ENABLE_TX1750 1573 | has_flashlight = self._memobj.BUILD_OPTIONS.ENABLE_FLASHLIGHT 1574 | lst = KEYACTIONS_LIST.copy() 1575 | if not has_alarm: 1576 | lst.remove("ALARM") 1577 | if not has1750: 1578 | lst.remove("1750Hz TONE") 1579 | if not has_flashlight: 1580 | lst.remove("FLASHLIGHT") 1581 | 1582 | action_num = int(action_num) 1583 | if action_num >= len(KEYACTIONS_LIST) or \ 1584 | KEYACTIONS_LIST[action_num] not in lst: 1585 | action_num = 0 1586 | return lst, KEYACTIONS_LIST[action_num] 1587 | 1588 | val = RadioSettingValueList(*get_action(_mem.key1_shortpress_action)) 1589 | rs = RadioSetting("key1_shortpress_action", 1590 | "Side key 1 short press (F1Shrt)", val) 1591 | keya.append(rs) 1592 | 1593 | val = RadioSettingValueList(*get_action(_mem.key1_longpress_action)) 1594 | rs = RadioSetting("key1_longpress_action", 1595 | "Side key 1 long press (F1Long)", val) 1596 | keya.append(rs) 1597 | 1598 | val = RadioSettingValueList(*get_action(_mem.key2_shortpress_action)) 1599 | rs = RadioSetting("key2_shortpress_action", 1600 | "Side key 2 short press (F2Shrt)", val) 1601 | keya.append(rs) 1602 | 1603 | val = RadioSettingValueList(*get_action(_mem.key2_longpress_action)) 1604 | rs = RadioSetting("key2_longpress_action", 1605 | "Side key 2 long press (F2Long)", val) 1606 | keya.append(rs) 1607 | 1608 | val = RadioSettingValueList(*get_action(_mem.keyM_longpress_action)) 1609 | rs = RadioSetting("keyM_longpress_action", 1610 | "Menu key long press (M Long)", val) 1611 | keya.append(rs) 1612 | 1613 | 1614 | ################## DTMF settings 1615 | 1616 | tmpval = str(_mem.dtmf.separate_code) 1617 | if tmpval not in DTMF_CODE_CHARS: 1618 | tmpval = '*' 1619 | val = RadioSettingValueString(1, 1, tmpval) 1620 | val.set_charset(DTMF_CODE_CHARS) 1621 | sep_code_setting = RadioSetting("dtmf_separate_code", 1622 | "Separate Code", val) 1623 | 1624 | tmpval = str(_mem.dtmf.group_call_code) 1625 | if tmpval not in DTMF_CODE_CHARS: 1626 | tmpval = '#' 1627 | val = RadioSettingValueString(1, 1, tmpval) 1628 | val.set_charset(DTMF_CODE_CHARS) 1629 | group_code_setting = RadioSetting("dtmf_group_call_code", 1630 | "Group Call Code", val) 1631 | 1632 | tmpval = min_max_def(_mem.dtmf.first_code_persist_time * 10, 1633 | 30, 1000, 300) 1634 | val = RadioSettingValueInteger(30, 1000, tmpval, 10) 1635 | first_code_per_setting = RadioSetting("dtmf_first_code_persist_time", 1636 | "First code persist time (ms)", val) 1637 | 1638 | tmpval = min_max_def(_mem.dtmf.hash_persist_time * 10, 30, 1000, 300) 1639 | val = RadioSettingValueInteger(30, 1000, tmpval, 10) 1640 | spec_per_setting = RadioSetting("dtmf_hash_persist_time", 1641 | "#/* persist time (ms)", val) 1642 | 1643 | tmpval = min_max_def(_mem.dtmf.code_persist_time * 10, 30, 1000, 300) 1644 | val = RadioSettingValueInteger(30, 1000, tmpval, 10) 1645 | code_per_setting = RadioSetting("dtmf_code_persist_time", 1646 | "Code persist time (ms)", val) 1647 | 1648 | tmpval = min_max_def(_mem.dtmf.code_interval_time * 10, 30, 1000, 300) 1649 | val = RadioSettingValueInteger(30, 1000, tmpval, 10) 1650 | code_int_setting = RadioSetting("dtmf_code_interval_time", 1651 | "Code interval time (ms)", val) 1652 | 1653 | tmpval = str(_mem.dtmf.local_code).upper().strip( 1654 | "\x00\xff\x20") 1655 | for i in tmpval: 1656 | if i in DTMF_CHARS_ID: 1657 | continue 1658 | tmpval = "103" 1659 | break 1660 | val = RadioSettingValueString(3, 3, tmpval) 1661 | val.set_charset(DTMF_CHARS_ID) 1662 | ani_id_setting = RadioSetting("dtmf_dtmf_local_code", 1663 | "Local code (3 chars 0-9 ABCD) (ANI ID)", val) 1664 | 1665 | tmpval = str(_mem.dtmf.up_code).upper().strip( 1666 | "\x00\xff\x20") 1667 | for i in tmpval: 1668 | if i in DTMF_CHARS_UPDOWN or i == "": 1669 | continue 1670 | else: 1671 | tmpval = "123" 1672 | break 1673 | val = RadioSettingValueString(1, 16, tmpval) 1674 | val.set_charset(DTMF_CHARS_UPDOWN) 1675 | up_code_setting = RadioSetting("dtmf_dtmf_up_code", 1676 | "Up code (1-16 chars 0-9 ABCD*#) (UPCode)", val) 1677 | 1678 | tmpval = str(_mem.dtmf.down_code).upper().strip( 1679 | "\x00\xff\x20") 1680 | for i in tmpval: 1681 | if i in DTMF_CHARS_UPDOWN: 1682 | continue 1683 | else: 1684 | tmpval = "456" 1685 | break 1686 | val = RadioSettingValueString(1, 16, tmpval) 1687 | val.set_charset(DTMF_CHARS_UPDOWN) 1688 | dw_code_setting = RadioSetting("dtmf_dtmf_down_code", 1689 | "Down code (1-16 chars 0-9 ABCD*#) (DWCode)", val) 1690 | 1691 | val = RadioSettingValueBoolean(_mem.dtmf.side_tone) 1692 | dtmf_side_tone_setting = RadioSetting("dtmf_side_tone", 1693 | "DTMF Sidetone on speaker when sent (D ST)", val) 1694 | 1695 | tmpval = list_def(_mem.dtmf.decode_response, 1696 | DTMF_DECODE_RESPONSE_LIST, 0) 1697 | val = RadioSettingValueList(DTMF_DECODE_RESPONSE_LIST, None,tmpval) 1698 | dtmf_resp_setting = RadioSetting("dtmf_decode_response", 1699 | "Decode Response (D Resp)", val) 1700 | 1701 | tmpval = min_max_def(_mem.dtmf.auto_reset_time, 5, 60, 5) 1702 | val = RadioSettingValueInteger(5, 60, tmpval) 1703 | d_hold_setting = RadioSetting("dtmf_auto_reset_time", 1704 | "Auto reset time (s) (D Hold)", val) 1705 | 1706 | 1707 | # D Prel 1708 | tmpval = min_max_def(_mem.dtmf.preload_time * 10, 30, 990, 300) 1709 | val = RadioSettingValueInteger(30, 990, tmpval, 10) 1710 | d_prel_setting = RadioSetting("dtmf_preload_time", 1711 | "Pre-load time (ms) (D Prel)", val) 1712 | 1713 | # D LIVE 1714 | val = RadioSettingValueBoolean(_mem.live_DTMF_decoder) 1715 | d_live_setting = RadioSetting("live_DTMF_decoder", "Displays DTMF codes" 1716 | " received in the middle of the screen (D Live)", val) 1717 | 1718 | val = RadioSettingValueBoolean(_mem.dtmf.permit_remote_kill) 1719 | perm_kill_setting = RadioSetting("dtmf_permit_remote_kill", 1720 | "Permit remote kill", val) 1721 | 1722 | tmpval = str(_mem.dtmf.kill_code).upper().strip( 1723 | "\x00\xff\x20") 1724 | for i in tmpval: 1725 | if i in DTMF_CHARS_KILL: 1726 | continue 1727 | else: 1728 | tmpval = "77777" 1729 | break 1730 | if not len(tmpval) == 5: 1731 | tmpval = "77777" 1732 | val = RadioSettingValueString(5, 5, tmpval) 1733 | val.set_charset(DTMF_CHARS_KILL) 1734 | kill_code_setting = RadioSetting("dtmf_kill_code", 1735 | "Kill code (5 chars 0-9 ABCD)", val) 1736 | 1737 | tmpval = str(_mem.dtmf.revive_code).upper().strip( 1738 | "\x00\xff\x20") 1739 | for i in tmpval: 1740 | if i in DTMF_CHARS_KILL: 1741 | continue 1742 | else: 1743 | tmpval = "88888" 1744 | break 1745 | if not len(tmpval) == 5: 1746 | tmpval = "88888" 1747 | val = RadioSettingValueString(5, 5, tmpval) 1748 | val.set_charset(DTMF_CHARS_KILL) 1749 | rev_code_setting = RadioSetting("dtmf_revive_code", 1750 | "Revive code (5 chars 0-9 ABCD)", val) 1751 | 1752 | val = RadioSettingValueBoolean(_mem.int_KILLED) 1753 | killed_setting = RadioSetting("int_KILLED", "DTMF kill lock", val) 1754 | 1755 | ################## DTMF Contacts 1756 | 1757 | append_label(dtmfc, "DTMF Contacts (D List)", 1758 | "All DTMF Contacts are 3 codes " 1759 | "(valid: 0-9 * # ABCD), " 1760 | "or an empty string") 1761 | 1762 | for i in range(1, 17): 1763 | varname = "DTMF_"+str(i) 1764 | varnumname = "DTMFNUM_"+str(i) 1765 | vardescr = "DTMF Contact "+str(i)+" name" 1766 | varinumdescr = "DTMF Contact "+str(i)+" number" 1767 | 1768 | cntn = str(_mem.dtmfcontact[i-1].name).strip("\x20\x00\xff") 1769 | cntnum = str(_mem.dtmfcontact[i-1].number).strip("\x20\x00\xff") 1770 | 1771 | val = RadioSettingValueString(0, 8, cntn) 1772 | rs = RadioSetting(varname, vardescr, val) 1773 | dtmfc.append(rs) 1774 | 1775 | val = RadioSettingValueString(0, 3, cntnum) 1776 | val.set_charset(DTMF_CHARS) 1777 | rs = RadioSetting(varnumname, varinumdescr, val) 1778 | dtmfc.append(rs) 1779 | 1780 | ################## Scan Lists 1781 | 1782 | tmpscanl = list_def(_mem.slDef, SCANLIST_SELECT_LIST, 0) 1783 | val = RadioSettingValueList(SCANLIST_SELECT_LIST, None, tmpscanl) 1784 | rs = RadioSetting("slDef", "Default scanlist (SList)", val) 1785 | scanl.append(rs) 1786 | 1787 | val = RadioSettingValueBoolean(_mem.sl1PriorEnab) 1788 | rs = RadioSetting("sl1PriorEnab", 1789 | "Scanlist 1 priority channel scan", val) 1790 | scanl.append(rs) 1791 | 1792 | tmpch = min_max_def(_mem.sl1PriorCh1 + 1, 0, 200, 0) 1793 | val = RadioSettingValueInteger(0, 200, tmpch) 1794 | rs = RadioSetting("sl1PriorCh1", 1795 | "Scanlist 1 priority channel 1 (0 - OFF)", val) 1796 | scanl.append(rs) 1797 | 1798 | tmpch = min_max_def(_mem.sl1PriorCh2 + 1, 0, 200, 0) 1799 | val = RadioSettingValueInteger(0, 200, tmpch) 1800 | rs = RadioSetting("sl1PriorCh2", 1801 | "Scanlist 1 priority channel 2 (0 - OFF)", val) 1802 | scanl.append(rs) 1803 | 1804 | val = RadioSettingValueBoolean(_mem.sl2PriorEnab) 1805 | rs = RadioSetting("sl2PriorEnab", 1806 | "Scanlist 2 priority channel scan", val) 1807 | scanl.append(rs) 1808 | 1809 | tmpch = min_max_def(_mem.sl2PriorCh1 + 1, 0, 200, 0) 1810 | val = RadioSettingValueInteger(0, 200, tmpch) 1811 | rs = RadioSetting("sl2PriorCh1", 1812 | "Scanlist 2 priority channel 1 (0 - OFF)", val) 1813 | scanl.append(rs) 1814 | 1815 | tmpch = min_max_def(_mem.sl2PriorCh2 + 1, 0, 200, 0) 1816 | val = RadioSettingValueInteger(0, 200, tmpch) 1817 | rs = RadioSetting("sl2PriorCh2", 1818 | "Scanlist 2 priority channel 2 (0 - OFF)", val) 1819 | scanl.append(rs) 1820 | 1821 | ################## Basic settings 1822 | 1823 | ch_list = [] 1824 | for ch in range(1, 201): 1825 | ch_list.append("Channel M" + str(ch)) 1826 | for bnd in range(1, 8): 1827 | ch_list.append("Band F" + str(bnd)) 1828 | if _mem.BUILD_OPTIONS.ENABLE_NOAA: 1829 | for bnd in range(1, 11): 1830 | ch_list.append("NOAA N" + str(bnd)) 1831 | 1832 | tmpfreq0 = list_def(_mem.ScreenChannel_A, ch_list, 0) 1833 | val = RadioSettingValueList(ch_list, None, tmpfreq0) 1834 | freq0_setting = RadioSetting("VFO_A_chn", 1835 | "VFO A current channel/band", val) 1836 | 1837 | tmpfreq1 = list_def(_mem.ScreenChannel_B, ch_list, 0) 1838 | val = RadioSettingValueList(ch_list, None, tmpfreq1) 1839 | freq1_setting = RadioSetting("VFO_B_chn", 1840 | "VFO B current channel/band", val) 1841 | 1842 | tmptxvfo = list_def(_mem.TX_VFO, TX_VFO_LIST, "A") 1843 | val = RadioSettingValueList(TX_VFO_LIST, None, tmptxvfo) 1844 | tx_vfo_setting = RadioSetting("TX_VFO", "Main VFO", val) 1845 | 1846 | tmpsq = min_max_def(_mem.squelch, 0, 9, 1) 1847 | val = RadioSettingValueInteger(0, 9, tmpsq) 1848 | squelch_setting = RadioSetting("squelch", "Squelch (Sql)", val) 1849 | 1850 | tmpc = min_max_def(_mem.call_channel + 1, 1, 200, 1) 1851 | val = RadioSettingValueInteger(1, 200, tmpc) 1852 | call_channel_setting = RadioSetting("call_channel", 1853 | "One key call channel", val) 1854 | 1855 | val = RadioSettingValueBoolean(_mem.key_lock) 1856 | keypad_cock_setting = RadioSetting("key_lock", "Keypad locked", val) 1857 | 1858 | val = RadioSettingValueBoolean(_mem.auto_keypad_lock) 1859 | auto_keypad_lock_setting = RadioSetting("auto_keypad_lock", 1860 | "Auto keypad lock (KeyLck)", val) 1861 | 1862 | tmptot = list_def(_mem.max_talk_time, TALK_TIME_LIST, "1 min") 1863 | val = RadioSettingValueList(TALK_TIME_LIST, None, tmptot) 1864 | tx_t_out_setting = RadioSetting("tot", 1865 | "Max talk, TX Time Out (TxTOut)", val) 1866 | 1867 | tmpbatsave = list_def(_mem.battery_save, BATSAVE_LIST, "1:4") 1868 | val = RadioSettingValueList(BATSAVE_LIST, None, tmpbatsave) 1869 | bat_save_setting = RadioSetting("battery_save", 1870 | "Battery save (BatSav)", val) 1871 | 1872 | val = RadioSettingValueBoolean(_mem.noaa_autoscan) 1873 | noaa_auto_scan_setting = RadioSetting("noaa_autoscan", 1874 | "NOAA Autoscan (NOAA-S)", val) 1875 | 1876 | tmpmicgain = list_def(_mem.mic_gain, MIC_GAIN_LIST, "+8.0dB") 1877 | val = RadioSettingValueList(MIC_GAIN_LIST, None, tmpmicgain) 1878 | mic_gain_setting = RadioSetting("mic_gain", "Mic Gain (Mic)", val) 1879 | 1880 | val = RadioSettingValueBoolean(_mem.mic_bar) 1881 | mic_bar_setting = RadioSetting("mic_bar", 1882 | "Microphone Bar display (MicBar)", val) 1883 | 1884 | tmpchdispmode = list_def(_mem.channel_display_mode, 1885 | CHANNELDISP_LIST, "Frequency") 1886 | val = RadioSettingValueList(CHANNELDISP_LIST, None, tmpchdispmode) 1887 | ch_disp_setting = RadioSetting("channel_display_mode", 1888 | "Channel display mode (ChDisp)", val) 1889 | 1890 | tmpdispmode = list_def(_mem.power_on_dispmode, WELCOME_LIST, 0) 1891 | val = RadioSettingValueList(WELCOME_LIST, None, tmpdispmode) 1892 | p_on_msg_setting = RadioSetting("welcome_mode", 1893 | "Power ON display message (POnMsg)", 1894 | val) 1895 | 1896 | logo1 = str(_mem.logo_line1).strip("\x20\x00\xff") + "\x00" 1897 | logo1 = _getstring(logo1.encode('ascii', errors='ignore'), 0, 12) 1898 | val = RadioSettingValueString(0, 12, logo1) 1899 | logo1_setting = RadioSetting("logo1", 1900 | "Message line 1 ( MAX 12 characters )", 1901 | val) 1902 | 1903 | logo2 = str(_mem.logo_line2).strip("\x20\x00\xff") + "\x00" 1904 | logo2 = _getstring(logo2.encode('ascii', errors='ignore'), 0, 12) 1905 | val = RadioSettingValueString(0, 12, logo2) 1906 | logo2_setting = RadioSetting("logo2", 1907 | "Message line 2 ( MAX 12 characters )", 1908 | val) 1909 | 1910 | tmpbattxt = list_def(_mem.battery_text, BAT_TXT_LIST, 2) 1911 | val = RadioSettingValueList(BAT_TXT_LIST, None, tmpbattxt) 1912 | bat_txt_setting = RadioSetting("battery_text", 1913 | "Battery Level Display (BatTXT)", val) 1914 | 1915 | tmpback = list_def(_mem.backlight_time, BACKLIGHT_LIST, 0) 1916 | val = RadioSettingValueList(BACKLIGHT_LIST, None, tmpback) 1917 | back_lt_setting = RadioSetting("backlight_time", 1918 | "Backlight time (BackLt)", val) 1919 | 1920 | tmpback = list_def(_mem.backlight_min, BACKLIGHT_LVL_LIST, 0) 1921 | val = RadioSettingValueList(BACKLIGHT_LVL_LIST, None, tmpback) 1922 | bl_min_setting = RadioSetting("backlight_min", 1923 | "Backlight level min (BLMin)", val) 1924 | 1925 | tmpback = list_def(_mem.backlight_max, BACKLIGHT_LVL_LIST, 10) 1926 | val = RadioSettingValueList(BACKLIGHT_LVL_LIST, None, tmpback) 1927 | bl_max_setting = RadioSetting("backlight_max", 1928 | "Backlight level max (BLMax)", val) 1929 | 1930 | tmpback = list_def(_mem.backlight_on_TX_RX, BACKLIGHT_TX_RX_LIST, 0) 1931 | val = RadioSettingValueList(BACKLIGHT_TX_RX_LIST, None, tmpback) 1932 | blt_trx_setting = RadioSetting("backlight_on_TX_RX", 1933 | "Backlight on TX/RX (BltTRX)", val) 1934 | 1935 | val = RadioSettingValueBoolean(_mem.button_beep) 1936 | beep_setting = RadioSetting("button_beep", 1937 | "Key press beep sound (Beep)", val) 1938 | 1939 | tmpalarmmode = list_def(_mem.roger_beep, REMENDOFTALK_LIST, 0) 1940 | val = RadioSettingValueList(REMENDOFTALK_LIST, None, tmpalarmmode) 1941 | roger_setting = RadioSetting("roger_beep", 1942 | "End of transmission beep (Roger)", val) 1943 | 1944 | val = RadioSettingValueBoolean(_mem.ste) 1945 | ste_setting = RadioSetting("ste", "Squelch tail elimination (STE)", val) 1946 | 1947 | tmprte = list_def(_mem.rp_ste, RTE_LIST, 0) 1948 | val = RadioSettingValueList(RTE_LIST, None, tmprte) 1949 | rp_ste_setting = RadioSetting("rp_ste", 1950 | "Repeater squelch tail elimination (RP STE)", val) 1951 | 1952 | val = RadioSettingValueBoolean(_mem.AM_fix) 1953 | am_fix_setting = RadioSetting("AM_fix", 1954 | "AM reception fix (AM Fix)", val) 1955 | 1956 | tmpvox = min_max_def((_mem.vox_level + 1) * _mem.vox_switch, 0, 10, 0) 1957 | val = RadioSettingValueList(VOX_LIST, None, tmpvox) 1958 | vox_setting = RadioSetting("vox", "Voice-operated switch (VOX)", val) 1959 | 1960 | tmprxmode = list_def((bool(_mem.crossband) << 1) 1961 | + bool(_mem.dual_watch), 1962 | RXMODE_LIST, 0) 1963 | val = RadioSettingValueList(RXMODE_LIST, None, tmprxmode) 1964 | rx_mode_setting = RadioSetting("rx_mode", "RX Mode (RxMode)", val) 1965 | 1966 | val = RadioSettingValueBoolean(_mem.freq_mode_allowed) 1967 | freq_mode_allowed_setting = RadioSetting("freq_mode_allowed", 1968 | "Frequency mode allowed", val) 1969 | 1970 | tmpscanres = list_def(_mem.scan_resume_mode, SCANRESUME_LIST, 0) 1971 | val = RadioSettingValueList(SCANRESUME_LIST, None, tmpscanres) 1972 | scn_rev_setting = RadioSetting("scan_resume_mode", 1973 | "Scan resume mode (ScnRev)", val) 1974 | 1975 | tmpvoice = list_def(_mem.voice, VOICE_LIST, 0) 1976 | val = RadioSettingValueList(VOICE_LIST, None, tmpvoice) 1977 | voice_setting = RadioSetting("voice", "Voice", val) 1978 | 1979 | tmpalarmmode = list_def(_mem.alarm_mode, ALARMMODE_LIST, 0) 1980 | val = RadioSettingValueList(ALARMMODE_LIST, None, tmpalarmmode) 1981 | alarm_setting = RadioSetting("alarm_mode", "Alarm mode", val) 1982 | 1983 | ################## Extra settings 1984 | 1985 | # S-meter 1986 | tmp_s0 = -int(_mem.s0_level) 1987 | tmp_s9 = -int(_mem.s9_level) 1988 | 1989 | if tmp_s0 not in range(-200, -91) or tmp_s9 not in range(-160, -51) \ 1990 | or tmp_s9 < tmp_s0+9: 1991 | tmp_s0 = -130 1992 | tmp_s9 = -76 1993 | val = RadioSettingValueInteger(-200, -90, tmp_s0) 1994 | s0_level_setting = RadioSetting("s0_level", 1995 | "S-meter S0 level [dBm]", val) 1996 | 1997 | val = RadioSettingValueInteger(-160, -50, tmp_s9) 1998 | s9_level_setting = RadioSetting("s9_level", 1999 | "S-meter S9 level [dBm]", val) 2000 | 2001 | # Battery Type 2002 | tmpbtype = list_def(_mem.Battery_type, BATTYPE_LIST, 0) 2003 | val = RadioSettingValueList(BATTYPE_LIST, BATTYPE_LIST[tmpbtype]) 2004 | bat_type_setting = RadioSetting("Battery_type", 2005 | "Battery Type (BatTyp)", val) 2006 | 2007 | # Power on password 2008 | def validate_password(value): 2009 | value = value.strip(" ") 2010 | if value.isdigit(): 2011 | return value.zfill(6) 2012 | if value != "": 2013 | raise InvalidValueError("Power on password " 2014 | "can only have digits") 2015 | return "" 2016 | 2017 | pswd_str = str(int(_mem.password)).zfill(6) \ 2018 | if _mem.password < 1000000 else "" 2019 | val = RadioSettingValueString(0, 6, pswd_str) 2020 | val.set_validate_callback(validate_password) 2021 | pswd_setting = RadioSetting("password", "Power on password", val) 2022 | 2023 | ################## FM radio 2024 | 2025 | append_label(fmradio, "Channel", "Frequency [MHz]") 2026 | 2027 | for i in range(1, 21): 2028 | fmfreq = _mem.fmfreq[i-1]/10.0 2029 | freq_name = str(fmfreq) 2030 | if fmfreq < FMMIN or fmfreq > FMMAX: 2031 | freq_name = "" 2032 | rs = RadioSetting("FM_" + str(i), "Ch " + str(i), 2033 | RadioSettingValueString(0, 5, freq_name)) 2034 | fmradio.append(rs) 2035 | 2036 | ################## Unlock settings 2037 | 2038 | # F-LOCK 2039 | def validate_int_flock( value): 2040 | mem_val = self._memobj.int_flock 2041 | if mem_val!=7 and value==FLOCK_LIST[7]: 2042 | msg = "\"" + value + "\" can only be enabled from radio menu" 2043 | raise InvalidValueError(msg) 2044 | return value 2045 | 2046 | tmpflock = list_def(_mem.int_flock, FLOCK_LIST, 0) 2047 | val = RadioSettingValueList(FLOCK_LIST, None, tmpflock) 2048 | val.set_validate_callback(validate_int_flock) 2049 | f_lock_setting = RadioSetting("int_flock", 2050 | "TX Frequency Lock (F Lock)", val) 2051 | 2052 | val = RadioSettingValueBoolean(_mem.int_200tx) 2053 | tx200_setting = RadioSetting("int_200tx", 2054 | "Unlock 174-350MHz TX (Tx 200)", val) 2055 | 2056 | val = RadioSettingValueBoolean(_mem.int_350tx) 2057 | tx350_setting = RadioSetting("int_350tx", 2058 | "Unlock 350-400MHz TX (Tx 350)", val) 2059 | 2060 | val = RadioSettingValueBoolean(_mem.int_500tx) 2061 | tx500_setting = RadioSetting("int_500tx", 2062 | "Unlock 500-600MHz TX (Tx 500)", val) 2063 | 2064 | val = RadioSettingValueBoolean(_mem.int_350en) 2065 | en350_setting = RadioSetting("int_350en", 2066 | "Unlock 350-400MHz RX (350 En)", val) 2067 | 2068 | val = RadioSettingValueBoolean(_mem.int_scren) 2069 | en_scrambler_setting = RadioSetting("int_scren", 2070 | "Scrambler enabled (ScraEn)", val) 2071 | 2072 | 2073 | ################## Driver Info 2074 | 2075 | if self.FIRMWARE_VERSION == "": 2076 | firmware = "To get the firmware version please download" \ 2077 | "the image from the radio first" 2078 | else: 2079 | firmware = self.FIRMWARE_VERSION 2080 | 2081 | append_label(roinfo, "Firmware Version", firmware) 2082 | append_label(roinfo, "Driver version", DRIVER_VERSION) 2083 | 2084 | ################## Calibration 2085 | 2086 | 2087 | val = RadioSettingValueBoolean(False) 2088 | def validate_upload_calibration(value): 2089 | if value and not self.upload_calibration: 2090 | ret = wx.MessageBox("This option may brake your radio!!!\n" 2091 | "You are doing this at your own risk.\n" 2092 | "Make sure you have a working calibration backup.\n" 2093 | "Don't use it unless you know what you're doing.", 2094 | "Warning", 2095 | wx.OK | wx.CANCEL | wx.CANCEL_DEFAULT | wx.ICON_WARNING) 2096 | value = ret==wx.OK 2097 | self.upload_calibration = value 2098 | return value 2099 | 2100 | val.set_validate_callback(validate_upload_calibration) 2101 | radio_setting = RadioSetting("upload_calibration", 2102 | "Upload calibration", val) 2103 | calibration.append(radio_setting) 2104 | 2105 | 2106 | radio_setting_group = RadioSettingGroup("squelch_calibration", 2107 | "Squelch") 2108 | calibration.append(radio_setting_group) 2109 | 2110 | bands = {"sqlBand1_3": "Frequency Band 1-3", 2111 | "sqlBand4_7": "Frequency Band 4-7"} 2112 | for bnd, bndn in bands.items(): 2113 | append_label(radio_setting_group, 2114 | "=" * 6 + " " + bndn + " " + "=" * 300, "=" * 300) 2115 | for sql in range(0, 10): 2116 | prefix = "_mem.cal." + bnd + "." 2117 | postfix = "[" + str(sql) + "]" 2118 | append_label(radio_setting_group, "Squelch " + str(sql)) 2119 | 2120 | name = prefix + "openRssiThr" + postfix 2121 | tempval = min_max_def(eval(name), 0, 255, 0) 2122 | val = RadioSettingValueInteger(0, 255, tempval) 2123 | radio_setting = RadioSetting(name, "RSSI threshold open", val) 2124 | radio_setting_group.append(radio_setting) 2125 | 2126 | name = prefix + "closeRssiThr" + postfix 2127 | tempval = min_max_def(eval(name), 0, 255, 0) 2128 | val = RadioSettingValueInteger(0, 255, tempval) 2129 | radio_setting = RadioSetting(name, "RSSI threshold close", val) 2130 | radio_setting_group.append(radio_setting) 2131 | 2132 | name = prefix + "openNoiseThr" + postfix 2133 | tempval = min_max_def(eval(name), 0, 127, 0) 2134 | val = RadioSettingValueInteger(0, 127, tempval) 2135 | radio_setting = RadioSetting(name, "Noise threshold open", val) 2136 | radio_setting_group.append(radio_setting) 2137 | 2138 | name = prefix + "closeNoiseThr" + postfix 2139 | tempval = min_max_def(eval(name), 0, 127, 0) 2140 | val = RadioSettingValueInteger(0, 127, tempval) 2141 | radio_setting = RadioSetting(name, "Noise threshold close", val) 2142 | radio_setting_group.append(radio_setting) 2143 | 2144 | name = prefix + "openGlitchThr" + postfix 2145 | tempval = min_max_def(eval(name), 0, 255, 0) 2146 | val = RadioSettingValueInteger(0, 255, tempval) 2147 | radio_setting = RadioSetting(name, "Glitch threshold open", val) 2148 | radio_setting_group.append(radio_setting) 2149 | 2150 | name = prefix + "closeGlitchThr" + postfix 2151 | tempval = min_max_def(eval(name), 0, 255, 0) 2152 | val = RadioSettingValueInteger(0, 255, tempval) 2153 | radio_setting = RadioSetting(name, "Glitch threshold close", 2154 | val) 2155 | radio_setting_group.append(radio_setting) 2156 | 2157 | 2158 | 2159 | radio_setting_group = RadioSettingGroup("rssi_level_calibration", 2160 | "RSSI levels") 2161 | calibration.append(radio_setting_group) 2162 | 2163 | bands = {"rssiLevelsBands1_2": "1-2 ", "rssiLevelsBands3_7": "3-7 "} 2164 | for bnd, bndn in bands.items(): 2165 | append_label(radio_setting_group, 2166 | "=" * 6 + 2167 | " RSSI levels for QS original small bar graph, bands " 2168 | + bndn + "=" * 300, "=" * 300) 2169 | for lvl in [1, 2, 4, 6]: 2170 | name = "_mem.cal." + bnd + ".level" + str(lvl) 2171 | tempval = min_max_def(eval(name), 0, 65535, 0) 2172 | val = RadioSettingValueInteger(0, 65535, tempval) 2173 | radio_setting = RadioSetting(name, "Level " + str(lvl), val) 2174 | radio_setting_group.append(radio_setting) 2175 | 2176 | 2177 | 2178 | radio_setting_group = RadioSettingGroup("tx_power_calibration", 2179 | "TX power") 2180 | calibration.append(radio_setting_group) 2181 | 2182 | for bnd in range(0,7): 2183 | append_label(radio_setting_group, "=" * 6 + " TX power band " 2184 | + str(bnd+1) + " " + "=" * 300, "=" * 300) 2185 | powers = {"low": "Low", "mid": "Medium", "hi": "High"} 2186 | for pwr, pwrn in powers.items(): 2187 | append_label(radio_setting_group, pwrn) 2188 | bounds = ["lower", "center", "upper"] 2189 | for bound in bounds: 2190 | name = "_mem.cal.txp[" + str(bnd) + "]." + pwr + "." + bound 2191 | tempval = min_max_def(eval(name), 0, 255, 0) 2192 | val = RadioSettingValueInteger(0, 255, tempval) 2193 | radio_setting = RadioSetting(name, bound.capitalize(), val) 2194 | radio_setting_group.append(radio_setting) 2195 | 2196 | 2197 | 2198 | radio_setting_group = RadioSettingGroup("battery_calibration", 2199 | "Battery") 2200 | calibration.append(radio_setting_group) 2201 | 2202 | for lvl in range(0,6): 2203 | name = "_mem.cal.batLvl[" + str(lvl) + "]" 2204 | temp_val = min_max_def(eval(name), 0, 4999, 4999) 2205 | val = RadioSettingValueInteger(0, 4999, temp_val) 2206 | radio_setting = RadioSetting(name, 2207 | "Level " + str(lvl) + 2208 | (" (voltage calibration)" if lvl==3 else ""), val) 2209 | radio_setting_group.append(radio_setting) 2210 | 2211 | 2212 | 2213 | radio_setting_group = RadioSettingGroup("vox_calibration", "VOX") 2214 | calibration.append(radio_setting_group) 2215 | 2216 | for lvl in range(0,10): 2217 | append_label(radio_setting_group, "Level " + str(lvl + 1)) 2218 | 2219 | name = "_mem.cal.vox1Thr[" + str(lvl) + "]" 2220 | val = RadioSettingValueInteger(0, 65535, eval(name)) 2221 | radio_setting = RadioSetting(name, "On", val) 2222 | radio_setting_group.append(radio_setting) 2223 | 2224 | name = "_mem.cal.vox0Thr[" + str(lvl) + "]" 2225 | val = RadioSettingValueInteger(0, 65535, eval(name)) 2226 | radio_setting = RadioSetting(name, "Off", val) 2227 | radio_setting_group.append(radio_setting) 2228 | 2229 | 2230 | 2231 | radio_setting_group = RadioSettingGroup("mic_calibration", 2232 | "Microphone sensitivity") 2233 | calibration.append(radio_setting_group) 2234 | 2235 | for lvl in range(0,5): 2236 | name = "_mem.cal.micLevel[" + str(lvl) + "]" 2237 | tempval = min_max_def(eval(name), 0, 31, 31) 2238 | val = RadioSettingValueInteger(0, 31, tempval) 2239 | radio_setting = RadioSetting(name, "Level " + str(lvl), val) 2240 | radio_setting_group.append(radio_setting) 2241 | 2242 | 2243 | radio_setting_group = RadioSettingGroup("other_calibration", "Other") 2244 | calibration.append(radio_setting_group) 2245 | 2246 | name = "_mem.cal.xtalFreqLow" 2247 | temp_val = min_max_def(eval(name), -1000, 1000, 0) 2248 | val = RadioSettingValueInteger(-1000, 1000, temp_val) 2249 | radio_setting = RadioSetting(name, "Xtal frequecy low", val) 2250 | radio_setting_group.append(radio_setting) 2251 | 2252 | name = "_mem.cal.volumeGain" 2253 | temp_val = min_max_def(eval(name), 0, 63, 58) 2254 | val = RadioSettingValueInteger(0, 63, temp_val) 2255 | radio_setting = RadioSetting(name, "Volume gain", val) 2256 | radio_setting_group.append(radio_setting) 2257 | 2258 | name = "_mem.cal.dacGain" 2259 | temp_val = min_max_def(eval(name), 0, 15, 8) 2260 | val = RadioSettingValueInteger(0, 15, temp_val) 2261 | radio_setting = RadioSetting(name, "DAC gain", val) 2262 | radio_setting_group.append(radio_setting) 2263 | 2264 | ################## LAYOUT 2265 | 2266 | basic.append(squelch_setting) 2267 | basic.append(rx_mode_setting) 2268 | basic.append(call_channel_setting) 2269 | basic.append(auto_keypad_lock_setting) 2270 | basic.append(tx_t_out_setting) 2271 | basic.append(bat_save_setting) 2272 | basic.append(scn_rev_setting) 2273 | if _mem.BUILD_OPTIONS.ENABLE_NOAA: 2274 | basic.append(noaa_auto_scan_setting) 2275 | if _mem.BUILD_OPTIONS.ENABLE_AM_FIX: 2276 | basic.append(am_fix_setting) 2277 | 2278 | append_label(basic, 2279 | "=" * 6 + " Display settings " + "=" * 300, "=" * 300) 2280 | 2281 | basic.append(bat_txt_setting) 2282 | basic.append(mic_bar_setting) 2283 | basic.append(ch_disp_setting) 2284 | basic.append(p_on_msg_setting) 2285 | basic.append(logo1_setting) 2286 | basic.append(logo2_setting) 2287 | 2288 | append_label(basic, 2289 | "=" * 6 + " Backlight settings " + "=" * 300, "=" * 300) 2290 | 2291 | basic.append(back_lt_setting) 2292 | basic.append(bl_min_setting) 2293 | basic.append(bl_max_setting) 2294 | basic.append(blt_trx_setting) 2295 | 2296 | append_label(basic, 2297 | "=" * 6 + " Audio related settings " + "=" * 300, "=" * 300) 2298 | 2299 | if _mem.BUILD_OPTIONS.ENABLE_VOX: 2300 | basic.append(vox_setting) 2301 | basic.append(mic_gain_setting) 2302 | basic.append(beep_setting) 2303 | basic.append(roger_setting) 2304 | basic.append(ste_setting) 2305 | basic.append(rp_ste_setting) 2306 | if _mem.BUILD_OPTIONS.ENABLE_VOICE: 2307 | basic.append(voice_setting) 2308 | if _mem.BUILD_OPTIONS.ENABLE_ALARM: 2309 | basic.append(alarm_setting) 2310 | 2311 | append_label(basic, "=" * 6 + " Radio state " + "=" * 300, "=" * 300) 2312 | 2313 | basic.append(freq0_setting) 2314 | basic.append(freq1_setting) 2315 | basic.append(tx_vfo_setting) 2316 | basic.append(keypad_cock_setting) 2317 | 2318 | advanced.append(freq_mode_allowed_setting) 2319 | advanced.append(bat_type_setting) 2320 | advanced.append(s0_level_setting) 2321 | advanced.append(s9_level_setting) 2322 | if _mem.BUILD_OPTIONS.ENABLE_PWRON_PASSWORD: 2323 | advanced.append(pswd_setting) 2324 | 2325 | if _mem.BUILD_OPTIONS.ENABLE_DTMF_CALLING: 2326 | dtmf.append(sep_code_setting) 2327 | dtmf.append(group_code_setting) 2328 | dtmf.append(first_code_per_setting) 2329 | dtmf.append(spec_per_setting) 2330 | dtmf.append(code_per_setting) 2331 | dtmf.append(code_int_setting) 2332 | if _mem.BUILD_OPTIONS.ENABLE_DTMF_CALLING: 2333 | dtmf.append(ani_id_setting) 2334 | dtmf.append(up_code_setting) 2335 | dtmf.append(dw_code_setting) 2336 | dtmf.append(d_prel_setting) 2337 | dtmf.append(dtmf_side_tone_setting) 2338 | if _mem.BUILD_OPTIONS.ENABLE_DTMF_CALLING: 2339 | dtmf.append(dtmf_resp_setting) 2340 | dtmf.append(d_hold_setting) 2341 | dtmf.append(d_live_setting) 2342 | dtmf.append(perm_kill_setting) 2343 | dtmf.append(kill_code_setting) 2344 | dtmf.append(rev_code_setting) 2345 | dtmf.append(killed_setting) 2346 | 2347 | unlock.append(f_lock_setting) 2348 | unlock.append(tx200_setting) 2349 | unlock.append(tx350_setting) 2350 | unlock.append(tx500_setting) 2351 | unlock.append(en350_setting) 2352 | unlock.append(en_scrambler_setting) 2353 | 2354 | return top 2355 | 2356 | def set_memory(self, memory): 2357 | """ 2358 | Store details about a high-level memory to the memory map 2359 | This is called when a user edits a memory in the UI 2360 | """ 2361 | number = memory.number-1 2362 | 2363 | # Get a low-level memory object mapped to the image 2364 | _mem = self._memobj.channel[number] 2365 | _mem4 = self._memobj 2366 | # empty memory 2367 | if memory.empty: 2368 | _mem.set_raw("\xFF" * 16) 2369 | if number < 200: 2370 | _mem2 = self._memobj.channelname[number] 2371 | _mem2.set_raw("\xFF" * 16) 2372 | _mem4.ch_attr[number].is_scanlist1 = 0 2373 | _mem4.ch_attr[number].is_scanlist2 = 0 2374 | _mem4.ch_attr[number].compander = 0 2375 | _mem4.ch_attr[number].is_free = 1 2376 | _mem4.ch_attr[number].band = 0x7 2377 | return memory 2378 | 2379 | if number < 200: 2380 | _mem4.ch_attr[number].is_scanlist1 = 0 2381 | _mem4.ch_attr[number].is_scanlist2 = 0 2382 | _mem4.ch_attr[number].compander = 0 2383 | _mem4.ch_attr[number].is_free = 1 2384 | _mem4.ch_attr[number].band = 0x7 2385 | 2386 | # find band 2387 | band = self._find_band(memory.freq) 2388 | 2389 | # mode 2390 | tmp_mode = self.get_features().valid_modes.index(memory.mode) 2391 | _mem.modulation = tmp_mode / 2 2392 | _mem.bandwidth = tmp_mode % 2 2393 | if memory.mode == "USB": 2394 | _mem.bandwidth = 1 # narrow 2395 | 2396 | # frequency/offset 2397 | _mem.freq = memory.freq/10 2398 | _mem.offset = memory.offset/10 2399 | 2400 | if memory.duplex == "": 2401 | _mem.offset = 0 2402 | _mem.offsetDir = 0 2403 | elif memory.duplex == '-': 2404 | _mem.offsetDir = FLAGS1_OFFSET_MINUS 2405 | elif memory.duplex == '+': 2406 | _mem.offsetDir = FLAGS1_OFFSET_PLUS 2407 | elif memory.duplex == 'off': 2408 | # we fake tx disable by setting the tx freq to 0 MHz 2409 | _mem.offsetDir = FLAGS1_OFFSET_MINUS 2410 | _mem.offset = _mem.freq 2411 | # set band 2412 | if number < 200: 2413 | _mem4.ch_attr[number].is_free = 0 2414 | _mem4.ch_attr[number].band = band 2415 | 2416 | # channels >200 are the 14 VFO chanells and don't have names 2417 | if number < 200: 2418 | _mem2 = self._memobj.channelname[number] 2419 | tag = memory.name.ljust(10) + "\x00"*6 2420 | _mem2.name = tag # Store the alpha tag 2421 | 2422 | # tone data 2423 | self._set_tone(memory, _mem) 2424 | 2425 | # step 2426 | _mem.step = STEPS.index(memory.tuning_step) 2427 | 2428 | # tx power 2429 | if str(memory.power) == str(UVK5_POWER_LEVELS[2]): 2430 | _mem.txpower = POWER_HIGH 2431 | elif str(memory.power) == str(UVK5_POWER_LEVELS[1]): 2432 | _mem.txpower = POWER_MEDIUM 2433 | else: 2434 | _mem.txpower = POWER_LOW 2435 | 2436 | 2437 | 2438 | 2439 | ######### EXTRA SETTINGS 2440 | 2441 | def get_setting(name, def_val): 2442 | if name in memory.extra: 2443 | return int(memory.extra[name].value) 2444 | return def_val 2445 | 2446 | _mem.busyChLockout = get_setting("busyChLockout", False) 2447 | _mem.dtmf_pttid = get_setting("pttid", 0) 2448 | _mem.freq_reverse = get_setting("frev", False) 2449 | _mem.dtmf_decode = get_setting("dtmfdecode", False) 2450 | _mem.scrambler = get_setting("scrambler", 0) 2451 | _mem4.ch_attr[number].compander = get_setting("compander", 0) 2452 | if number < 200: 2453 | tmp_val = get_setting("scanlists", 0) 2454 | _mem4.ch_attr[number].is_scanlist1 = bool(tmp_val & 1) 2455 | _mem4.ch_attr[number].is_scanlist2 = bool(tmp_val & 2) 2456 | 2457 | return memory 2458 | --------------------------------------------------------------------------------